Skip to content

Commit a00124c

Browse files
authored
Parse the Microsoft Graph spec with the lean OpenAPI parser to fit the Workers memory limit (#1080)
The full Graph OpenAPI document (37MB, ~16.5k operations) parsed through the bespoke eemeli/yaml `YAML.parse` builds a full CST that peaks ~720MB heap and OOMs the 128MB Cloudflare Workers isolate (measured on a real isolate: HTTP 503, outcome=exceededMemory). Delegate the parse to the OpenAPI SDK's `parse`, which uses js-yaml's `load` and does not inline `$ref`s. It peaks ~315MB heap and returns HTTP 200 in-band on the same isolate. `parse` already validates an OpenAPI 3.x object, so the redundant object/version checks are dropped. Serialize the small filtered preset spec as JSON (a YAML subset the OpenAPI parser reads back) instead of `YAML.stringify`, so the plugin no longer needs its own YAML library at all. The eemeli/yaml dependency is removed; the only remaining YAML parser in this path is the SDK's js-yaml.
1 parent 5f08d68 commit a00124c

4 files changed

Lines changed: 34 additions & 30 deletions

File tree

bun.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/plugins/microsoft/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@
5555
"dependencies": {
5656
"@executor-js/plugin-openapi": "workspace:*",
5757
"@executor-js/sdk": "workspace:*",
58-
"lucide-react": "^1.7.0",
59-
"yaml": "^2.7.1"
58+
"lucide-react": "^1.7.0"
6059
},
6160
"devDependencies": {
6261
"@effect/atom-react": "catalog:",

packages/plugins/microsoft/src/sdk/graph.test.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, expect, it } from "@effect/vitest";
2-
import { Effect } from "effect";
3-
import * as YAML from "yaml";
2+
import { Effect, Schema } from "effect";
43

54
import { MICROSOFT_AUTH_TEMPLATE_SLUG, MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES } from "./presets";
65
import {
@@ -9,6 +8,10 @@ import {
98
parseMicrosoftGraphDelegatedScopes,
109
} from "./graph";
1110

11+
// The filtered preset spec is emitted as JSON, so re-read it with Effect Schema
12+
// rather than a YAML parser.
13+
const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
14+
1215
const graphFixture = `
1316
openapi: 3.0.4
1417
info:
@@ -127,7 +130,7 @@ describe("Microsoft Graph OpenAPI filtering", () => {
127130
pathPrefixes: ["/me/messages"],
128131
tagPrefixes: [],
129132
});
130-
const doc = YAML.parse(filtered) as {
133+
const doc = decodeJson(filtered) as {
131134
readonly paths: Record<string, unknown>;
132135
readonly components: {
133136
readonly securitySchemes: Record<string, unknown>;
@@ -153,7 +156,7 @@ describe("Microsoft Graph OpenAPI filtering", () => {
153156
pathPrefixes: ["/me/messages"],
154157
tagPrefixes: [],
155158
});
156-
const doc = YAML.parse(filtered) as {
159+
const doc = decodeJson(filtered) as {
157160
readonly components: {
158161
readonly schemas?: Record<string, unknown>;
159162
readonly securitySchemes: Record<string, unknown>;
@@ -177,7 +180,7 @@ describe("Microsoft Graph OpenAPI filtering", () => {
177180
pathPrefixes: [],
178181
tagPrefixes: [],
179182
});
180-
const doc = YAML.parse(filtered) as {
183+
const doc = decodeJson(filtered) as {
181184
readonly paths: Record<string, unknown>;
182185
};
183186

@@ -196,7 +199,7 @@ describe("Microsoft Graph OpenAPI filtering", () => {
196199
pathPrefixes: [],
197200
tagPrefixes: ["teams."],
198201
});
199-
const doc = YAML.parse(filtered) as {
202+
const doc = decodeJson(filtered) as {
200203
readonly paths: Record<string, unknown>;
201204
};
202205

@@ -213,7 +216,7 @@ describe("Microsoft Graph OpenAPI filtering", () => {
213216
tagPrefixes: [],
214217
fullGraphScopes: ["User.Read", "Mail.ReadWrite", "Notes.ReadWrite"],
215218
});
216-
const doc = YAML.parse(filtered.specText) as {
219+
const doc = decodeJson(filtered.specText) as {
217220
readonly paths: Record<string, unknown>;
218221
readonly security: readonly Record<string, readonly string[]>[];
219222
};
@@ -281,7 +284,7 @@ components:
281284
tagPrefixes: [],
282285
},
283286
);
284-
const doc = YAML.parse(filtered) as {
287+
const doc = decodeJson(filtered) as {
285288
readonly servers: readonly { readonly url: string }[];
286289
readonly paths: Record<string, unknown>;
287290
readonly components: {

packages/plugins/microsoft/src/sdk/graph.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Effect, Option, Schema } from "effect";
22
import type { Layer } from "effect";
33
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4-
import * as YAML from "yaml";
54

65
import { AuthTemplateSlug } from "@executor-js/sdk/core";
76
import {
87
AuthenticationSchema,
98
OpenApiParseError,
9+
parse as parseOpenApiDocument,
1010
type Authentication,
1111
type OpenApiIntegrationConfig,
1212
type ParsedDocument,
@@ -174,6 +174,10 @@ const microsoftOAuthTemplate = (
174174
const isRecord = (value: unknown): value is Record<string, unknown> =>
175175
value !== null && typeof value === "object" && !Array.isArray(value);
176176

177+
// Hoisted so the compiled encoder is built once, not per call. Emits the small
178+
// filtered preset spec as JSON, which the OpenAPI SDK's `parse` reads back.
179+
const encodeOpenApiDocumentToJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
180+
177181
const HTTP_METHODS = new Set(["delete", "get", "patch", "post", "put"]);
178182
const BASE_OAUTH_SCOPES = new Set(["offline_access", "openid", "profile", "email"]);
179183

@@ -538,26 +542,21 @@ export const fetchMicrosoftGraphPermissionsReference = Effect.fn(
538542
const parseMicrosoftGraphOpenApiDocument = (
539543
specText: string,
540544
): Effect.Effect<MicrosoftGraphOpenApiDocument, OpenApiParseError> =>
541-
Effect.gen(function* () {
542-
const parsed = yield* Effect.try({
543-
try: () => YAML.parse(specText) as unknown,
544-
catch: () =>
545+
// Delegate to the OpenAPI SDK's lean js-yaml parser. The bespoke eemeli/yaml
546+
// parser this replaced built a full CST that peaks ~720MB heap on the 37MB
547+
// Graph spec and OOMs the 128MB Cloudflare Workers isolate (measured: HTTP
548+
// 503, outcome=exceededMemory). The SDK parser peaks ~315MB and runs in-band
549+
// on Workers. `parse` already validates an OpenAPI 3.x object and does not
550+
// inline `$ref`s, so peak memory stays bounded for big specs.
551+
parseOpenApiDocument(specText).pipe(
552+
Effect.mapError(
553+
() =>
545554
new OpenApiParseError({
546555
message: "Failed to parse Microsoft Graph OpenAPI document",
547556
}),
548-
});
549-
if (!isRecord(parsed)) {
550-
return yield* new OpenApiParseError({
551-
message: "Microsoft Graph OpenAPI document must be an object",
552-
});
553-
}
554-
if (typeof parsed.openapi !== "string" || !parsed.openapi.startsWith("3.")) {
555-
return yield* new OpenApiParseError({
556-
message: "Microsoft Graph OpenAPI document must be OpenAPI 3.x",
557-
});
558-
}
559-
return parsed as MicrosoftGraphOpenApiDocument;
560-
});
557+
),
558+
Effect.map((doc) => doc as MicrosoftGraphOpenApiDocument),
559+
);
561560

562561
export const buildFilteredMicrosoftGraphOpenApiSpecFromDocument = (
563562
parsed: MicrosoftGraphOpenApiDocument,
@@ -632,8 +631,12 @@ export const buildFilteredMicrosoftGraphOpenApiSpecFromDocument = (
632631
security: [{ [MICROSOFT_AUTH_TEMPLATE_SLUG]: [...scopes] }],
633632
};
634633

634+
// Serialize as JSON, not YAML. The OpenAPI SDK's `parse` reads JSON (a YAML
635+
// subset) on the way back in, so the filtered preset spec round-trips without
636+
// pulling in a second YAML library. This is the small filtered spec, never
637+
// the 37MB source document.
635638
const filteredSpecText = yield* Effect.try({
636-
try: () => YAML.stringify(next),
639+
try: () => encodeOpenApiDocumentToJson(next),
637640
catch: () =>
638641
new OpenApiParseError({
639642
message: "Failed to serialize Microsoft Graph OpenAPI document",

0 commit comments

Comments
 (0)