Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ self.MonacoEnvironment = {
/**
* We're building into the dist/ folder. When application starts on
* URL=https://example.com then SwaggerEditor will look for
* `apidom.worker.js` on https://example.com/dist/apidom.worker.js and
* `editor.worker` on https://example.com/dist/editor.worker.js.
* `apidom.worker.js` on https://example.com/dist/apidom.worker.js,
* `editor.worker` on https://example.com/dist/editor.worker.js and
* `asyncapi-parser.worker.js` on https://example.com/dist/asyncapi-parser.worker.js.
*/
baseUrl: `${document.baseURI || location.href}dist/`,
}
Expand Down Expand Up @@ -117,6 +118,7 @@ module.exports = {
app: './index.js',
'apidom.worker': 'swagger-editor/apidom.worker',
'editor.worker': 'swagger-editor/editor.worker',
'asyncapi-parser.worker': 'swagger-editor/asyncapi-parser.worker',
},
output: {
globalObject: 'self',
Expand Down Expand Up @@ -249,6 +251,10 @@ module.exports = {
{
from: 'node_modules/swagger-editor/dist/umd/editor.worker.js',
to: 'static/js',
},
{
from: 'node_modules/swagger-editor/dist/umd/asyncapi-parser.worker.js',
to: 'static/js',
}
]
}),
Expand Down Expand Up @@ -456,6 +462,9 @@ SwaggerEditor maps its [build artifacts](#build-artifacts) in `package.json` fil
},
"./editor.worker": {
"browser": "./dist/esm/editor.worker.js"
},
"./asyncapi-parser.worker": {
"browser": "./dist/esm/asyncapi-parser.worker.js"
}
}
```
Expand Down
97 changes: 90 additions & 7 deletions docs/workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

## Overview

SwaggerEditor uses two Web Workers:
SwaggerEditor uses three Web Workers:

| Worker | Label | Purpose |
|--------|-------|---------|
| `editor.worker` | `editorWorkerService` | Monaco core text-model operations (diffing, link detection, bracket matching) |
| `apidom.worker` | `apidom` | ApiDOM language server (validation, hover, completion, semantic tokens) |
| `asyncapi-parser.worker` | — | AsyncAPI spec parsing off the main thread (Comlink-based, not Monaco-managed) |

Monaco's `StandaloneWebWorkerService` dispatches to whichever worker matches a given label via `MonacoEnvironment.getWorker`.
Monaco's `StandaloneWebWorkerService` dispatches to whichever worker matches a given label via `MonacoEnvironment.getWorker`. The AsyncAPI parser worker is not Monaco-managed and is spawned directly by the `editor-preview-asyncapi` plugin.

---

Expand Down Expand Up @@ -132,6 +133,86 @@ Output: `build/static/js/apidom.worker.[hash].js`. The hashed URL is injected au

---

## AsyncAPI Parser Worker

### Purpose

Offloads `@asyncapi/parser` parsing off the main thread so large or complex AsyncAPI specs don't block the UI. Managed by the `editor-preview-asyncapi` plugin via [Comlink](https://github.com/GoogleChromeLabs/comlink), not by Monaco.

### How it is loaded

`src/plugins/editor-preview-asyncapi/worker/parser-worker-proxy.ts` holds a lazy singleton:

```ts
import * as Comlink from 'comlink';
import AsyncAPIParserWorkerConstructor from './asyncapi-parser.worker.ts?worker';

let proxy = null;

const getParserProxy = async (parserOptions) => {
if (!proxy) {
proxy = Comlink.wrap(new AsyncAPIParserWorkerConstructor());
await proxy.init(wrapResolvers(parserOptions));
}
return proxy;
};
```

The `?worker` import is intercepted by `vite/plugins/rewrite-editor-worker-import.js` — the same virtual-constructor mechanism used by the Monaco workers — so the worker URL resolves correctly in both dev and production.

### Passing custom `parserOptions`

`getParserProxy(parserOptions)` accepts a subset of `@asyncapi/parser`'s `ParserOptions`. Fields that contain functions (e.g. custom `__unstable.resolver.resolvers`) must be wrapped with `Comlink.proxy()` **before** being passed to the worker, so they execute on the main thread and retain access to the Redux store, `document`, etc.:

```js
import * as Comlink from 'comlink';

const parserOptions = {
__unstable: {
resolver: {
resolvers: [httpsFetchResolver, httpFetchResolver].map((r) => ({
schema: r.schema,
order: r.order,
canRead: r.canRead,
read: Comlink.proxy(r.read.bind(r)),
})),
},
},
};
```

`parser-worker-proxy.ts` applies this wrapping automatically via its `wrapResolvers` helper — consumers pass plain resolver objects and the proxy handles the `Comlink.proxy()` calls.

`parserOptions` are applied once when the proxy is first created. Changing them after the first `parse()` call requires re-creating the proxy (not currently supported).

### `Uri` transfer handler

`@asyncapi/parser` passes `urijs` `Uri` objects to resolver `read()` and `canRead()` functions. `Uri` class instances don't survive structured clone, so the worker registers a Comlink transfer handler at module load that converts `Uri → string` transparently before the argument crosses the boundary:

```ts
Comlink.transferHandlers.set('URI', {
canHandle: (obj) => obj instanceof Uri,
serialize: (uri) => [uri.toString(), []],
deserialize: (str) => str,
});
```

The main thread `read(uri)` functions receive the string. `uri.toString()` on a string is a no-op, so existing resolver implementations work without modification.

### Dev mode

Loaded via the virtual constructor module (same as Monaco workers). Vite's transform middleware rewrites bare specifiers inside the worker source.

### Production (app build)

Built by Vite's worker builder via the `?worker` import. Output: `build/static/js/asyncapi-parser.worker.[hash].js`.

### ESM / UMD library builds

Built as a separate self-contained bundle by `vite/scripts/build-bundle-esm.js` and `build-bundle-umd.js` using `asyncapiParserWorkerConfig`. Both configs include `fsShimPlugin` and `nodePolyfills()` to satisfy `@asyncapi/parser`'s Node.js `fs` references in the browser bundle.

---

## `MonacoEnvironment` Setup Order

`after-load.js` runs first and sets `getWorker`. It spreads any existing `MonacoEnvironment` at the end, allowing a consuming app to pre-set overrides (e.g. `baseUrl`):
Expand Down Expand Up @@ -199,12 +280,14 @@ Pre-declaring these avoids a cold-start dep-optimization reload on the first pag
```
dist/
esm/
apidom.worker.js ← self-contained ESM worker bundle
editor.worker.js ← self-contained ESM worker bundle
index.js ← library entry
apidom.worker.js ← self-contained ESM worker bundle
editor.worker.js ← self-contained ESM worker bundle
asyncapi-parser.worker.js ← self-contained ESM worker bundle
index.js ← library entry
umd/
apidom.worker.js ← self-contained IIFE worker bundle
editor.worker.js ← self-contained IIFE worker bundle
apidom.worker.js ← self-contained IIFE worker bundle
editor.worker.js ← self-contained IIFE worker bundle
asyncapi-parser.worker.js ← self-contained IIFE worker bundle
index.js
```

Expand Down
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"axios": "1.16.0",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"comlink": "^4.4.2",
"deep-extend": "=0.6.0",
"file-dialog": "^0.0.8",
"immutable": "^3.8.3",
Expand Down Expand Up @@ -186,6 +187,7 @@
"source-map-explorer": "^2.5.3",
"start-server-and-test": "^2.0.11",
"typescript": "^6.0.3",
"urijs": "^1.19.11",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-static-copy": "^4.1.0",
Expand Down
43 changes: 12 additions & 31 deletions src/plugins/editor-preview-asyncapi/actions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Parser } from '@asyncapi/parser';
import { OpenAPISchemaParser } from '@asyncapi/openapi-schema-parser';
import { AvroSchemaParser } from '@asyncapi/avro-schema-parser';
import { ProtoBuffSchemaParser } from '@asyncapi/protobuf-schema-parser';
import { toAsyncAPIDocument } from '@asyncapi/parser';

import { Raml10SchemaParser } from './util/parsers/raml-1-0-parser.js';
import getParserProxy from './worker/parser-worker-proxy.ts';

/**
* Action types.
Expand Down Expand Up @@ -49,46 +46,31 @@ export const parseFailure = ({ error, parseResult, content, requestId }) => ({
* Async thunks.
*/

export const parse = (content, options = {}) => {
/**
* We give ability to fully distinguish between parser and parse options.
* If parser or parse options are not provided, we will use the options object as it is.
*/
const { parserOptions, parseOptions } = options;
const schemaParsers = [
OpenAPISchemaParser(),
AvroSchemaParser(),
Raml10SchemaParser(),
ProtoBuffSchemaParser(),
];
const parser = new Parser({ schemaParsers, ...(parserOptions ?? options) });

return async (system) => {
/**
* This code can easily be offloaded to a web worker and allow MRT
* not to be blocked by the detection.
*/
export const parse =
(content, options = {}) =>
async (system) => {
const { editorPreviewAsyncAPIActions, fn } = system;
const requestId = fn.generateRequestId();

editorPreviewAsyncAPIActions.parseStarted({ content, requestId });

try {
const parseResult = await parser.parse(content, parseOptions ?? options);

parseResult.extras = null;
const { parserOptions, parseOptions } = options;
const proxy = await getParserProxy(parserOptions);
const { schema, diagnostics } = await proxy.parse(content, parseOptions ?? options);
const document = schema ? toAsyncAPIDocument(schema) : null;

if (parseResult.document) {
if (document) {
return editorPreviewAsyncAPIActions.parseSuccess({
parseResult,
parseResult: { document, diagnostics },
content,
requestId,
});
}

return editorPreviewAsyncAPIActions.parseFailure({
error: new Error('Document is empty'),
parseResult,
parseResult: { diagnostics },
content,
requestId,
});
Expand All @@ -100,4 +82,3 @@ export const parse = (content, options = {}) => {
});
}
};
};

This file was deleted.

Loading
Loading