@riebeckite/runtime 0.0.0-stage → 0.0.36
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/LICENSE +201 -0
- package/README.md +97 -2
- package/README_ja.md +100 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.js +1343 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +617 -0
- package/dist/src/client/errors.d.ts +2 -0
- package/dist/src/client/fallback.d.ts +7 -0
- package/dist/src/client/host.d.ts +10 -0
- package/dist/src/client/iframe.d.ts +16 -0
- package/dist/src/client/main.d.ts +19 -0
- package/dist/src/client/wasm_worker.d.ts +12 -0
- package/dist/src/client/worker.d.ts +31 -0
- package/dist/src/core/abort.d.ts +1 -0
- package/dist/src/core/errors.d.ts +44 -0
- package/dist/src/core/instance.d.ts +32 -0
- package/dist/src/core/outcome.d.ts +7 -0
- package/dist/src/core/payload.d.ts +6 -0
- package/dist/src/core/protocol.d.ts +26 -0
- package/dist/src/core/registry.d.ts +17 -0
- package/dist/src/core/types.d.ts +77 -0
- package/dist/worker.d.ts +37 -0
- package/dist/worker.js +438 -0
- package/package.json +66 -3
package/dist/client.js
ADDED
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var RuntimeError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = "RuntimeError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var RuntimeStateError = class extends RuntimeError {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "RuntimeStateError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var RuntimeFailureError = class extends RuntimeStateError {
|
|
15
|
+
constructor(message, options) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "RuntimeFailureError";
|
|
18
|
+
if (options?.cause !== void 0) this.cause = options.cause;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var RuntimeAbortError = class extends RuntimeError {
|
|
22
|
+
constructor(message = "Interactive runtime operation was aborted.") {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "RuntimeAbortError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var RuntimeTimeoutError = class extends RuntimeError {
|
|
28
|
+
constructor(message = "Interactive runtime operation timed out.") {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "RuntimeTimeoutError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var RuntimePayloadError = class extends RuntimeError {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "RuntimePayloadError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var RuntimeConcurrencyError = class extends RuntimeError {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "RuntimeConcurrencyError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var RuntimeWorkerError = class extends RuntimeError {
|
|
46
|
+
remoteName;
|
|
47
|
+
remoteKind;
|
|
48
|
+
constructor(message, options) {
|
|
49
|
+
super(message, options);
|
|
50
|
+
this.name = "RuntimeWorkerError";
|
|
51
|
+
this.remoteName = options?.remoteName;
|
|
52
|
+
this.remoteKind = options?.remoteKind;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var RuntimeWasmError = class extends RuntimeError {
|
|
56
|
+
trap;
|
|
57
|
+
constructor(message, options) {
|
|
58
|
+
super(message, options);
|
|
59
|
+
this.name = "RuntimeWasmError";
|
|
60
|
+
this.trap = options?.trap ?? false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var RuntimeIframeError = class extends RuntimeError {
|
|
64
|
+
remoteKind;
|
|
65
|
+
constructor(message, options) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.name = "RuntimeIframeError";
|
|
68
|
+
this.remoteKind = options?.remoteKind;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var RuntimeUnsupportedError = class extends RuntimeError {
|
|
72
|
+
constructor(message) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "RuntimeUnsupportedError";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/core/abort.ts
|
|
79
|
+
var knownReasonTypes = [
|
|
80
|
+
RuntimeAbortError,
|
|
81
|
+
RuntimeTimeoutError,
|
|
82
|
+
RuntimeWasmError,
|
|
83
|
+
RuntimeWorkerError,
|
|
84
|
+
RuntimeIframeError,
|
|
85
|
+
RuntimeFailureError
|
|
86
|
+
];
|
|
87
|
+
function abortReasonToError(signal) {
|
|
88
|
+
const reason = signal.reason;
|
|
89
|
+
if (reason instanceof Error) {
|
|
90
|
+
for (const type of knownReasonTypes) {
|
|
91
|
+
if (reason instanceof type) return reason;
|
|
92
|
+
}
|
|
93
|
+
if (reason.message) return new RuntimeAbortError(reason.message);
|
|
94
|
+
}
|
|
95
|
+
return new RuntimeAbortError();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/core/outcome.ts
|
|
99
|
+
var RuntimeTransfer = class {
|
|
100
|
+
constructor(value, transfer = []) {
|
|
101
|
+
this.value = value;
|
|
102
|
+
this.transfer = transfer;
|
|
103
|
+
}
|
|
104
|
+
value;
|
|
105
|
+
transfer;
|
|
106
|
+
};
|
|
107
|
+
function transferOutput(value, transfer = []) {
|
|
108
|
+
return new RuntimeTransfer(value, transfer);
|
|
109
|
+
}
|
|
110
|
+
function isRuntimeTransfer(value) {
|
|
111
|
+
return value instanceof RuntimeTransfer;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/core/payload.ts
|
|
115
|
+
var MAX_DEPTH = 16;
|
|
116
|
+
var MAX_ENTRIES = 1e5;
|
|
117
|
+
var encoder = new TextEncoder();
|
|
118
|
+
function estimatePayloadSize(value) {
|
|
119
|
+
return estimate(value, 0, { entries: 0 }, /* @__PURE__ */ new WeakSet());
|
|
120
|
+
}
|
|
121
|
+
function assertPayloadSize(value, limit, label) {
|
|
122
|
+
if (limit <= 0) return;
|
|
123
|
+
const size = estimatePayloadSize(value);
|
|
124
|
+
if (size > limit) {
|
|
125
|
+
throw new RuntimePayloadError(
|
|
126
|
+
`${label} is ${size} bytes, exceeding the ${limit} byte runtime limit.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function estimate(value, depth, counter, ancestors) {
|
|
131
|
+
counter.entries += 1;
|
|
132
|
+
if (counter.entries > MAX_ENTRIES) {
|
|
133
|
+
throw new RuntimePayloadError(
|
|
134
|
+
"Runtime payload contains too many entries to measure."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (value === void 0 || value === null) return 0;
|
|
138
|
+
if (typeof value === "string") return encoder.encode(value).byteLength;
|
|
139
|
+
if (typeof value === "number") return 8;
|
|
140
|
+
if (typeof value === "boolean") return 1;
|
|
141
|
+
if (typeof value === "bigint") return 16;
|
|
142
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
143
|
+
throw new RuntimePayloadError(
|
|
144
|
+
"Runtime payload is not structured-cloneable."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (value instanceof ArrayBuffer) return value.byteLength;
|
|
148
|
+
if (ArrayBuffer.isView(value)) return value.byteLength;
|
|
149
|
+
if (value instanceof Date) return 8;
|
|
150
|
+
if (depth >= MAX_DEPTH) {
|
|
151
|
+
throw new RuntimePayloadError("Runtime payload is nested too deeply.");
|
|
152
|
+
}
|
|
153
|
+
const objectValue = value;
|
|
154
|
+
if (ancestors.has(objectValue)) {
|
|
155
|
+
throw new RuntimePayloadError("Runtime payload must not contain cycles.");
|
|
156
|
+
}
|
|
157
|
+
ancestors.add(objectValue);
|
|
158
|
+
let total = 0;
|
|
159
|
+
if (value instanceof Map) {
|
|
160
|
+
for (const [key, item] of value) {
|
|
161
|
+
total += estimate(key, depth + 1, counter, ancestors);
|
|
162
|
+
total += estimate(item, depth + 1, counter, ancestors);
|
|
163
|
+
}
|
|
164
|
+
} else if (value instanceof Set) {
|
|
165
|
+
for (const item of value) {
|
|
166
|
+
total += estimate(item, depth + 1, counter, ancestors);
|
|
167
|
+
}
|
|
168
|
+
} else if (Array.isArray(value)) {
|
|
169
|
+
for (const item of value) {
|
|
170
|
+
total += estimate(item, depth + 1, counter, ancestors);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
for (const key of Object.keys(value)) {
|
|
174
|
+
total += encoder.encode(key).byteLength;
|
|
175
|
+
total += estimate(
|
|
176
|
+
value[key],
|
|
177
|
+
depth + 1,
|
|
178
|
+
counter,
|
|
179
|
+
ancestors
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
ancestors.delete(objectValue);
|
|
184
|
+
return total;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/core/instance.ts
|
|
188
|
+
var instanceCounter = 0;
|
|
189
|
+
var RuntimeInstance = class {
|
|
190
|
+
id;
|
|
191
|
+
instanceId;
|
|
192
|
+
kind;
|
|
193
|
+
capabilities;
|
|
194
|
+
driver;
|
|
195
|
+
controller = new AbortController();
|
|
196
|
+
listeners = /* @__PURE__ */ new Set();
|
|
197
|
+
active = /* @__PURE__ */ new Set();
|
|
198
|
+
limits;
|
|
199
|
+
onError;
|
|
200
|
+
currentState = "created";
|
|
201
|
+
requestCount = 0;
|
|
202
|
+
mountPromise;
|
|
203
|
+
disposePromise;
|
|
204
|
+
stopPromise;
|
|
205
|
+
failure;
|
|
206
|
+
constructor(definition, options) {
|
|
207
|
+
this.id = definition.id;
|
|
208
|
+
this.instanceId = createInstanceId(definition.id);
|
|
209
|
+
this.kind = definition.kind;
|
|
210
|
+
this.capabilities = definition.capabilities;
|
|
211
|
+
this.limits = resolveLimits(definition.limits);
|
|
212
|
+
this.onError = options.onError ?? defaultErrorReporter;
|
|
213
|
+
this.driver = definition.create({
|
|
214
|
+
instanceId: this.instanceId,
|
|
215
|
+
signal: this.controller.signal,
|
|
216
|
+
limits: this.limits,
|
|
217
|
+
root: options.root
|
|
218
|
+
});
|
|
219
|
+
if (options.signal?.aborted) {
|
|
220
|
+
void this.dispose();
|
|
221
|
+
} else {
|
|
222
|
+
options.signal?.addEventListener("abort", () => void this.dispose(), {
|
|
223
|
+
once: true
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
get state() {
|
|
228
|
+
return this.currentState;
|
|
229
|
+
}
|
|
230
|
+
get signal() {
|
|
231
|
+
return this.controller.signal;
|
|
232
|
+
}
|
|
233
|
+
onStateChange(listener) {
|
|
234
|
+
this.listeners.add(listener);
|
|
235
|
+
return () => this.listeners.delete(listener);
|
|
236
|
+
}
|
|
237
|
+
mount() {
|
|
238
|
+
if (this.disposePromise) {
|
|
239
|
+
return Promise.reject(this.disposedError());
|
|
240
|
+
}
|
|
241
|
+
if (this.mountPromise) return this.mountPromise;
|
|
242
|
+
if (this.currentState !== "created") {
|
|
243
|
+
return Promise.reject(
|
|
244
|
+
new RuntimeStateError(
|
|
245
|
+
`Cannot mount runtime "${this.id}" from state "${this.currentState}".`
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
this.mountPromise = this.runStart();
|
|
250
|
+
return this.mountPromise;
|
|
251
|
+
}
|
|
252
|
+
async invoke(input, options = {}) {
|
|
253
|
+
if (this.currentState === "failed") {
|
|
254
|
+
throw new RuntimeFailureError(
|
|
255
|
+
`Runtime "${this.id}" failed and cannot be reused; create a new instance.`,
|
|
256
|
+
{ cause: this.failure }
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
if (this.currentState !== "ready" && this.currentState !== "running") {
|
|
260
|
+
throw new RuntimeStateError(
|
|
261
|
+
`Cannot invoke runtime "${this.id}" from state "${this.currentState}".`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (this.active.size >= this.limits.maxConcurrent) {
|
|
265
|
+
throw new RuntimeConcurrencyError(
|
|
266
|
+
`Runtime "${this.id}" rejected an invocation: ${this.active.size} requests are already in flight.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (options.signal?.aborted) throw abortReasonToError(options.signal);
|
|
270
|
+
if (options.capability && !this.capabilities.includes(options.capability)) {
|
|
271
|
+
throw new RuntimeStateError(
|
|
272
|
+
`Runtime "${this.id}" does not declare the "${options.capability}" capability.`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
assertPayloadSize(input, this.limits.maxInputBytes, "Runtime input");
|
|
276
|
+
const requestId = ++this.requestCount;
|
|
277
|
+
const call = new AbortController();
|
|
278
|
+
const unlinkInstance = linkSignal(this.controller.signal, call);
|
|
279
|
+
const unlinkExternal = options.signal ? linkSignal(options.signal, call) : () => {
|
|
280
|
+
};
|
|
281
|
+
const timeoutMs = options.timeoutMs ?? this.limits.invokeTimeoutMs;
|
|
282
|
+
const timer = timeoutMs > 0 ? setTimeout(
|
|
283
|
+
() => call.abort(
|
|
284
|
+
new RuntimeTimeoutError(
|
|
285
|
+
`Runtime "${this.id}" invocation timed out after ${timeoutMs}ms.`
|
|
286
|
+
)
|
|
287
|
+
),
|
|
288
|
+
timeoutMs
|
|
289
|
+
) : void 0;
|
|
290
|
+
this.active.add(requestId);
|
|
291
|
+
this.transition("running");
|
|
292
|
+
try {
|
|
293
|
+
const pending = Promise.resolve(
|
|
294
|
+
this.driver.invoke(input, {
|
|
295
|
+
signal: call.signal,
|
|
296
|
+
requestId,
|
|
297
|
+
capability: options.capability,
|
|
298
|
+
transfer: options.transfer
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
const raw = await awaitWithSignal(pending, call.signal);
|
|
302
|
+
const output = isRuntimeTransfer(raw) ? raw.value : raw;
|
|
303
|
+
assertPayloadSize(output, this.limits.maxOutputBytes, "Runtime output");
|
|
304
|
+
return output;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error instanceof RuntimeFailureError) {
|
|
307
|
+
this.failure = error;
|
|
308
|
+
if (this.currentState === "running") this.transition("failed");
|
|
309
|
+
this.report(error, "invoke");
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
} finally {
|
|
313
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
314
|
+
unlinkExternal();
|
|
315
|
+
unlinkInstance();
|
|
316
|
+
this.active.delete(requestId);
|
|
317
|
+
if (this.active.size === 0 && this.currentState === "running") {
|
|
318
|
+
this.transition("ready");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
dispose() {
|
|
323
|
+
if (this.disposePromise) return this.disposePromise;
|
|
324
|
+
if (this.currentState === "disposed") return Promise.resolve();
|
|
325
|
+
this.disposePromise = this.runStop();
|
|
326
|
+
return this.disposePromise;
|
|
327
|
+
}
|
|
328
|
+
async runStart() {
|
|
329
|
+
this.transition("starting");
|
|
330
|
+
try {
|
|
331
|
+
const start = Promise.resolve(this.driver.start());
|
|
332
|
+
await awaitWithSignal(
|
|
333
|
+
withTimeout(
|
|
334
|
+
start,
|
|
335
|
+
this.limits.startTimeoutMs,
|
|
336
|
+
`Runtime "${this.id}" start timed out.`
|
|
337
|
+
),
|
|
338
|
+
this.controller.signal
|
|
339
|
+
);
|
|
340
|
+
if (this.currentState !== "starting") {
|
|
341
|
+
throw this.disposedError();
|
|
342
|
+
}
|
|
343
|
+
this.transition("ready");
|
|
344
|
+
} catch (error) {
|
|
345
|
+
this.failure = error;
|
|
346
|
+
if (this.currentState === "starting") this.transition("failed");
|
|
347
|
+
this.report(error, "start");
|
|
348
|
+
await this.stopDriver();
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async runStop() {
|
|
353
|
+
if (this.currentState !== "disposed") this.transition("stopping");
|
|
354
|
+
this.controller.abort(new RuntimeAbortError("Runtime was disposed."));
|
|
355
|
+
await this.mountPromise?.catch(() => {
|
|
356
|
+
});
|
|
357
|
+
await this.stopDriver();
|
|
358
|
+
if (this.currentState !== "disposed") this.transition("disposed");
|
|
359
|
+
this.listeners.clear();
|
|
360
|
+
}
|
|
361
|
+
stopDriver() {
|
|
362
|
+
this.stopPromise ??= (async () => {
|
|
363
|
+
try {
|
|
364
|
+
await this.driver.stop();
|
|
365
|
+
} catch (error) {
|
|
366
|
+
this.report(error, "stop");
|
|
367
|
+
}
|
|
368
|
+
})();
|
|
369
|
+
return this.stopPromise;
|
|
370
|
+
}
|
|
371
|
+
disposedError() {
|
|
372
|
+
return new RuntimeAbortError(`Runtime "${this.id}" was disposed.`);
|
|
373
|
+
}
|
|
374
|
+
transition(state) {
|
|
375
|
+
const previous = this.currentState;
|
|
376
|
+
if (previous === state) return;
|
|
377
|
+
this.currentState = state;
|
|
378
|
+
const change = { previous, state };
|
|
379
|
+
for (const listener of this.listeners) {
|
|
380
|
+
try {
|
|
381
|
+
listener(change);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
this.report(error, "state-change");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
report(error, phase) {
|
|
388
|
+
try {
|
|
389
|
+
this.onError(error, phase);
|
|
390
|
+
} catch {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
function resolveLimits(limits) {
|
|
396
|
+
return {
|
|
397
|
+
maxInputBytes: limits.maxInputBytes ?? 4 * 1024 * 1024,
|
|
398
|
+
maxOutputBytes: limits.maxOutputBytes ?? 16 * 1024 * 1024,
|
|
399
|
+
maxConcurrent: limits.maxConcurrent ?? 16,
|
|
400
|
+
invokeTimeoutMs: limits.invokeTimeoutMs ?? 0,
|
|
401
|
+
startTimeoutMs: limits.startTimeoutMs ?? 15e3
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function createInstanceId(id) {
|
|
405
|
+
const random = globalThis.crypto?.randomUUID?.();
|
|
406
|
+
if (random) return `${id}#${random}`;
|
|
407
|
+
instanceCounter += 1;
|
|
408
|
+
return `${id}#${Date.now().toString(36)}-${instanceCounter}`;
|
|
409
|
+
}
|
|
410
|
+
function linkSignal(parent, child) {
|
|
411
|
+
if (parent.aborted) {
|
|
412
|
+
child.abort(parent.reason);
|
|
413
|
+
return () => {
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const listener = () => child.abort(parent.reason);
|
|
417
|
+
parent.addEventListener("abort", listener, { once: true });
|
|
418
|
+
return () => parent.removeEventListener("abort", listener);
|
|
419
|
+
}
|
|
420
|
+
function abortFrom(signal) {
|
|
421
|
+
return abortReasonToError(signal);
|
|
422
|
+
}
|
|
423
|
+
function awaitWithSignal(promise, signal) {
|
|
424
|
+
if (signal.aborted) {
|
|
425
|
+
void promise.catch(() => {
|
|
426
|
+
});
|
|
427
|
+
return Promise.reject(abortFrom(signal));
|
|
428
|
+
}
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
const onAbort = () => {
|
|
431
|
+
cleanup();
|
|
432
|
+
reject(abortFrom(signal));
|
|
433
|
+
};
|
|
434
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
435
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
436
|
+
promise.then(
|
|
437
|
+
(value) => {
|
|
438
|
+
cleanup();
|
|
439
|
+
resolve(value);
|
|
440
|
+
},
|
|
441
|
+
(error) => {
|
|
442
|
+
cleanup();
|
|
443
|
+
reject(error);
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function withTimeout(promise, timeoutMs, message) {
|
|
449
|
+
if (timeoutMs <= 0) return promise;
|
|
450
|
+
return new Promise((resolve, reject) => {
|
|
451
|
+
const timer = setTimeout(
|
|
452
|
+
() => reject(new RuntimeTimeoutError(message)),
|
|
453
|
+
timeoutMs
|
|
454
|
+
);
|
|
455
|
+
promise.then(
|
|
456
|
+
(value) => {
|
|
457
|
+
clearTimeout(timer);
|
|
458
|
+
resolve(value);
|
|
459
|
+
},
|
|
460
|
+
(error) => {
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
reject(error);
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function defaultErrorReporter(error, phase) {
|
|
468
|
+
if (error instanceof RuntimeAbortError) return;
|
|
469
|
+
console.error(`[riebeckite] Interactive runtime ${phase} failed.`, error);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/core/protocol.ts
|
|
473
|
+
var RUNTIME_PROTOCOL = "riebeckite:runtime";
|
|
474
|
+
var RUNTIME_PROTOCOL_VERSION = 1;
|
|
475
|
+
var RUNTIME_MESSAGE_KINDS = [
|
|
476
|
+
"init",
|
|
477
|
+
"invoke",
|
|
478
|
+
"cancel",
|
|
479
|
+
"ready",
|
|
480
|
+
"result",
|
|
481
|
+
"error",
|
|
482
|
+
"fatal",
|
|
483
|
+
"dispose"
|
|
484
|
+
];
|
|
485
|
+
function isRuntimeMessage(value) {
|
|
486
|
+
if (!value || typeof value !== "object") return false;
|
|
487
|
+
const message = value;
|
|
488
|
+
if (message.protocol !== RUNTIME_PROTOCOL) return false;
|
|
489
|
+
if (message.version !== RUNTIME_PROTOCOL_VERSION) return false;
|
|
490
|
+
if (typeof message.instanceId !== "string") return false;
|
|
491
|
+
return RUNTIME_MESSAGE_KINDS.includes(message.kind);
|
|
492
|
+
}
|
|
493
|
+
function serializeRuntimeError(error) {
|
|
494
|
+
if (error instanceof Error) {
|
|
495
|
+
const details = error;
|
|
496
|
+
return {
|
|
497
|
+
name: error.name,
|
|
498
|
+
message: error.message,
|
|
499
|
+
kind: typeof details.kind === "string" ? details.kind : void 0,
|
|
500
|
+
trap: details.trap === true,
|
|
501
|
+
stack: error.stack
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
name: "Error",
|
|
506
|
+
message: typeof error === "string" ? error : "Unknown runtime error."
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/core/registry.ts
|
|
511
|
+
var InteractiveRuntimeRegistry = class {
|
|
512
|
+
definitions = /* @__PURE__ */ new Map();
|
|
513
|
+
register(definition) {
|
|
514
|
+
if (this.definitions.has(definition.id)) {
|
|
515
|
+
throw new RuntimeStateError(
|
|
516
|
+
`Interactive runtime "${definition.id}" is already registered.`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
this.definitions.set(
|
|
520
|
+
definition.id,
|
|
521
|
+
definition
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
has(id) {
|
|
525
|
+
return this.definitions.has(id);
|
|
526
|
+
}
|
|
527
|
+
get(id) {
|
|
528
|
+
const definition = this.definitions.get(id);
|
|
529
|
+
if (!definition) {
|
|
530
|
+
throw new RuntimeStateError(
|
|
531
|
+
`Interactive runtime "${id}" is not registered.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return definition;
|
|
535
|
+
}
|
|
536
|
+
get ids() {
|
|
537
|
+
return [...this.definitions.keys()];
|
|
538
|
+
}
|
|
539
|
+
create(id, options = {}) {
|
|
540
|
+
return new RuntimeInstance(this.get(id), options);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
function defineInteractiveRuntime(definition) {
|
|
544
|
+
if (!definition.id.trim()) {
|
|
545
|
+
throw new RuntimeStateError("Interactive runtime id must not be empty.");
|
|
546
|
+
}
|
|
547
|
+
const capabilities = [...definition.capabilities ?? []];
|
|
548
|
+
if (new Set(capabilities).size !== capabilities.length) {
|
|
549
|
+
throw new RuntimeStateError(
|
|
550
|
+
`Interactive runtime "${definition.id}" declares duplicate capabilities.`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return Object.freeze({
|
|
554
|
+
id: definition.id,
|
|
555
|
+
kind: definition.kind,
|
|
556
|
+
capabilities: Object.freeze(capabilities),
|
|
557
|
+
limits: Object.freeze({ ...definition.limits ?? {} }),
|
|
558
|
+
create: definition.create
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function createInteractiveRuntime(definition, options = {}) {
|
|
562
|
+
return new RuntimeInstance(definition, options);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/core/types.ts
|
|
566
|
+
var RUNTIME_KINDS = [
|
|
567
|
+
"main",
|
|
568
|
+
"worker",
|
|
569
|
+
"wasm-worker",
|
|
570
|
+
"iframe"
|
|
571
|
+
];
|
|
572
|
+
var RUNTIME_STATES = [
|
|
573
|
+
"created",
|
|
574
|
+
"starting",
|
|
575
|
+
"ready",
|
|
576
|
+
"running",
|
|
577
|
+
"stopping",
|
|
578
|
+
"disposed",
|
|
579
|
+
"failed"
|
|
580
|
+
];
|
|
581
|
+
var RUNTIME_CAPABILITIES = [
|
|
582
|
+
"dom",
|
|
583
|
+
"worker",
|
|
584
|
+
"wasm",
|
|
585
|
+
"network",
|
|
586
|
+
"message"
|
|
587
|
+
];
|
|
588
|
+
|
|
589
|
+
// src/client/errors.ts
|
|
590
|
+
function runtimeErrorFromWire(error, fallbackMessage) {
|
|
591
|
+
if (!error) return new RuntimeWorkerError(fallbackMessage);
|
|
592
|
+
const message = error.message || fallbackMessage;
|
|
593
|
+
switch (error.name) {
|
|
594
|
+
case "RuntimeAbortError":
|
|
595
|
+
return new RuntimeAbortError(message);
|
|
596
|
+
case "RuntimeTimeoutError":
|
|
597
|
+
return new RuntimeTimeoutError(message);
|
|
598
|
+
case "RuntimePayloadError":
|
|
599
|
+
return new RuntimePayloadError(message);
|
|
600
|
+
case "RuntimeConcurrencyError":
|
|
601
|
+
return new RuntimeConcurrencyError(message);
|
|
602
|
+
case "RuntimeUnsupportedError":
|
|
603
|
+
return new RuntimeUnsupportedError(message);
|
|
604
|
+
case "RuntimeWasmError":
|
|
605
|
+
return new RuntimeWasmError(message, { trap: error.trap === true });
|
|
606
|
+
case "RuntimeFailureError":
|
|
607
|
+
return new RuntimeFailureError(message);
|
|
608
|
+
case "RuntimeStateError":
|
|
609
|
+
return new RuntimeStateError(message);
|
|
610
|
+
case "RuntimeIframeError":
|
|
611
|
+
return new RuntimeIframeError(message, { remoteKind: error.kind });
|
|
612
|
+
default:
|
|
613
|
+
return new RuntimeWorkerError(message, {
|
|
614
|
+
remoteName: error.name,
|
|
615
|
+
remoteKind: error.kind
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/client/fallback.ts
|
|
621
|
+
var FALLBACK_SELECTOR = "[data-runtime-fallback]";
|
|
622
|
+
var LOADING_SELECTOR = "[data-runtime-loading]";
|
|
623
|
+
var ERROR_SELECTOR = "[data-runtime-error]";
|
|
624
|
+
function createRuntimeFallback(root) {
|
|
625
|
+
if (typeof Element === "undefined" || !(root instanceof Element)) {
|
|
626
|
+
throw new RuntimeUnsupportedError(
|
|
627
|
+
"A runtime fallback requires a DOM element and must not be created during SSR."
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
const fallback = root.querySelector(FALLBACK_SELECTOR);
|
|
631
|
+
const loading = root.querySelector(LOADING_SELECTOR);
|
|
632
|
+
const error = root.querySelector(ERROR_SELECTOR);
|
|
633
|
+
const setBusy = (busy) => {
|
|
634
|
+
if (busy) {
|
|
635
|
+
root.setAttribute("aria-busy", "true");
|
|
636
|
+
} else {
|
|
637
|
+
root.removeAttribute("aria-busy");
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
return {
|
|
641
|
+
showLoading() {
|
|
642
|
+
if (fallback) fallback.hidden = true;
|
|
643
|
+
if (loading) loading.hidden = false;
|
|
644
|
+
if (error) error.hidden = true;
|
|
645
|
+
setBusy(true);
|
|
646
|
+
},
|
|
647
|
+
showReady() {
|
|
648
|
+
if (fallback) fallback.hidden = true;
|
|
649
|
+
if (loading) loading.hidden = true;
|
|
650
|
+
if (error) error.hidden = true;
|
|
651
|
+
setBusy(false);
|
|
652
|
+
},
|
|
653
|
+
showError(cause) {
|
|
654
|
+
if (fallback) fallback.hidden = false;
|
|
655
|
+
if (loading) loading.hidden = true;
|
|
656
|
+
if (error) {
|
|
657
|
+
error.hidden = false;
|
|
658
|
+
error.textContent = cause instanceof Error ? cause.message : String(cause);
|
|
659
|
+
}
|
|
660
|
+
setBusy(false);
|
|
661
|
+
},
|
|
662
|
+
reset() {
|
|
663
|
+
if (fallback) fallback.hidden = false;
|
|
664
|
+
if (loading) loading.hidden = true;
|
|
665
|
+
if (error) {
|
|
666
|
+
error.hidden = true;
|
|
667
|
+
error.textContent = "";
|
|
668
|
+
}
|
|
669
|
+
setBusy(false);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/client/host.ts
|
|
675
|
+
function createClientRuntimeHost(options) {
|
|
676
|
+
const instances = /* @__PURE__ */ new Set();
|
|
677
|
+
let disposed = false;
|
|
678
|
+
return {
|
|
679
|
+
has(id) {
|
|
680
|
+
return options.registry.has(id);
|
|
681
|
+
},
|
|
682
|
+
ids() {
|
|
683
|
+
return options.registry.ids;
|
|
684
|
+
},
|
|
685
|
+
create(id, createOptions = {}) {
|
|
686
|
+
if (disposed) {
|
|
687
|
+
throw new RuntimeStateError(
|
|
688
|
+
`Client runtime host is disposed; cannot create "${id}".`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const instance = options.registry.create(id, {
|
|
692
|
+
root: createOptions.root ?? options.root,
|
|
693
|
+
signal: createOptions.signal ?? options.signal,
|
|
694
|
+
onError: options.onError
|
|
695
|
+
});
|
|
696
|
+
instances.add(instance);
|
|
697
|
+
return instance;
|
|
698
|
+
},
|
|
699
|
+
async dispose() {
|
|
700
|
+
if (disposed) return;
|
|
701
|
+
disposed = true;
|
|
702
|
+
await Promise.allSettled(
|
|
703
|
+
[...instances].map((instance) => instance.dispose())
|
|
704
|
+
);
|
|
705
|
+
instances.clear();
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/client/iframe.ts
|
|
711
|
+
var IFRAME_PROTOCOL = "riebeckite:iframe";
|
|
712
|
+
var IFRAME_PROTOCOL_VERSION = 1;
|
|
713
|
+
var MAX_ACTIVE_IFRAMES = 4;
|
|
714
|
+
var DEFAULT_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; font-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'";
|
|
715
|
+
var ALLOWED_SANDBOX_TOKENS = /* @__PURE__ */ new Set(["allow-scripts", "allow-forms"]);
|
|
716
|
+
var activeIframes = 0;
|
|
717
|
+
function createIframeRuntime(options) {
|
|
718
|
+
return defineInteractiveRuntime({
|
|
719
|
+
id: options.id,
|
|
720
|
+
kind: "iframe",
|
|
721
|
+
capabilities: options.capabilities ?? ["message"],
|
|
722
|
+
limits: {
|
|
723
|
+
...options.limits,
|
|
724
|
+
invokeTimeoutMs: options.limits?.invokeTimeoutMs ?? 1e4
|
|
725
|
+
},
|
|
726
|
+
create(context) {
|
|
727
|
+
let iframe;
|
|
728
|
+
let sessionId;
|
|
729
|
+
let readySeen = false;
|
|
730
|
+
let readyResolve;
|
|
731
|
+
let readyReject;
|
|
732
|
+
let started = false;
|
|
733
|
+
let stopped = false;
|
|
734
|
+
let failed;
|
|
735
|
+
let counted = false;
|
|
736
|
+
const pending = /* @__PURE__ */ new Map();
|
|
737
|
+
const expectedOrigin = options.expectedOrigin ?? "null";
|
|
738
|
+
const targetOrigin = expectedOrigin === "null" ? "*" : expectedOrigin;
|
|
739
|
+
const fail = (error, message) => {
|
|
740
|
+
const failure = error instanceof RuntimeFailureError ? error : new RuntimeFailureError(message, { cause: error });
|
|
741
|
+
failed = failure;
|
|
742
|
+
readyReject?.(failure);
|
|
743
|
+
for (const request of pending.values()) {
|
|
744
|
+
request.unlink();
|
|
745
|
+
request.reject(failure);
|
|
746
|
+
}
|
|
747
|
+
pending.clear();
|
|
748
|
+
cleanup();
|
|
749
|
+
};
|
|
750
|
+
const takePending2 = (requestId) => {
|
|
751
|
+
if (requestId === void 0) return void 0;
|
|
752
|
+
const request = pending.get(requestId);
|
|
753
|
+
if (!request) return void 0;
|
|
754
|
+
pending.delete(requestId);
|
|
755
|
+
request.unlink();
|
|
756
|
+
return request;
|
|
757
|
+
};
|
|
758
|
+
const onMessage = (event) => {
|
|
759
|
+
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
760
|
+
if (event.origin !== expectedOrigin) return;
|
|
761
|
+
const message = event.data;
|
|
762
|
+
if (!message || typeof message !== "object") return;
|
|
763
|
+
if (message.protocol !== IFRAME_PROTOCOL) return;
|
|
764
|
+
if (message.version !== IFRAME_PROTOCOL_VERSION) return;
|
|
765
|
+
if (!sessionId || message.sessionId !== sessionId) return;
|
|
766
|
+
switch (message.kind) {
|
|
767
|
+
case "ready":
|
|
768
|
+
if (!readySeen) {
|
|
769
|
+
readySeen = true;
|
|
770
|
+
readyResolve?.();
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
case "result": {
|
|
774
|
+
const request = takePending2(message.requestId);
|
|
775
|
+
if (!request) return;
|
|
776
|
+
const transfer = message.transfer;
|
|
777
|
+
request.resolve(
|
|
778
|
+
transfer && transfer.length > 0 ? transferOutput(message.payload, transfer) : message.payload
|
|
779
|
+
);
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case "error": {
|
|
783
|
+
const request = takePending2(message.requestId);
|
|
784
|
+
if (!request) return;
|
|
785
|
+
request.reject(
|
|
786
|
+
runtimeErrorFromWire(
|
|
787
|
+
message.error,
|
|
788
|
+
`Iframe runtime "${options.id}" request failed.`
|
|
789
|
+
)
|
|
790
|
+
);
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
default:
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
const onLoad = () => {
|
|
798
|
+
if (!iframe) return;
|
|
799
|
+
if (readySeen) {
|
|
800
|
+
fail(
|
|
801
|
+
new RuntimeIframeError(
|
|
802
|
+
`Iframe runtime "${options.id}" navigated unexpectedly.`
|
|
803
|
+
),
|
|
804
|
+
`Iframe runtime "${options.id}" navigated unexpectedly.`
|
|
805
|
+
);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (!sessionId) return;
|
|
809
|
+
iframe.contentWindow?.postMessage(
|
|
810
|
+
{
|
|
811
|
+
protocol: IFRAME_PROTOCOL,
|
|
812
|
+
version: IFRAME_PROTOCOL_VERSION,
|
|
813
|
+
sessionId,
|
|
814
|
+
kind: "init"
|
|
815
|
+
},
|
|
816
|
+
targetOrigin
|
|
817
|
+
);
|
|
818
|
+
};
|
|
819
|
+
const start = () => {
|
|
820
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
821
|
+
throw new RuntimeUnsupportedError(
|
|
822
|
+
`Iframe runtime "${options.id}" requires a browser DOM.`
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
if (activeIframes >= MAX_ACTIVE_IFRAMES) {
|
|
826
|
+
throw new RuntimeConcurrencyError(
|
|
827
|
+
`Iframe runtime "${options.id}" cannot start: ${MAX_ACTIVE_IFRAMES} iframe runtimes are already active.`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
const sandbox = resolveSandbox(options.sandbox, options.id);
|
|
831
|
+
const container = resolveContainer(options.container, context.root);
|
|
832
|
+
if (!container) {
|
|
833
|
+
throw new RuntimeUnsupportedError(
|
|
834
|
+
`Iframe runtime "${options.id}" has no document body to mount into.`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const ready = new Promise((resolve, reject) => {
|
|
838
|
+
readyResolve = resolve;
|
|
839
|
+
readyReject = reject;
|
|
840
|
+
});
|
|
841
|
+
sessionId = createSessionId();
|
|
842
|
+
iframe = document.createElement("iframe");
|
|
843
|
+
iframe.setAttribute("sandbox", sandbox.join(" "));
|
|
844
|
+
iframe.setAttribute("referrerpolicy", "no-referrer");
|
|
845
|
+
iframe.setAttribute("title", options.title ?? "Interactive runtime");
|
|
846
|
+
iframe.setAttribute(
|
|
847
|
+
"style",
|
|
848
|
+
"width:100%;height:100%;border:0;display:block"
|
|
849
|
+
);
|
|
850
|
+
iframe.srcdoc = buildDocument({
|
|
851
|
+
html: options.html,
|
|
852
|
+
title: options.title ?? "Interactive runtime",
|
|
853
|
+
csp: options.csp ?? DEFAULT_CSP,
|
|
854
|
+
sessionId
|
|
855
|
+
});
|
|
856
|
+
iframe.addEventListener("load", onLoad);
|
|
857
|
+
window.addEventListener("message", onMessage);
|
|
858
|
+
activeIframes += 1;
|
|
859
|
+
counted = true;
|
|
860
|
+
try {
|
|
861
|
+
container.appendChild(iframe);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
cleanup();
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
started = true;
|
|
867
|
+
return ready;
|
|
868
|
+
};
|
|
869
|
+
const invoke = (input, requestContext) => {
|
|
870
|
+
if (failed) return Promise.reject(failed);
|
|
871
|
+
if (stopped) {
|
|
872
|
+
return Promise.reject(
|
|
873
|
+
new RuntimeAbortError(
|
|
874
|
+
`Iframe runtime "${options.id}" was disposed.`
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
if (!started || !iframe) {
|
|
879
|
+
return Promise.reject(
|
|
880
|
+
new RuntimeStateError(
|
|
881
|
+
`Iframe runtime "${options.id}" is not started.`
|
|
882
|
+
)
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
const { signal, requestId } = requestContext;
|
|
886
|
+
if (signal.aborted) {
|
|
887
|
+
return Promise.reject(abortReasonToError(signal));
|
|
888
|
+
}
|
|
889
|
+
return new Promise(
|
|
890
|
+
(resolve, reject) => {
|
|
891
|
+
const onAbort = () => {
|
|
892
|
+
post({ kind: "cancel", requestId });
|
|
893
|
+
takePending2(requestId);
|
|
894
|
+
reject(abortReasonToError(signal));
|
|
895
|
+
};
|
|
896
|
+
const unlink = () => signal.removeEventListener("abort", onAbort);
|
|
897
|
+
pending.set(requestId, { resolve, reject, unlink });
|
|
898
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
899
|
+
try {
|
|
900
|
+
post(
|
|
901
|
+
{
|
|
902
|
+
kind: "invoke",
|
|
903
|
+
requestId,
|
|
904
|
+
capability: requestContext.capability,
|
|
905
|
+
payload: input
|
|
906
|
+
},
|
|
907
|
+
requestContext.transfer
|
|
908
|
+
);
|
|
909
|
+
} catch (error) {
|
|
910
|
+
takePending2(requestId);
|
|
911
|
+
reject(error);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
};
|
|
916
|
+
const post = (message, transfer) => {
|
|
917
|
+
if (!iframe?.contentWindow || !sessionId) return;
|
|
918
|
+
iframe.contentWindow.postMessage(
|
|
919
|
+
{
|
|
920
|
+
protocol: IFRAME_PROTOCOL,
|
|
921
|
+
version: IFRAME_PROTOCOL_VERSION,
|
|
922
|
+
sessionId,
|
|
923
|
+
...message
|
|
924
|
+
},
|
|
925
|
+
targetOrigin,
|
|
926
|
+
transfer ? [...transfer] : void 0
|
|
927
|
+
);
|
|
928
|
+
};
|
|
929
|
+
const cleanup = () => {
|
|
930
|
+
window.removeEventListener("message", onMessage);
|
|
931
|
+
if (iframe) {
|
|
932
|
+
iframe.removeEventListener("load", onLoad);
|
|
933
|
+
iframe.remove();
|
|
934
|
+
iframe = void 0;
|
|
935
|
+
}
|
|
936
|
+
if (counted) {
|
|
937
|
+
counted = false;
|
|
938
|
+
activeIframes = Math.max(0, activeIframes - 1);
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const stop = () => {
|
|
942
|
+
if (stopped) return;
|
|
943
|
+
stopped = true;
|
|
944
|
+
const error = new RuntimeAbortError(
|
|
945
|
+
`Iframe runtime "${options.id}" was disposed.`
|
|
946
|
+
);
|
|
947
|
+
for (const request of pending.values()) {
|
|
948
|
+
request.unlink();
|
|
949
|
+
request.reject(error);
|
|
950
|
+
}
|
|
951
|
+
pending.clear();
|
|
952
|
+
readyReject?.(error);
|
|
953
|
+
cleanup();
|
|
954
|
+
started = false;
|
|
955
|
+
};
|
|
956
|
+
return { start, invoke, stop };
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function resolveSandbox(tokens, id) {
|
|
961
|
+
const sandbox = [...tokens ?? ["allow-scripts"]];
|
|
962
|
+
if (!sandbox.includes("allow-scripts")) {
|
|
963
|
+
throw new RuntimeIframeError(
|
|
964
|
+
`Iframe runtime "${id}" requires the "allow-scripts" sandbox token.`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
for (const token of sandbox) {
|
|
968
|
+
if (!ALLOWED_SANDBOX_TOKENS.has(token)) {
|
|
969
|
+
throw new RuntimeIframeError(
|
|
970
|
+
`Iframe runtime "${id}" rejects the "${token}" sandbox token.`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (sandbox.includes("allow-scripts") && sandbox.includes("allow-same-origin")) {
|
|
975
|
+
throw new RuntimeIframeError(
|
|
976
|
+
`Iframe runtime "${id}" must not combine "allow-scripts" with "allow-same-origin".`
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
return sandbox;
|
|
980
|
+
}
|
|
981
|
+
function resolveContainer(container, root) {
|
|
982
|
+
if (typeof container === "function") return container();
|
|
983
|
+
if (typeof container === "string") {
|
|
984
|
+
return document.querySelector(container);
|
|
985
|
+
}
|
|
986
|
+
if (container) return container;
|
|
987
|
+
if (root instanceof Element) return root;
|
|
988
|
+
return document.body;
|
|
989
|
+
}
|
|
990
|
+
function createSessionId() {
|
|
991
|
+
const random = globalThis.crypto?.randomUUID?.();
|
|
992
|
+
if (random) return random;
|
|
993
|
+
return `iframe-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
994
|
+
}
|
|
995
|
+
function buildDocument(options) {
|
|
996
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${escapeAttribute(options.csp)}"><meta name="referrer" content="no-referrer"><title>${escapeHtml(options.title)}</title></head><body>${options.html}<script>${bridgeScript(options.sessionId)}</script></body></html>`;
|
|
997
|
+
}
|
|
998
|
+
function bridgeScript(sessionId) {
|
|
999
|
+
return `(function(){
|
|
1000
|
+
var session=${JSON.stringify(sessionId)};
|
|
1001
|
+
function send(message,transfer){
|
|
1002
|
+
message.protocol="riebeckite:iframe";message.version=1;message.sessionId=session;
|
|
1003
|
+
if(transfer&&transfer.length){window.parent.postMessage(message,"*",transfer);}
|
|
1004
|
+
else{window.parent.postMessage(message,"*");}
|
|
1005
|
+
}
|
|
1006
|
+
window.addEventListener("message",function(event){
|
|
1007
|
+
if(event.source!==window.parent)return;
|
|
1008
|
+
var data=event.data;
|
|
1009
|
+
if(!data||data.protocol!=="riebeckite:iframe"||data.version!==1||data.sessionId!==session)return;
|
|
1010
|
+
if(data.kind==="init"){
|
|
1011
|
+
window.dispatchEvent(new CustomEvent("riebeckite:iframe-init",{detail:data.payload}));
|
|
1012
|
+
send({kind:"ready"});
|
|
1013
|
+
}else if(data.kind==="invoke"){
|
|
1014
|
+
window.dispatchEvent(new CustomEvent("riebeckite:iframe-invoke",{detail:{requestId:data.requestId,payload:data.payload,capability:data.capability}}));
|
|
1015
|
+
}else if(data.kind==="cancel"){
|
|
1016
|
+
window.dispatchEvent(new CustomEvent("riebeckite:iframe-cancel",{detail:{requestId:data.requestId}}));
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
window.riebeckiteIframe={
|
|
1020
|
+
result:function(requestId,payload){send({kind:"result",requestId:requestId,payload:payload});},
|
|
1021
|
+
transfer:function(requestId,payload,transfer){send({kind:"result",requestId:requestId,payload:payload,transfer:transfer},transfer);},
|
|
1022
|
+
error:function(requestId,error){send({kind:"error",requestId:requestId,error:{name:"RuntimeIframeError",message:String(error&&error.message?error.message:error)}});}
|
|
1023
|
+
};
|
|
1024
|
+
})();`;
|
|
1025
|
+
}
|
|
1026
|
+
function escapeAttribute(value) {
|
|
1027
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
1028
|
+
}
|
|
1029
|
+
function escapeHtml(value) {
|
|
1030
|
+
return escapeAttribute(value).replaceAll(">", ">");
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/client/main.ts
|
|
1034
|
+
function createMainRuntime(options) {
|
|
1035
|
+
return defineInteractiveRuntime({
|
|
1036
|
+
id: options.id,
|
|
1037
|
+
kind: "main",
|
|
1038
|
+
capabilities: options.capabilities ?? ["dom"],
|
|
1039
|
+
limits: options.limits ?? {},
|
|
1040
|
+
create(context) {
|
|
1041
|
+
const runtimeContext = {
|
|
1042
|
+
instanceId: context.instanceId,
|
|
1043
|
+
signal: context.signal,
|
|
1044
|
+
limits: context.limits,
|
|
1045
|
+
root: context.root
|
|
1046
|
+
};
|
|
1047
|
+
return {
|
|
1048
|
+
start: () => options.start?.(runtimeContext),
|
|
1049
|
+
invoke: (input, invokeContext) => options.invoke(input, {
|
|
1050
|
+
...runtimeContext,
|
|
1051
|
+
requestId: invokeContext.requestId,
|
|
1052
|
+
capability: invokeContext.capability
|
|
1053
|
+
}),
|
|
1054
|
+
stop: () => options.stop?.(runtimeContext)
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/client/worker.ts
|
|
1061
|
+
var MAX_ACTIVE_WORKERS = 8;
|
|
1062
|
+
var activeWorkers = 0;
|
|
1063
|
+
function createWorkerRuntime(options) {
|
|
1064
|
+
return defineInteractiveRuntime({
|
|
1065
|
+
id: options.id,
|
|
1066
|
+
kind: options.kind ?? "worker",
|
|
1067
|
+
capabilities: options.capabilities ?? ["worker", "message"],
|
|
1068
|
+
limits: {
|
|
1069
|
+
...options.limits,
|
|
1070
|
+
invokeTimeoutMs: options.limits?.invokeTimeoutMs ?? 3e4
|
|
1071
|
+
},
|
|
1072
|
+
create(context) {
|
|
1073
|
+
let worker;
|
|
1074
|
+
let readyResolve;
|
|
1075
|
+
let readyReject;
|
|
1076
|
+
let failed;
|
|
1077
|
+
let stopped = false;
|
|
1078
|
+
let counted = false;
|
|
1079
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1080
|
+
const post = (message, transfer) => {
|
|
1081
|
+
worker?.postMessage(message, transfer);
|
|
1082
|
+
};
|
|
1083
|
+
const base = {
|
|
1084
|
+
protocol: RUNTIME_PROTOCOL,
|
|
1085
|
+
version: RUNTIME_PROTOCOL_VERSION,
|
|
1086
|
+
instanceId: context.instanceId
|
|
1087
|
+
};
|
|
1088
|
+
const fail = (error, message) => {
|
|
1089
|
+
if (stopped) return;
|
|
1090
|
+
const specific = error instanceof Error ? error : new RuntimeWorkerError(message);
|
|
1091
|
+
const failure = specific instanceof RuntimeFailureError ? specific : new RuntimeFailureError(message, { cause: specific });
|
|
1092
|
+
failed = failure;
|
|
1093
|
+
readyReject?.(specific);
|
|
1094
|
+
for (const request of pending.values()) {
|
|
1095
|
+
request.unlink();
|
|
1096
|
+
request.reject(failure);
|
|
1097
|
+
}
|
|
1098
|
+
pending.clear();
|
|
1099
|
+
worker?.terminate();
|
|
1100
|
+
worker = void 0;
|
|
1101
|
+
if (counted) {
|
|
1102
|
+
counted = false;
|
|
1103
|
+
activeWorkers = Math.max(0, activeWorkers - 1);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
const onMessage = (event) => {
|
|
1107
|
+
const message = event.data;
|
|
1108
|
+
if (!isRuntimeMessage(message)) return;
|
|
1109
|
+
if (message.instanceId !== context.instanceId) return;
|
|
1110
|
+
switch (message.kind) {
|
|
1111
|
+
case "ready":
|
|
1112
|
+
readyResolve?.();
|
|
1113
|
+
break;
|
|
1114
|
+
case "result": {
|
|
1115
|
+
const request = takePending(pending, message.requestId);
|
|
1116
|
+
if (!request) return;
|
|
1117
|
+
try {
|
|
1118
|
+
const value = options.mapResult ? options.mapResult(message.payload) : message.payload;
|
|
1119
|
+
const transfer = message.transfer;
|
|
1120
|
+
request.resolve(
|
|
1121
|
+
transfer && transfer.length > 0 ? transferOutput(value, transfer) : value
|
|
1122
|
+
);
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
request.reject(error);
|
|
1125
|
+
}
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
case "error": {
|
|
1129
|
+
const request = takePending(pending, message.requestId);
|
|
1130
|
+
const error = runtimeErrorFromWire(
|
|
1131
|
+
message.error,
|
|
1132
|
+
`Worker runtime "${options.id}" request failed.`
|
|
1133
|
+
);
|
|
1134
|
+
if (request) request.reject(error);
|
|
1135
|
+
else if (error instanceof RuntimeFailureError)
|
|
1136
|
+
fail(error, error.message);
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
case "fatal": {
|
|
1140
|
+
const error = runtimeErrorFromWire(
|
|
1141
|
+
message.error,
|
|
1142
|
+
`Worker runtime "${options.id}" failed.`
|
|
1143
|
+
);
|
|
1144
|
+
fail(error, error.message);
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
default:
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
const onError = (event) => {
|
|
1152
|
+
const message = event.error instanceof Error ? event.error.message : event.message || `Worker runtime "${options.id}" crashed.`;
|
|
1153
|
+
fail(event.error, message);
|
|
1154
|
+
};
|
|
1155
|
+
const onMessageError = () => {
|
|
1156
|
+
fail(
|
|
1157
|
+
void 0,
|
|
1158
|
+
`Worker runtime "${options.id}" received an uncloneable message.`
|
|
1159
|
+
);
|
|
1160
|
+
};
|
|
1161
|
+
const start = () => {
|
|
1162
|
+
if (activeWorkers >= MAX_ACTIVE_WORKERS) {
|
|
1163
|
+
throw new RuntimeConcurrencyError(
|
|
1164
|
+
`Worker runtime "${options.id}" cannot start: ${MAX_ACTIVE_WORKERS} worker runtimes are already active.`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const ready = new Promise((resolve, reject) => {
|
|
1168
|
+
readyResolve = resolve;
|
|
1169
|
+
readyReject = reject;
|
|
1170
|
+
});
|
|
1171
|
+
worker = options.createWorker();
|
|
1172
|
+
counted = true;
|
|
1173
|
+
activeWorkers += 1;
|
|
1174
|
+
worker.addEventListener("message", onMessage);
|
|
1175
|
+
worker.addEventListener("error", onError);
|
|
1176
|
+
worker.addEventListener("messageerror", onMessageError);
|
|
1177
|
+
post(
|
|
1178
|
+
{
|
|
1179
|
+
...base,
|
|
1180
|
+
kind: "init",
|
|
1181
|
+
payload: options.init,
|
|
1182
|
+
limits: context.limits
|
|
1183
|
+
},
|
|
1184
|
+
options.initTransfer ? [...options.initTransfer] : void 0
|
|
1185
|
+
);
|
|
1186
|
+
return ready;
|
|
1187
|
+
};
|
|
1188
|
+
const invoke = (input, requestContext) => {
|
|
1189
|
+
if (failed) return Promise.reject(failed);
|
|
1190
|
+
if (stopped) {
|
|
1191
|
+
return Promise.reject(
|
|
1192
|
+
new RuntimeAbortError(
|
|
1193
|
+
`Worker runtime "${options.id}" was disposed.`
|
|
1194
|
+
)
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
if (!worker) {
|
|
1198
|
+
return Promise.reject(
|
|
1199
|
+
new RuntimeWorkerError(
|
|
1200
|
+
`Worker runtime "${options.id}" is not started.`
|
|
1201
|
+
)
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const { signal, requestId } = requestContext;
|
|
1205
|
+
if (signal.aborted) {
|
|
1206
|
+
return Promise.reject(abortReasonToError(signal));
|
|
1207
|
+
}
|
|
1208
|
+
return new Promise(
|
|
1209
|
+
(resolve, reject) => {
|
|
1210
|
+
const onAbort = () => {
|
|
1211
|
+
post({ ...base, kind: "cancel", requestId });
|
|
1212
|
+
takePending(pending, requestId);
|
|
1213
|
+
reject(abortReasonToError(signal));
|
|
1214
|
+
};
|
|
1215
|
+
const unlink = () => signal.removeEventListener("abort", onAbort);
|
|
1216
|
+
pending.set(requestId, { resolve, reject, unlink });
|
|
1217
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1218
|
+
try {
|
|
1219
|
+
post(
|
|
1220
|
+
{
|
|
1221
|
+
...base,
|
|
1222
|
+
kind: "invoke",
|
|
1223
|
+
requestId,
|
|
1224
|
+
capability: requestContext.capability,
|
|
1225
|
+
payload: options.invokePayload ? options.invokePayload(input) : input
|
|
1226
|
+
},
|
|
1227
|
+
requestContext.transfer ? [...requestContext.transfer] : void 0
|
|
1228
|
+
);
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
takePending(pending, requestId);
|
|
1231
|
+
reject(error);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
};
|
|
1236
|
+
const stop = () => {
|
|
1237
|
+
if (stopped) return;
|
|
1238
|
+
stopped = true;
|
|
1239
|
+
const error = new RuntimeAbortError(
|
|
1240
|
+
`Worker runtime "${options.id}" was disposed.`
|
|
1241
|
+
);
|
|
1242
|
+
for (const request of pending.values()) {
|
|
1243
|
+
request.unlink();
|
|
1244
|
+
request.reject(error);
|
|
1245
|
+
}
|
|
1246
|
+
pending.clear();
|
|
1247
|
+
readyReject?.(error);
|
|
1248
|
+
if (worker) {
|
|
1249
|
+
try {
|
|
1250
|
+
post({ ...base, kind: "dispose" });
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
worker.removeEventListener("message", onMessage);
|
|
1254
|
+
worker.removeEventListener("error", onError);
|
|
1255
|
+
worker.removeEventListener("messageerror", onMessageError);
|
|
1256
|
+
worker.terminate();
|
|
1257
|
+
worker = void 0;
|
|
1258
|
+
if (counted) {
|
|
1259
|
+
counted = false;
|
|
1260
|
+
activeWorkers = Math.max(0, activeWorkers - 1);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
return {
|
|
1265
|
+
start,
|
|
1266
|
+
invoke,
|
|
1267
|
+
stop
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
function takePending(pending, requestId) {
|
|
1273
|
+
if (requestId === void 0) return void 0;
|
|
1274
|
+
const request = pending.get(requestId);
|
|
1275
|
+
if (!request) return void 0;
|
|
1276
|
+
pending.delete(requestId);
|
|
1277
|
+
request.unlink();
|
|
1278
|
+
return request;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/client/wasm_worker.ts
|
|
1282
|
+
function createWasmWorkerRuntime(options) {
|
|
1283
|
+
const capabilities = /* @__PURE__ */ new Set([
|
|
1284
|
+
"worker",
|
|
1285
|
+
"wasm",
|
|
1286
|
+
"message",
|
|
1287
|
+
...options.capabilities ?? []
|
|
1288
|
+
]);
|
|
1289
|
+
return createWorkerRuntime({
|
|
1290
|
+
id: options.id,
|
|
1291
|
+
kind: "wasm-worker",
|
|
1292
|
+
capabilities: [...capabilities],
|
|
1293
|
+
limits: options.limits,
|
|
1294
|
+
createWorker: options.createWorker,
|
|
1295
|
+
init: {
|
|
1296
|
+
wasmUrl: options.wasmUrl,
|
|
1297
|
+
exportName: options.exportName
|
|
1298
|
+
},
|
|
1299
|
+
invokePayload: (input) => input,
|
|
1300
|
+
mapResult: (payload) => options.mapOutput ? options.mapOutput(payload) : payload
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
export {
|
|
1304
|
+
IFRAME_PROTOCOL,
|
|
1305
|
+
IFRAME_PROTOCOL_VERSION,
|
|
1306
|
+
InteractiveRuntimeRegistry,
|
|
1307
|
+
MAX_ACTIVE_IFRAMES,
|
|
1308
|
+
MAX_ACTIVE_WORKERS,
|
|
1309
|
+
RUNTIME_CAPABILITIES,
|
|
1310
|
+
RUNTIME_KINDS,
|
|
1311
|
+
RUNTIME_MESSAGE_KINDS,
|
|
1312
|
+
RUNTIME_PROTOCOL,
|
|
1313
|
+
RUNTIME_PROTOCOL_VERSION,
|
|
1314
|
+
RUNTIME_STATES,
|
|
1315
|
+
RuntimeAbortError,
|
|
1316
|
+
RuntimeConcurrencyError,
|
|
1317
|
+
RuntimeError,
|
|
1318
|
+
RuntimeFailureError,
|
|
1319
|
+
RuntimeIframeError,
|
|
1320
|
+
RuntimeInstance,
|
|
1321
|
+
RuntimePayloadError,
|
|
1322
|
+
RuntimeStateError,
|
|
1323
|
+
RuntimeTimeoutError,
|
|
1324
|
+
RuntimeTransfer,
|
|
1325
|
+
RuntimeUnsupportedError,
|
|
1326
|
+
RuntimeWasmError,
|
|
1327
|
+
RuntimeWorkerError,
|
|
1328
|
+
assertPayloadSize,
|
|
1329
|
+
createClientRuntimeHost,
|
|
1330
|
+
createIframeRuntime,
|
|
1331
|
+
createInteractiveRuntime,
|
|
1332
|
+
createMainRuntime,
|
|
1333
|
+
createRuntimeFallback,
|
|
1334
|
+
createWasmWorkerRuntime,
|
|
1335
|
+
createWorkerRuntime,
|
|
1336
|
+
defineInteractiveRuntime,
|
|
1337
|
+
estimatePayloadSize,
|
|
1338
|
+
isRuntimeMessage,
|
|
1339
|
+
isRuntimeTransfer,
|
|
1340
|
+
runtimeErrorFromWire,
|
|
1341
|
+
serializeRuntimeError,
|
|
1342
|
+
transferOutput
|
|
1343
|
+
};
|