@immediately-run/worker-transport 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @immediately-run/worker-transport
2
+
3
+ The same-origin **worker MessagePort transport** for immediately.run: the
4
+ `WorkerMessageBus` request/response protocol and the one-time `{ type: 'connect' }`
5
+ MessagePort handshake, extracted into a leaf package so a **worker-owning service
6
+ package** can ship its own worker entry without depending on `sandbox`.
7
+
8
+ ## Why a dedicated leaf package (not the SDK)
9
+
10
+ The transport is imported by the worker-owning packages (today
11
+ `@immediately-run/transpiler`'s Babel worker; later the `tsc`/`eslint`/`prettier`
12
+ authoring services per R3-107) **and** by `sandbox`. Routing it through the SDK would
13
+ pull the *host* (`immediately-run-site-main`) into a build dependency on the app-facing
14
+ client library — which the host↔SDK design deliberately avoids (the SDK is fetched at
15
+ runtime, never a host build input; `SDK_PACKAGING_SPEC §5`). A leaf package keeps the
16
+ transport entanglement-free. See `SIMPLIFIED_DEPLOYMENT_SPEC §14` / roadmap **R3-149**.
17
+
18
+ ## API
19
+
20
+ - `WorkerMessageBus` — the channel-scoped request/response bus over any
21
+ `MessageEndpoint` (a `Worker`, a `MessagePort`, or `self` in a worker).
22
+ - `bindWorkerMessageBus(self, opts)` — installs the `{ type: 'connect' }` handshake on a
23
+ worker's `self`: waits for the parent to transfer a `MessagePort`, starts it, and binds
24
+ a `WorkerMessageBus` on it. This is the exact handshake sandbox's `babel-worker.ts` used,
25
+ generalized so every worker entry installs it identically.
26
+
27
+ ```ts
28
+ import { bindWorkerMessageBus } from '@immediately-run/worker-transport';
29
+
30
+ bindWorkerMessageBus(self, {
31
+ channel: 'sandpack-babel',
32
+ handleRequest: (method, data) =>
33
+ method === 'transform' ? doTransform(data) : Promise.reject(new Error('Unknown method')),
34
+ });
35
+ ```
36
+
37
+ ## Runtime topology
38
+
39
+ The worker is spawned by the **parent page** (not the sandboxed iframe) so the iframe can
40
+ drop `allow-same-origin`. The parent transfers a `MessagePort` entangled with one handed
41
+ into the iframe, so transform requests flow directly between the iframe and the worker
42
+ (the MessagePort identity rule, `HOST_ORIGIN_HARDENING_SPEC §2.4`). This package only owns
43
+ the wire protocol; where the bytes are served from is the consumer's concern.
@@ -0,0 +1,34 @@
1
+ export type RequestHandlerFn = (method: string, ...params: any[]) => Promise<any>;
2
+ export type NotificationHandlerFn = (method: string, data: any) => Promise<any>;
3
+ export type ErrorHandlerFn = (error: Error) => Promise<any>;
4
+ export interface MessageEndpoint {
5
+ postMessage(message: any, ...params: any[]): void;
6
+ addEventListener(type: string, listener: (ev: any) => any): any;
7
+ removeEventListener(type: string, listener: (ev: any) => any): any;
8
+ }
9
+ export interface WorkerMessageBusOpts {
10
+ /** name for the channel */
11
+ channel: string;
12
+ endpoint: MessageEndpoint;
13
+ handleRequest: RequestHandlerFn;
14
+ handleNotification: NotificationHandlerFn;
15
+ handleError: ErrorHandlerFn;
16
+ timeoutMs: number;
17
+ }
18
+ export interface PendingRequest {
19
+ resolve: (result: any) => void;
20
+ reject: (err: Error) => void;
21
+ }
22
+ export declare class WorkerMessageBus {
23
+ private endpoint;
24
+ private handleRequest;
25
+ private handleNotification;
26
+ private handleError;
27
+ private channel;
28
+ private timeoutMs;
29
+ private pendingRequests;
30
+ private _messageId;
31
+ constructor(opts: WorkerMessageBusOpts);
32
+ nextMessageId(): number;
33
+ request(method: string, ...params: any): Promise<any>;
34
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkerMessageBus = void 0;
4
+ // The transport is a leaf package (the @immediately-run/platform-constants shape):
5
+ // it must not depend on any consumer's logger. Error logging here only affects the
6
+ // diagnostics channel, never the bytes a request handler returns, so a plain
7
+ // console.error is behaviour-equivalent to the sandbox's log-level-gated logger.
8
+ const logError = (...data) => {
9
+ console.error(...data);
10
+ };
11
+ const serializeError = (originalError) => {
12
+ if (typeof originalError !== 'object') {
13
+ return { message: originalError };
14
+ }
15
+ else {
16
+ return {
17
+ message: originalError.message,
18
+ name: originalError.name,
19
+ stack: originalError.stack,
20
+ ...originalError,
21
+ };
22
+ }
23
+ };
24
+ const parseError = (serializedError) => {
25
+ const error = new Error(serializedError.message);
26
+ for (const key of Object.keys(serializedError)) {
27
+ // @ts-expect-error indexing Error with a dynamic key to rehydrate custom fields
28
+ error[key] = serializedError[key];
29
+ }
30
+ return error;
31
+ };
32
+ class WorkerMessageBus {
33
+ endpoint;
34
+ handleRequest;
35
+ handleNotification;
36
+ handleError;
37
+ channel;
38
+ timeoutMs;
39
+ pendingRequests = new Map();
40
+ _messageId = 0;
41
+ constructor(opts) {
42
+ this.channel = opts.channel;
43
+ this.endpoint = opts.endpoint;
44
+ this.handleRequest = opts.handleRequest;
45
+ this.handleNotification = opts.handleNotification;
46
+ this.handleError = opts.handleError;
47
+ this.timeoutMs = opts.timeoutMs;
48
+ this.endpoint.addEventListener('message', async (evt) => {
49
+ const data = evt.data;
50
+ if (data.channel !== this.channel) {
51
+ return;
52
+ }
53
+ const messageId = data.id;
54
+ if (data.method) {
55
+ if (messageId == null) {
56
+ this.handleNotification(data.method, data.data);
57
+ }
58
+ else if (data.method && data.params) {
59
+ // It's a request
60
+ try {
61
+ const result = await this.handleRequest(data.method, ...data.params);
62
+ this.endpoint.postMessage({
63
+ id: messageId,
64
+ channel: this.channel,
65
+ result,
66
+ });
67
+ }
68
+ catch (err) {
69
+ logError(err);
70
+ this.endpoint.postMessage({
71
+ id: messageId,
72
+ channel: this.channel,
73
+ error: serializeError(err),
74
+ });
75
+ }
76
+ }
77
+ }
78
+ else if (messageId != null) {
79
+ // It's a response
80
+ const pendingRequest = this.pendingRequests.get(messageId);
81
+ if (!pendingRequest) {
82
+ return;
83
+ }
84
+ if (data.error !== undefined) {
85
+ pendingRequest.reject(parseError(data.error));
86
+ }
87
+ else {
88
+ pendingRequest.resolve(data.result);
89
+ }
90
+ }
91
+ });
92
+ this.endpoint.addEventListener('error', (err) => this.handleError(err));
93
+ }
94
+ nextMessageId() {
95
+ this._messageId++;
96
+ return this._messageId;
97
+ }
98
+ request(method, ...params) {
99
+ const messageId = this.nextMessageId();
100
+ const message = {
101
+ channel: this.channel,
102
+ id: messageId,
103
+ method,
104
+ params,
105
+ };
106
+ const promise = new Promise((resolve, reject) => {
107
+ const timeoutRef = setTimeout(() => {
108
+ this.pendingRequests.delete(messageId);
109
+ reject(new Error(`Request on channel ${this.channel} timed out`));
110
+ }, this.timeoutMs);
111
+ this.pendingRequests.set(messageId, {
112
+ resolve: (data) => {
113
+ clearTimeout(timeoutRef);
114
+ resolve(data);
115
+ },
116
+ reject: (err) => {
117
+ clearTimeout(timeoutRef);
118
+ reject(err);
119
+ },
120
+ });
121
+ });
122
+ this.endpoint.postMessage(message);
123
+ return promise;
124
+ }
125
+ }
126
+ exports.WorkerMessageBus = WorkerMessageBus;
@@ -0,0 +1,13 @@
1
+ import { type RequestHandlerFn, type NotificationHandlerFn, type ErrorHandlerFn } from './WorkerMessageBus';
2
+ export interface BindWorkerMessageBusOpts {
3
+ /** channel name — must match the main-thread bus (e.g. 'sandpack-babel') */
4
+ channel: string;
5
+ handleRequest: RequestHandlerFn;
6
+ handleNotification?: NotificationHandlerFn;
7
+ handleError?: ErrorHandlerFn;
8
+ timeoutMs?: number;
9
+ }
10
+ export declare function bindWorkerMessageBus(self: {
11
+ addEventListener(type: string, listener: (ev: any) => any): any;
12
+ removeEventListener(type: string, listener: (ev: any) => any): any;
13
+ }, opts: BindWorkerMessageBusOpts): void;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bindWorkerMessageBus = bindWorkerMessageBus;
4
+ const WorkerMessageBus_1 = require("./WorkerMessageBus");
5
+ // The one-time `{ type: 'connect' }` MessagePort handshake, extracted verbatim
6
+ // from sandbox's babel-worker.ts so every worker-owning package (transpiler's
7
+ // Babel worker, later the authoring services) installs it identically.
8
+ //
9
+ // This worker is created by the *parent* page (not the sandboxed iframe) so the
10
+ // iframe can drop `allow-same-origin`. The parent hands the worker a `MessagePort`
11
+ // entangled with one transferred into the iframe, so transform requests flow
12
+ // directly between the iframe and this worker without the parent relaying them.
13
+ // We wait for that one-time `{ type: 'connect' }` handshake, then talk over the
14
+ // transferred port instead of `self`.
15
+ function bindWorkerMessageBus(self, opts) {
16
+ const { channel, handleRequest, handleNotification = () => Promise.resolve(), handleError = () => Promise.resolve(), timeoutMs = 30000, } = opts;
17
+ self.addEventListener('message', function onConnect(evt) {
18
+ if (evt.data && evt.data.type === 'connect') {
19
+ const port = evt.ports && evt.ports[0];
20
+ if (port) {
21
+ self.removeEventListener('message', onConnect);
22
+ port.start();
23
+ new WorkerMessageBus_1.WorkerMessageBus({
24
+ channel,
25
+ endpoint: port,
26
+ handleRequest,
27
+ handleNotification,
28
+ handleError,
29
+ timeoutMs,
30
+ });
31
+ }
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,2 @@
1
+ export { WorkerMessageBus, type MessageEndpoint, type WorkerMessageBusOpts, type RequestHandlerFn, type NotificationHandlerFn, type ErrorHandlerFn, type PendingRequest, } from './WorkerMessageBus';
2
+ export { bindWorkerMessageBus, type BindWorkerMessageBusOpts, } from './connect';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ // @immediately-run/worker-transport — the leaf package that owns the same-origin
3
+ // worker MessagePort transport, so a worker-owning service package (the transpiler's
4
+ // Babel worker; later tsc/eslint/prettier per R3-107) can ship its own worker entry
5
+ // without depending on `sandbox`. A leaf package, NOT the SDK: routing the transport
6
+ // through the SDK would pull the *host* (immediately-run-site-main) into a build
7
+ // dependency on the app-facing client lib, which the host↔SDK design deliberately
8
+ // avoids (SDK_PACKAGING_SPEC §5). See SIMPLIFIED_DEPLOYMENT_SPEC §14 / R3-149.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.bindWorkerMessageBus = exports.WorkerMessageBus = void 0;
11
+ var WorkerMessageBus_1 = require("./WorkerMessageBus");
12
+ Object.defineProperty(exports, "WorkerMessageBus", { enumerable: true, get: function () { return WorkerMessageBus_1.WorkerMessageBus; } });
13
+ var connect_1 = require("./connect");
14
+ Object.defineProperty(exports, "bindWorkerMessageBus", { enumerable: true, get: function () { return connect_1.bindWorkerMessageBus; } });
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@immediately-run/worker-transport",
3
+ "version": "0.1.0",
4
+ "description": "The same-origin worker MessagePort transport (WorkerMessageBus + the {type:'connect'} handshake), extracted so a worker-owning service package can ship its own worker entry without depending on `sandbox`. A leaf package, NOT the SDK (SIMPLIFIED_DEPLOYMENT_SPEC §14 / R3-149).",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/immediately-run/immediately-run-worker-transport.git"
9
+ },
10
+ "type": "commonjs",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "test": "jest",
26
+ "lint": "eslint src test",
27
+ "typecheck": "tsc --noEmit",
28
+ "prepare": "npm run build"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^9.18.0",
32
+ "@types/jest": "^29.5.14",
33
+ "@types/node": "^20.17.0",
34
+ "eslint": "^9.18.0",
35
+ "jest": "^29.7.0",
36
+ "ts-jest": "^29.2.5",
37
+ "typescript": "^5.9.3",
38
+ "typescript-eslint": "^8.20.0"
39
+ }
40
+ }