@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 +43 -0
- package/dist/WorkerMessageBus.d.ts +34 -0
- package/dist/WorkerMessageBus.js +126 -0
- package/dist/connect.d.ts +13 -0
- package/dist/connect.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/package.json +40 -0
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;
|
package/dist/connect.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|