@crowdedkingdoms/crowdyjs 8.21.0 → 8.22.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 +5 -0
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/player-runtime/glue-runtime.d.ts +13 -4
- package/dist/player-runtime/glue-runtime.d.ts.map +1 -1
- package/dist/player-runtime/glue-runtime.js +52 -8
- package/dist/player-runtime/glue-sab.d.ts +8 -5
- package/dist/player-runtime/glue-sab.d.ts.map +1 -1
- package/dist/player-runtime/glue-sab.js +36 -10
- package/dist/player-runtime/player-code-broker.d.ts +33 -4
- package/dist/player-runtime/player-code-broker.d.ts.map +1 -1
- package/dist/player-runtime/player-code-broker.js +332 -51
- package/dist/player-runtime/player-glue-worker.d.ts +3 -2
- package/dist/player-runtime/player-glue-worker.d.ts.map +1 -1
- package/dist/player-runtime/player-glue-worker.js +68 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,11 @@ CrowdyJS v4 targets browsers by default and uses native `fetch`, `WebSocket`, `c
|
|
|
25
25
|
> corresponding 2026-07-22 Game API claim schema. Older servers reject only
|
|
26
26
|
> those operations during GraphQL validation.
|
|
27
27
|
|
|
28
|
+
> **v8.21.1 Node realtime routing hotfix:** app-routing responses provide a
|
|
29
|
+
> WebSocket base URL. CrowdyJS now normalizes that base to `/graphql` before
|
|
30
|
+
> constructing the realtime client; previously Node/bot clients connected to
|
|
31
|
+
> the origin root and remained stuck in `connecting` with no notifications.
|
|
32
|
+
>
|
|
28
33
|
> **Player-code authoring DX:** `playerCompute.setRequires`,
|
|
29
34
|
> `marketplace.trustGridAuthor`, self-authored `gridClientMods` fields, and
|
|
30
35
|
> attachment-keyed client artifact fetches require the 2026-07-22 authoring-DX
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crowdy-client.d.ts","sourceRoot":"","sources":["../src/crowdy-client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"crowdy-client.d.ts","sourceRoot":"","sources":["../src/crowdy-client.ts"],"names":[],"mappings":"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"}
|
package/dist/crowdy-client.js
CHANGED
|
@@ -75,7 +75,6 @@ export class CrowdyClient {
|
|
|
75
75
|
}, this.session);
|
|
76
76
|
this.metrics = new RealtimeMetrics();
|
|
77
77
|
this.realtime = new SubscriptionManager({
|
|
78
|
-
wsUrl: config.wsUrl,
|
|
79
78
|
wsEndpoint: config.wsEndpoint ?? toGraphqlEndpoint(config.wsUrl, 'graphql'),
|
|
80
79
|
logger: config.logger,
|
|
81
80
|
lbCookieStore,
|
package/dist/index.d.ts
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* or internal tooling, never an untrusted browser.
|
|
46
46
|
*/
|
|
47
47
|
/** The published package version. Mirrors `package.json`. */
|
|
48
|
-
export declare const VERSION = "8.
|
|
48
|
+
export declare const VERSION = "8.22.0";
|
|
49
49
|
export { LbCookieStore } from './lb-cookie-store.js';
|
|
50
50
|
export { CrowdyClient, createCrowdyClient, type CrowdyClientConfig, } from './crowdy-client.js';
|
|
51
51
|
export { BrowserLocalStorageTokenStore, SessionStore, type SessionListener, type TokenStore, } from './session.js';
|
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* or internal tooling, never an untrusted browser.
|
|
46
46
|
*/
|
|
47
47
|
/** The published package version. Mirrors `package.json`. */
|
|
48
|
-
export const VERSION = '8.
|
|
48
|
+
export const VERSION = '8.22.0';
|
|
49
49
|
export { LbCookieStore } from './lb-cookie-store.js';
|
|
50
50
|
export { CrowdyClient, createCrowdyClient, } from './crowdy-client.js';
|
|
51
51
|
export { BrowserLocalStorageTokenStore, SessionStore, } from './session.js';
|
|
@@ -25,12 +25,18 @@ export interface GlueInitMessage {
|
|
|
25
25
|
type: 'init';
|
|
26
26
|
artifact: ArrayBuffer;
|
|
27
27
|
authority: 'player';
|
|
28
|
+
/** Server-authored budget loaded into an injected mutable `ck_fuel` global. */
|
|
28
29
|
fuelPerDispatch?: string;
|
|
30
|
+
/** Legacy metadata; the hard watchdog is owned by the page-side broker. */
|
|
29
31
|
watchdogMs?: number;
|
|
32
|
+
hostCallTimeoutMs?: number;
|
|
30
33
|
/** Local client tick cadence in ms (0/undefined => no self-tick). */
|
|
31
34
|
tickIntervalMs?: number;
|
|
32
35
|
}
|
|
33
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Parse the server-authored budget used to refill an instrumented artifact's
|
|
38
|
+
* mutable `ck_fuel` global before every guest dispatch.
|
|
39
|
+
*/
|
|
34
40
|
export declare function parseFuelBudget(raw: string | undefined): bigint | null;
|
|
35
41
|
/** A dispatch outcome the worker reports back to the broker. */
|
|
36
42
|
export type GlueDispatchResult = {
|
|
@@ -41,9 +47,9 @@ export type GlueDispatchResult = {
|
|
|
41
47
|
detail?: string;
|
|
42
48
|
};
|
|
43
49
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
50
|
+
* Classify a completed guest dispatch by elapsed wall time. This cannot
|
|
51
|
+
* interrupt synchronous WASM; the page-side PlayerCodeBroker owns the hard
|
|
52
|
+
* dispatch watchdog and terminates a worker whose dispatch never returns.
|
|
47
53
|
*/
|
|
48
54
|
export declare function runWithWatchdog(dispatch: () => unknown, watchdogMs: number, now?: () => number): Promise<GlueDispatchResult>;
|
|
49
55
|
/** The minimal guest-instance surface the runtime drives (a real WebAssembly.Instance satisfies it). */
|
|
@@ -51,6 +57,7 @@ export interface GuestExports {
|
|
|
51
57
|
memory: {
|
|
52
58
|
buffer: ArrayBuffer;
|
|
53
59
|
};
|
|
60
|
+
ck_fuel?: WebAssembly.Global;
|
|
54
61
|
ck_alloc(len: number): number;
|
|
55
62
|
ck_free?(ptr: number, len: number): void;
|
|
56
63
|
init?(): void;
|
|
@@ -66,6 +73,7 @@ export interface GlueRuntimeOptions {
|
|
|
66
73
|
/** Deterministic-enough randomness for the guest `random_get` (defaults to crypto). */
|
|
67
74
|
randomFill?: (buf: Uint8Array) => void;
|
|
68
75
|
now?: () => number;
|
|
76
|
+
fuelPerDispatch?: bigint | null;
|
|
69
77
|
}
|
|
70
78
|
/**
|
|
71
79
|
* Drives one untrusted guest module: builds the `ck` + wasi import table,
|
|
@@ -79,6 +87,7 @@ export declare class GlueRuntime {
|
|
|
79
87
|
private exports;
|
|
80
88
|
private stateBlob;
|
|
81
89
|
constructor(options: GlueRuntimeOptions);
|
|
90
|
+
private resetFuel;
|
|
82
91
|
/** The import object handed to `WebAssembly.instantiate`. Guest sees only these. */
|
|
83
92
|
buildImports(getExports: () => GuestExports | null): WebAssembly.Imports;
|
|
84
93
|
instantiate(artifact: ArrayBuffer): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glue-runtime.d.ts","sourceRoot":"","sources":["../../src/player-runtime/glue-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,iVAoBtB,CAAC;AAEX,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,EAAE,QAAQ,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED
|
|
1
|
+
{"version":3,"file":"glue-runtime.d.ts","sourceRoot":"","sources":["../../src/player-runtime/glue-runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -41,7 +41,10 @@ export const GLUE_HOST_FUNCTIONS = [
|
|
|
41
41
|
'overlay_draw',
|
|
42
42
|
'grid_permission_check',
|
|
43
43
|
];
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Parse the server-authored budget used to refill an instrumented artifact's
|
|
46
|
+
* mutable `ck_fuel` global before every guest dispatch.
|
|
47
|
+
*/
|
|
45
48
|
export function parseFuelBudget(raw) {
|
|
46
49
|
if (raw == null)
|
|
47
50
|
return null;
|
|
@@ -54,9 +57,9 @@ export function parseFuelBudget(raw) {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
+
* Classify a completed guest dispatch by elapsed wall time. This cannot
|
|
61
|
+
* interrupt synchronous WASM; the page-side PlayerCodeBroker owns the hard
|
|
62
|
+
* dispatch watchdog and terminates a worker whose dispatch never returns.
|
|
60
63
|
*/
|
|
61
64
|
export async function runWithWatchdog(dispatch, watchdogMs, now = () => Date.now()) {
|
|
62
65
|
const start = now();
|
|
@@ -76,6 +79,16 @@ export async function runWithWatchdog(dispatch, watchdogMs, now = () => Date.now
|
|
|
76
79
|
return { ok: true };
|
|
77
80
|
}
|
|
78
81
|
const textDecoder = new TextDecoder();
|
|
82
|
+
function assertMemoryRange(buffer, ptr, len, operation) {
|
|
83
|
+
if (!Number.isSafeInteger(ptr) ||
|
|
84
|
+
!Number.isSafeInteger(len) ||
|
|
85
|
+
ptr < 0 ||
|
|
86
|
+
len < 0 ||
|
|
87
|
+
ptr > buffer.byteLength ||
|
|
88
|
+
len > buffer.byteLength - ptr) {
|
|
89
|
+
throw new RangeError(`${operation} is outside guest memory`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
79
92
|
function defaultRandomFill(buf) {
|
|
80
93
|
const c = globalThis.crypto;
|
|
81
94
|
if (c?.getRandomValues) {
|
|
@@ -102,6 +115,15 @@ export class GlueRuntime {
|
|
|
102
115
|
this.exports = null;
|
|
103
116
|
this.stateBlob = new Uint8Array(0);
|
|
104
117
|
}
|
|
118
|
+
resetFuel() {
|
|
119
|
+
const fuel = this.exports?.ck_fuel;
|
|
120
|
+
if (!fuel)
|
|
121
|
+
return;
|
|
122
|
+
if (this.options.fuelPerDispatch == null) {
|
|
123
|
+
throw new Error('instrumented artifact is missing a fuel budget');
|
|
124
|
+
}
|
|
125
|
+
fuel.value = this.options.fuelPerDispatch;
|
|
126
|
+
}
|
|
105
127
|
/** The import object handed to `WebAssembly.instantiate`. Guest sees only these. */
|
|
106
128
|
buildImports(getExports) {
|
|
107
129
|
const mem = () => {
|
|
@@ -114,17 +136,21 @@ export class GlueRuntime {
|
|
|
114
136
|
const ex = getExports();
|
|
115
137
|
if (!ex)
|
|
116
138
|
throw new Error('guest not instantiated');
|
|
139
|
+
const buffer = ex.memory.buffer;
|
|
140
|
+
assertMemoryRange(buffer, ptr, len, 'guest memory read');
|
|
117
141
|
// Copy into a fresh ArrayBuffer-backed view — the guest buffer may
|
|
118
142
|
// detach/grow between calls (and may be a SharedArrayBuffer).
|
|
119
143
|
const out = new Uint8Array(len);
|
|
120
|
-
out.set(new Uint8Array(
|
|
144
|
+
out.set(new Uint8Array(buffer, ptr, len));
|
|
121
145
|
return out;
|
|
122
146
|
};
|
|
123
147
|
const writeAt = (ptr, src) => {
|
|
124
148
|
const ex = getExports();
|
|
125
149
|
if (!ex)
|
|
126
150
|
throw new Error('guest not instantiated');
|
|
127
|
-
|
|
151
|
+
const buffer = ex.memory.buffer;
|
|
152
|
+
assertMemoryRange(buffer, ptr, src.length, 'guest memory write');
|
|
153
|
+
new Uint8Array(buffer, ptr, src.length).set(src);
|
|
128
154
|
};
|
|
129
155
|
const now = this.options.now ?? (() => Date.now());
|
|
130
156
|
const randomFill = this.options.randomFill ?? defaultRandomFill;
|
|
@@ -153,6 +179,9 @@ export class GlueRuntime {
|
|
|
153
179
|
if (!ex)
|
|
154
180
|
throw new Error('guest not instantiated');
|
|
155
181
|
const outPtr = ex.ck_alloc(respBytes.length);
|
|
182
|
+
if (respBytes.length > 0 && outPtr === 0) {
|
|
183
|
+
throw new RangeError('ck_alloc returned a null reply pointer');
|
|
184
|
+
}
|
|
156
185
|
writeAt(outPtr, respBytes);
|
|
157
186
|
// Packed (ptr << 32 | len); the guest reads then ck_frees it.
|
|
158
187
|
return (BigInt(outPtr) << 32n) | BigInt(respBytes.length >>> 0);
|
|
@@ -160,9 +189,13 @@ export class GlueRuntime {
|
|
|
160
189
|
};
|
|
161
190
|
const wasi = {
|
|
162
191
|
random_get: (ptr, len) => {
|
|
163
|
-
const
|
|
192
|
+
const ex = getExports();
|
|
193
|
+
if (!ex)
|
|
194
|
+
throw new Error('guest not instantiated');
|
|
195
|
+
const buffer = ex.memory.buffer;
|
|
196
|
+
assertMemoryRange(buffer, ptr, len, 'random_get write');
|
|
197
|
+
const buf = new Uint8Array(buffer, ptr, len);
|
|
164
198
|
randomFill(buf);
|
|
165
|
-
writeAt(ptr, buf);
|
|
166
199
|
return 0;
|
|
167
200
|
},
|
|
168
201
|
// A player artifact may pull in a few benign wasi stubs; keep them inert.
|
|
@@ -196,10 +229,12 @@ export class GlueRuntime {
|
|
|
196
229
|
}
|
|
197
230
|
/** Run the module's `init` export (once, after instantiate). */
|
|
198
231
|
init() {
|
|
232
|
+
this.resetFuel();
|
|
199
233
|
this.exports?.init?.();
|
|
200
234
|
}
|
|
201
235
|
/** Run one `tick(dt_ms)`. Throws propagate to the caller's watchdog wrapper. */
|
|
202
236
|
tick(dtMs) {
|
|
237
|
+
this.resetFuel();
|
|
203
238
|
this.exports?.tick?.(dtMs);
|
|
204
239
|
}
|
|
205
240
|
/** Invoke the module with an opaque payload; returns the reply bytes (copied out). */
|
|
@@ -208,13 +243,22 @@ export class GlueRuntime {
|
|
|
208
243
|
if (!ex || typeof ex.handle_invoke !== 'function')
|
|
209
244
|
return new Uint8Array(0);
|
|
210
245
|
const ptr = ex.ck_alloc(payload.length);
|
|
246
|
+
if (payload.length > 0 && ptr === 0) {
|
|
247
|
+
throw new RangeError('ck_alloc returned a null invoke pointer');
|
|
248
|
+
}
|
|
249
|
+
assertMemoryRange(ex.memory.buffer, ptr, payload.length, 'invoke request write');
|
|
211
250
|
new Uint8Array(ex.memory.buffer, ptr, payload.length).set(payload);
|
|
251
|
+
this.resetFuel();
|
|
212
252
|
const packed = BigInt(ex.handle_invoke(ptr, payload.length));
|
|
213
253
|
ex.ck_free?.(ptr, payload.length);
|
|
214
254
|
const outPtr = Number(packed >> 32n);
|
|
215
255
|
const outLen = Number(packed & 0xffffffffn);
|
|
216
256
|
if (outLen === 0)
|
|
217
257
|
return new Uint8Array(0);
|
|
258
|
+
if (outPtr === 0) {
|
|
259
|
+
throw new RangeError('guest returned a null invoke reply pointer');
|
|
260
|
+
}
|
|
261
|
+
assertMemoryRange(ex.memory.buffer, outPtr, outLen, 'invoke reply read');
|
|
218
262
|
const out = new Uint8Array(ex.memory.buffer, outPtr, outLen).slice();
|
|
219
263
|
ex.ck_free?.(outPtr, outLen);
|
|
220
264
|
return out;
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* (COOP: same-origin + COEP: require-corp) in the browser; Node worker
|
|
15
15
|
* threads have them unconditionally, which is how this is integration-tested.
|
|
16
16
|
*
|
|
17
|
-
* Layout: [ state:i32, len:i32 ] header, then a byte data
|
|
17
|
+
* Layout: [ state:i32, len:i32, requestId:i32 ] header, then a byte data
|
|
18
|
+
* region. The request id prevents a host response that completed after the
|
|
19
|
+
* worker's timeout from waking a later request that reused the same SAB.
|
|
18
20
|
* state: 0 IDLE, 1 PENDING (worker waiting), 2 DONE.
|
|
19
21
|
* The request travels to the page as a normal postMessage (before the
|
|
20
22
|
* worker blocks); only the reply uses the SAB.
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
export declare const SAB_STATE_IDLE = 0;
|
|
23
25
|
export declare const SAB_STATE_PENDING = 1;
|
|
24
26
|
export declare const SAB_STATE_DONE = 2;
|
|
27
|
+
export declare const GLUE_HOST_CALL_TIMEOUT_MS = 5000;
|
|
25
28
|
export declare const SAB_HEADER_BYTES: number;
|
|
26
29
|
/** 1 MiB reply region — host-call replies (chunk/actor reads) are well under this. */
|
|
27
30
|
export declare const SAB_DATA_BYTES: number;
|
|
@@ -39,9 +42,9 @@ declare const decoder: TextDecoder;
|
|
|
39
42
|
* and wake the blocked worker. `respBytes` must already be the SDK Response
|
|
40
43
|
* envelope (`{ok,data}` / `{ok:false,error}`) the guest expects.
|
|
41
44
|
*/
|
|
42
|
-
export declare function writeGlueReply(view: GlueSab, respBytes: Uint8Array):
|
|
45
|
+
export declare function writeGlueReply(view: GlueSab, respBytes: Uint8Array, requestId?: number): boolean;
|
|
43
46
|
/** Convenience for responders holding a plain object result. */
|
|
44
|
-
export declare function writeGlueResult(view: GlueSab, result: unknown):
|
|
47
|
+
export declare function writeGlueResult(view: GlueSab, result: unknown, requestId?: number): boolean;
|
|
45
48
|
/**
|
|
46
49
|
* Requester side (worker): mark PENDING before posting the wake, block until
|
|
47
50
|
* the page flips the state to DONE, then read the reply bytes out. The
|
|
@@ -49,7 +52,7 @@ export declare function writeGlueResult(view: GlueSab, result: unknown): void;
|
|
|
49
52
|
* `waitAndRead()` — that ordering (post, then wait) is what lets the page
|
|
50
53
|
* see the request while the worker is blocked.
|
|
51
54
|
*/
|
|
52
|
-
export declare function armGlueRequest(view: GlueSab): void;
|
|
53
|
-
export declare function waitAndReadGlueReply(view: GlueSab, timeoutMs?: number): Uint8Array;
|
|
55
|
+
export declare function armGlueRequest(view: GlueSab, requestId: number): void;
|
|
56
|
+
export declare function waitAndReadGlueReply(view: GlueSab, requestId: number, timeoutMs?: number): Uint8Array;
|
|
54
57
|
export { encoder as glueEncoder, decoder as glueDecoder };
|
|
55
58
|
//# sourceMappingURL=glue-sab.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"glue-sab.d.ts","sourceRoot":"","sources":["../../src/player-runtime/glue-sab.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"glue-sab.d.ts","sourceRoot":"","sources":["../../src/player-runtime/glue-sab.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAG9C,eAAO,MAAM,gBAAgB,QAAiB,CAAC;AAC/C,sFAAsF;AACtF,eAAO,MAAM,cAAc,QAAc,CAAC;AAE1C,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,iBAAiB,CAAC;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,wBAAgB,aAAa,CAAC,SAAS,SAAiB,GAAG,OAAO,CAGjE;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAM3D;AAED,QAAA,MAAM,OAAO,aAAoB,CAAC;AAClC,QAAA,MAAM,OAAO,aAAoB,CAAC;AAQlC;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,UAAU,EACrB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAkBT;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,OAAO,EACf,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAET;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAIrE;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,MAAM,EACjB,SAAS,SAA4B,GACpC,UAAU,CAuBZ;AAED,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,OAAO,IAAI,WAAW,EAAE,CAAC"}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* (COOP: same-origin + COEP: require-corp) in the browser; Node worker
|
|
15
15
|
* threads have them unconditionally, which is how this is integration-tested.
|
|
16
16
|
*
|
|
17
|
-
* Layout: [ state:i32, len:i32 ] header, then a byte data
|
|
17
|
+
* Layout: [ state:i32, len:i32, requestId:i32 ] header, then a byte data
|
|
18
|
+
* region. The request id prevents a host response that completed after the
|
|
19
|
+
* worker's timeout from waking a later request that reused the same SAB.
|
|
18
20
|
* state: 0 IDLE, 1 PENDING (worker waiting), 2 DONE.
|
|
19
21
|
* The request travels to the page as a normal postMessage (before the
|
|
20
22
|
* worker blocks); only the reply uses the SAB.
|
|
@@ -22,7 +24,8 @@
|
|
|
22
24
|
export const SAB_STATE_IDLE = 0;
|
|
23
25
|
export const SAB_STATE_PENDING = 1;
|
|
24
26
|
export const SAB_STATE_DONE = 2;
|
|
25
|
-
const
|
|
27
|
+
export const GLUE_HOST_CALL_TIMEOUT_MS = 5000;
|
|
28
|
+
const HEADER_I32 = 3; // state, len, request id
|
|
26
29
|
export const SAB_HEADER_BYTES = HEADER_I32 * 4;
|
|
27
30
|
/** 1 MiB reply region — host-call replies (chunk/actor reads) are well under this. */
|
|
28
31
|
export const SAB_DATA_BYTES = 1024 * 1024;
|
|
@@ -39,21 +42,34 @@ export function wrapGlueSab(sab) {
|
|
|
39
42
|
}
|
|
40
43
|
const encoder = new TextEncoder();
|
|
41
44
|
const decoder = new TextDecoder();
|
|
45
|
+
const overflowReply = encoder.encode(JSON.stringify({
|
|
46
|
+
ok: false,
|
|
47
|
+
error: { kind: 'response_too_large', message: 'host response exceeds reply limit' },
|
|
48
|
+
}));
|
|
42
49
|
/**
|
|
43
50
|
* Responder side (page/broker): write the reply envelope bytes into the SAB
|
|
44
51
|
* and wake the blocked worker. `respBytes` must already be the SDK Response
|
|
45
52
|
* envelope (`{ok,data}` / `{ok:false,error}`) the guest expects.
|
|
46
53
|
*/
|
|
47
|
-
export function writeGlueReply(view, respBytes) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
export function writeGlueReply(view, respBytes, requestId) {
|
|
55
|
+
if (requestId != null &&
|
|
56
|
+
(Atomics.load(view.header, 0) !== SAB_STATE_PENDING ||
|
|
57
|
+
Atomics.load(view.header, 2) !== requestId)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const payload = respBytes.length <= view.data.length ? respBytes : overflowReply;
|
|
61
|
+
if (payload.length > view.data.length) {
|
|
62
|
+
throw new RangeError('glue reply region cannot hold the overflow envelope');
|
|
63
|
+
}
|
|
64
|
+
view.data.set(payload);
|
|
65
|
+
Atomics.store(view.header, 1, payload.length);
|
|
51
66
|
Atomics.store(view.header, 0, SAB_STATE_DONE);
|
|
52
67
|
Atomics.notify(view.header, 0, 1);
|
|
68
|
+
return true;
|
|
53
69
|
}
|
|
54
70
|
/** Convenience for responders holding a plain object result. */
|
|
55
|
-
export function writeGlueResult(view, result) {
|
|
56
|
-
writeGlueReply(view, encoder.encode(JSON.stringify(result)));
|
|
71
|
+
export function writeGlueResult(view, result, requestId) {
|
|
72
|
+
return writeGlueReply(view, encoder.encode(JSON.stringify(result)), requestId);
|
|
57
73
|
}
|
|
58
74
|
/**
|
|
59
75
|
* Requester side (worker): mark PENDING before posting the wake, block until
|
|
@@ -62,16 +78,26 @@ export function writeGlueResult(view, result) {
|
|
|
62
78
|
* `waitAndRead()` — that ordering (post, then wait) is what lets the page
|
|
63
79
|
* see the request while the worker is blocked.
|
|
64
80
|
*/
|
|
65
|
-
export function armGlueRequest(view) {
|
|
81
|
+
export function armGlueRequest(view, requestId) {
|
|
66
82
|
Atomics.store(view.header, 1, 0);
|
|
83
|
+
Atomics.store(view.header, 2, requestId);
|
|
67
84
|
Atomics.store(view.header, 0, SAB_STATE_PENDING);
|
|
68
85
|
}
|
|
69
|
-
export function waitAndReadGlueReply(view, timeoutMs =
|
|
86
|
+
export function waitAndReadGlueReply(view, requestId, timeoutMs = GLUE_HOST_CALL_TIMEOUT_MS) {
|
|
70
87
|
const res = Atomics.wait(view.header, 0, SAB_STATE_PENDING, timeoutMs);
|
|
71
88
|
if (res === 'timed-out') {
|
|
89
|
+
Atomics.compareExchange(view.header, 0, SAB_STATE_PENDING, SAB_STATE_IDLE);
|
|
72
90
|
throw new Error('host call timed out');
|
|
73
91
|
}
|
|
92
|
+
if (Atomics.load(view.header, 2) !== requestId) {
|
|
93
|
+
Atomics.store(view.header, 0, SAB_STATE_IDLE);
|
|
94
|
+
throw new Error('host call reply id mismatch');
|
|
95
|
+
}
|
|
74
96
|
const len = Atomics.load(view.header, 1);
|
|
97
|
+
if (len < 0 || len > view.data.length) {
|
|
98
|
+
Atomics.store(view.header, 0, SAB_STATE_IDLE);
|
|
99
|
+
throw new Error('host call reply length is invalid');
|
|
100
|
+
}
|
|
75
101
|
const out = view.data.slice(0, len);
|
|
76
102
|
Atomics.store(view.header, 0, SAB_STATE_IDLE);
|
|
77
103
|
return out;
|
|
@@ -39,7 +39,10 @@ export interface PlayerCodeBrokerOptions {
|
|
|
39
39
|
* run (09 T7). Compute it from the same bytes the game-api served.
|
|
40
40
|
*/
|
|
41
41
|
artifactHash?: string;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Informational server-authored fuel metadata. The browser does not enforce
|
|
44
|
+
* this value; metering must be injected into the platform artifact.
|
|
45
|
+
*/
|
|
43
46
|
fuelPerDispatch?: bigint;
|
|
44
47
|
onHostCall: (call: PlayerCodeHostCall) => Promise<unknown>;
|
|
45
48
|
/** Optional sink for HUD/overlay presentation the mod emits (BWF wires this). */
|
|
@@ -51,6 +54,10 @@ export interface PlayerCodeBrokerOptions {
|
|
|
51
54
|
hashArtifact?: (artifact: ArrayBuffer) => Promise<string>;
|
|
52
55
|
/** Wall-clock ms allowed per dispatch before the broker recycles the worker. */
|
|
53
56
|
dispatchWatchdogMs?: number;
|
|
57
|
+
/** Wall-clock ms allowed for worker creation + WASM instantiation. */
|
|
58
|
+
startupWatchdogMs?: number;
|
|
59
|
+
/** Wall-clock ms allowed while an async SDK host call is outstanding. */
|
|
60
|
+
hostCallTimeoutMs?: number;
|
|
54
61
|
/**
|
|
55
62
|
* Local tick cadence (ms) for a client mod: the worker self-drives `tick`
|
|
56
63
|
* at this interval. Omit/0 for invoke-only mods (no periodic tick). A HUD
|
|
@@ -81,15 +88,25 @@ export interface PlayerCodeBrokerOptions {
|
|
|
81
88
|
export declare class PlayerCodeBroker {
|
|
82
89
|
private readonly options;
|
|
83
90
|
private worker;
|
|
91
|
+
private workerListener;
|
|
92
|
+
private artifact;
|
|
84
93
|
private circuitOpen;
|
|
85
94
|
private consecutiveTraps;
|
|
95
|
+
private hardTimeouts;
|
|
86
96
|
private readonly rateBuckets;
|
|
87
|
-
private
|
|
97
|
+
private globalCallBucket;
|
|
98
|
+
private generation;
|
|
99
|
+
private lifecycleVersion;
|
|
100
|
+
private starting;
|
|
101
|
+
private workerReady;
|
|
102
|
+
private startupTimer;
|
|
103
|
+
private dispatchTimer;
|
|
104
|
+
private activeDispatch;
|
|
88
105
|
constructor(options: PlayerCodeBrokerOptions);
|
|
89
106
|
/**
|
|
90
107
|
* Start the worker on a platform-fetched artifact. Verifies the artifact
|
|
91
|
-
* hash (side-load refusal, T7) before handing bytes to the worker
|
|
92
|
-
*
|
|
108
|
+
* hash (side-load refusal, T7) before handing bytes to the worker. A retained
|
|
109
|
+
* copy allows the page-side hard watchdog to replace a wedged worker.
|
|
93
110
|
*/
|
|
94
111
|
start(artifact: ArrayBuffer): Promise<void>;
|
|
95
112
|
/** Terminate + respawn on a fresh artifact — the client hot-reload path. */
|
|
@@ -108,6 +125,18 @@ export declare class PlayerCodeBroker {
|
|
|
108
125
|
* the browser; the postMessage is harmless there.
|
|
109
126
|
*/
|
|
110
127
|
private reply;
|
|
128
|
+
private spawnWorker;
|
|
129
|
+
private stopWorker;
|
|
130
|
+
private armStartupTimer;
|
|
131
|
+
private clearStartupTimer;
|
|
132
|
+
private armDispatchTimer;
|
|
133
|
+
private armHostCallTimer;
|
|
134
|
+
private resumeDispatchWatchdog;
|
|
135
|
+
private finishDispatch;
|
|
136
|
+
private clearDispatchTimer;
|
|
137
|
+
private recycleWorker;
|
|
138
|
+
private openCircuit;
|
|
139
|
+
private enforceGlobalRate;
|
|
111
140
|
private enforceRate;
|
|
112
141
|
private recordTrap;
|
|
113
142
|
private assertGridScope;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"player-code-broker.d.ts","sourceRoot":"","sources":["../../src/player-runtime/player-code-broker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"player-code-broker.d.ts","sourceRoot":"","sources":["../../src/player-runtime/player-code-broker.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { wrapGlueSab, writeGlueResult } from './glue-sab.js';
|
|
1
|
+
import { GLUE_HOST_CALL_TIMEOUT_MS, SAB_HEADER_BYTES, wrapGlueSab, writeGlueResult, } from './glue-sab.js';
|
|
2
2
|
/**
|
|
3
3
|
* Deny-by-default host-call allowlist, grouped by capability (04 §4). Only
|
|
4
4
|
* owner-lawful reads and effects cross the bridge; auth, admin, authoring,
|
|
@@ -57,6 +57,22 @@ for (const [group, fns] of Object.entries(ALLOWED_HOST_CALLS)) {
|
|
|
57
57
|
}
|
|
58
58
|
const RATE_WINDOW_MS = 1000;
|
|
59
59
|
const CIRCUIT_TRIP_THRESHOLD = 5;
|
|
60
|
+
const GLOBAL_HOST_CALL_CAP = 1000;
|
|
61
|
+
const MAX_HOST_CALL_FN_BYTES = 128;
|
|
62
|
+
const MAX_HOST_CALL_ARGS_BYTES = 256 * 1024;
|
|
63
|
+
const HOST_CALL_ENVELOPE_KEYS = new Set([
|
|
64
|
+
'type',
|
|
65
|
+
'id',
|
|
66
|
+
'fn',
|
|
67
|
+
'args',
|
|
68
|
+
'reply',
|
|
69
|
+
]);
|
|
70
|
+
const FORBIDDEN_BINDING_KEYS = new Set([
|
|
71
|
+
'authority',
|
|
72
|
+
'bindingKind',
|
|
73
|
+
'binding_kind',
|
|
74
|
+
]);
|
|
75
|
+
const utf8Encoder = new TextEncoder();
|
|
60
76
|
/**
|
|
61
77
|
* Page-side security broker for browser-target player WASM (production shape,
|
|
62
78
|
* player compute P3).
|
|
@@ -81,44 +97,52 @@ export class PlayerCodeBroker {
|
|
|
81
97
|
constructor(options) {
|
|
82
98
|
this.options = options;
|
|
83
99
|
this.worker = null;
|
|
100
|
+
this.workerListener = null;
|
|
101
|
+
this.artifact = null;
|
|
84
102
|
this.circuitOpen = false;
|
|
85
103
|
this.consecutiveTraps = 0;
|
|
104
|
+
this.hardTimeouts = 0;
|
|
86
105
|
this.rateBuckets = new Map();
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
this.globalCallBucket = [];
|
|
107
|
+
this.generation = 0;
|
|
108
|
+
this.lifecycleVersion = 0;
|
|
109
|
+
this.starting = false;
|
|
110
|
+
this.workerReady = false;
|
|
111
|
+
this.startupTimer = null;
|
|
112
|
+
this.dispatchTimer = null;
|
|
113
|
+
this.activeDispatch = null;
|
|
90
114
|
}
|
|
91
115
|
/**
|
|
92
116
|
* Start the worker on a platform-fetched artifact. Verifies the artifact
|
|
93
|
-
* hash (side-load refusal, T7) before handing bytes to the worker
|
|
94
|
-
*
|
|
117
|
+
* hash (side-load refusal, T7) before handing bytes to the worker. A retained
|
|
118
|
+
* copy allows the page-side hard watchdog to replace a wedged worker.
|
|
95
119
|
*/
|
|
96
120
|
async start(artifact) {
|
|
97
|
-
if (this.worker)
|
|
121
|
+
if (this.worker || this.starting) {
|
|
98
122
|
throw new Error('PlayerCodeBroker is already started');
|
|
123
|
+
}
|
|
99
124
|
if (this.circuitOpen) {
|
|
100
125
|
throw new Error('player code circuit is open; reset before starting');
|
|
101
126
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
const lifecycleVersion = ++this.lifecycleVersion;
|
|
128
|
+
this.starting = true;
|
|
129
|
+
try {
|
|
130
|
+
if (this.options.artifactHash) {
|
|
131
|
+
const actual = await this.hash(artifact);
|
|
132
|
+
if (actual !== this.options.artifactHash) {
|
|
133
|
+
throw new Error('refusing to run an artifact that was not fetched from the platform');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (lifecycleVersion !== this.lifecycleVersion) {
|
|
137
|
+
throw new Error('PlayerCodeBroker start was cancelled');
|
|
106
138
|
}
|
|
139
|
+
this.artifact = artifact.slice(0);
|
|
140
|
+
this.spawnWorker();
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
if (lifecycleVersion === this.lifecycleVersion)
|
|
144
|
+
this.starting = false;
|
|
107
145
|
}
|
|
108
|
-
const factory = this.options.workerFactory ??
|
|
109
|
-
((url) => new Worker(url, { type: 'module' }));
|
|
110
|
-
this.worker = factory(this.options.workerUrl);
|
|
111
|
-
this.worker.addEventListener('message', this.onMessage);
|
|
112
|
-
this.worker.postMessage({
|
|
113
|
-
type: 'init',
|
|
114
|
-
artifact,
|
|
115
|
-
authority: 'player',
|
|
116
|
-
fuelPerDispatch: this.options.fuelPerDispatch != null
|
|
117
|
-
? this.options.fuelPerDispatch.toString()
|
|
118
|
-
: undefined,
|
|
119
|
-
watchdogMs: this.options.dispatchWatchdogMs ?? 250,
|
|
120
|
-
tickIntervalMs: this.options.tickIntervalMs ?? 0,
|
|
121
|
-
}, [artifact]);
|
|
122
146
|
}
|
|
123
147
|
/** Terminate + respawn on a fresh artifact — the client hot-reload path. */
|
|
124
148
|
async restart(artifact) {
|
|
@@ -126,47 +150,119 @@ export class PlayerCodeBroker {
|
|
|
126
150
|
await this.start(artifact);
|
|
127
151
|
}
|
|
128
152
|
stop() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
this.
|
|
133
|
-
this.worker = null;
|
|
153
|
+
this.lifecycleVersion += 1;
|
|
154
|
+
this.starting = false;
|
|
155
|
+
this.stopWorker();
|
|
156
|
+
this.artifact = null;
|
|
134
157
|
}
|
|
135
158
|
/** Clear a tripped circuit so the caller can start again after a fix. */
|
|
136
159
|
resetCircuit() {
|
|
137
160
|
this.circuitOpen = false;
|
|
138
161
|
this.consecutiveTraps = 0;
|
|
162
|
+
this.hardTimeouts = 0;
|
|
139
163
|
this.rateBuckets.clear();
|
|
164
|
+
this.globalCallBucket = [];
|
|
140
165
|
}
|
|
141
|
-
async handleMessage(raw) {
|
|
142
|
-
if (
|
|
166
|
+
async handleMessage(raw, generation, sourceWorker) {
|
|
167
|
+
if (this.worker !== sourceWorker ||
|
|
168
|
+
generation !== this.generation ||
|
|
169
|
+
!isRecord(raw)) {
|
|
143
170
|
return;
|
|
171
|
+
}
|
|
172
|
+
if (raw.type === 'ready') {
|
|
173
|
+
this.workerReady = true;
|
|
174
|
+
this.clearStartupTimer();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (raw.type === 'dispatch-start') {
|
|
178
|
+
if (!Number.isSafeInteger(raw.id) || raw.id <= 0) {
|
|
179
|
+
this.recycleWorker('invalid dispatch-start id');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (this.activeDispatch) {
|
|
183
|
+
this.recycleWorker('overlapping guest dispatches');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.activeDispatch = {
|
|
187
|
+
generation,
|
|
188
|
+
id: raw.id,
|
|
189
|
+
kind: typeof raw.kind === 'string' ? raw.kind : 'unknown',
|
|
190
|
+
};
|
|
191
|
+
this.armDispatchTimer(generation, raw.id);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
144
194
|
if (raw.type === 'trap') {
|
|
145
|
-
this.
|
|
195
|
+
this.finishDispatch(generation, raw.id);
|
|
196
|
+
const reason = typeof raw.reason === 'string' ? raw.reason : 'trap';
|
|
197
|
+
const detail = typeof raw.detail === 'string' ? raw.detail : '';
|
|
198
|
+
const trapKind = typeof raw.kind === 'string' ? raw.kind : '';
|
|
199
|
+
if (reason === 'watchdog' || /host call timed out/i.test(detail)) {
|
|
200
|
+
this.recycleWorker(/host call timed out/i.test(detail) ? 'hostcall-timeout' : reason);
|
|
201
|
+
}
|
|
202
|
+
else if (trapKind === 'startup' || trapKind === 'init') {
|
|
203
|
+
this.stopWorker();
|
|
204
|
+
this.recordTrap(reason);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.recordTrap(reason);
|
|
208
|
+
}
|
|
146
209
|
return;
|
|
147
210
|
}
|
|
148
211
|
if (raw.type === 'dispatch-ok') {
|
|
149
|
-
|
|
212
|
+
const kind = this.activeDispatch?.kind;
|
|
213
|
+
this.finishDispatch(generation, raw.id);
|
|
214
|
+
if (kind && kind !== 'init') {
|
|
215
|
+
this.consecutiveTraps = 0;
|
|
216
|
+
this.hardTimeouts = 0;
|
|
217
|
+
}
|
|
150
218
|
return;
|
|
151
219
|
}
|
|
152
220
|
if (raw.type !== 'hostcall')
|
|
153
221
|
return;
|
|
222
|
+
this.armHostCallTimer(generation);
|
|
223
|
+
if (!this.enforceGlobalRate())
|
|
224
|
+
return;
|
|
154
225
|
const id = raw.id;
|
|
155
226
|
try {
|
|
156
|
-
if (
|
|
227
|
+
if (!Number.isSafeInteger(id) || id < 0) {
|
|
157
228
|
throw new Error('invalid hostcall id');
|
|
229
|
+
}
|
|
230
|
+
for (const key of Object.keys(raw)) {
|
|
231
|
+
if (!HOST_CALL_ENVELOPE_KEYS.has(key)) {
|
|
232
|
+
throw new Error(`unexpected hostcall envelope field '${key}'`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
158
235
|
if (typeof raw.fn !== 'string')
|
|
159
236
|
throw new Error('missing host call fn');
|
|
237
|
+
if (raw.fn.length === 0 ||
|
|
238
|
+
utf8Encoder.encode(raw.fn).length > MAX_HOST_CALL_FN_BYTES) {
|
|
239
|
+
throw new Error('invalid host call fn');
|
|
240
|
+
}
|
|
160
241
|
const group = FN_TO_GROUP.get(raw.fn);
|
|
161
242
|
if (!group) {
|
|
162
243
|
throw new Error('host call is not allowed in the player browser sandbox');
|
|
163
244
|
}
|
|
164
|
-
if (!
|
|
245
|
+
if (!isPlainRecord(raw.args)) {
|
|
165
246
|
// Confused-deputy guard: reject malformed payloads outright rather
|
|
166
247
|
// than coercing them (09 T7).
|
|
167
|
-
throw new Error('host call args must be
|
|
248
|
+
throw new Error('host call args must be a plain object');
|
|
168
249
|
}
|
|
169
250
|
const args = raw.args;
|
|
251
|
+
for (const key of FORBIDDEN_BINDING_KEYS) {
|
|
252
|
+
if (Object.prototype.hasOwnProperty.call(args, key)) {
|
|
253
|
+
throw new Error(`host call cannot override '${key}'`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
let encodedArgs;
|
|
257
|
+
try {
|
|
258
|
+
encodedArgs = utf8Encoder.encode(JSON.stringify(args));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
throw new Error('host call args must be JSON-serializable');
|
|
262
|
+
}
|
|
263
|
+
if (encodedArgs.length > MAX_HOST_CALL_ARGS_BYTES) {
|
|
264
|
+
throw new Error('host call args exceed the browser sandbox limit');
|
|
265
|
+
}
|
|
170
266
|
this.enforceRate(group, raw.fn);
|
|
171
267
|
this.assertGridScope(raw.fn, args);
|
|
172
268
|
let data;
|
|
@@ -179,7 +275,12 @@ export class PlayerCodeBroker {
|
|
|
179
275
|
low: { x: low.x.toString(), y: low.y.toString(), z: low.z.toString() },
|
|
180
276
|
high: { x: high.x.toString(), y: high.y.toString(), z: high.z.toString() },
|
|
181
277
|
};
|
|
182
|
-
this.reply(raw.reply, id, {
|
|
278
|
+
this.reply(sourceWorker, generation, raw.reply, id, {
|
|
279
|
+
type: 'hostcall-result',
|
|
280
|
+
id,
|
|
281
|
+
ok: true,
|
|
282
|
+
data,
|
|
283
|
+
});
|
|
183
284
|
return;
|
|
184
285
|
}
|
|
185
286
|
if (PRESENTATION_FUNCTIONS.has(raw.fn)) {
|
|
@@ -194,10 +295,15 @@ export class PlayerCodeBroker {
|
|
|
194
295
|
else {
|
|
195
296
|
data = await this.options.onHostCall({ fn: raw.fn, args });
|
|
196
297
|
}
|
|
197
|
-
this.reply(raw.reply, id, {
|
|
298
|
+
this.reply(sourceWorker, generation, raw.reply, id, {
|
|
299
|
+
type: 'hostcall-result',
|
|
300
|
+
id,
|
|
301
|
+
ok: true,
|
|
302
|
+
data,
|
|
303
|
+
});
|
|
198
304
|
}
|
|
199
305
|
catch (error) {
|
|
200
|
-
this.reply(raw.reply, id, {
|
|
306
|
+
this.reply(sourceWorker, generation, raw.reply, id, {
|
|
201
307
|
type: 'hostcall-result',
|
|
202
308
|
id,
|
|
203
309
|
ok: false,
|
|
@@ -214,15 +320,173 @@ export class PlayerCodeBroker {
|
|
|
214
320
|
* runs its message handler), so the SAB write is the load-bearing path in
|
|
215
321
|
* the browser; the postMessage is harmless there.
|
|
216
322
|
*/
|
|
217
|
-
reply(replyBuffer,
|
|
218
|
-
this.worker
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
323
|
+
reply(sourceWorker, generation, replyBuffer, id, message) {
|
|
324
|
+
if (this.worker !== sourceWorker || generation !== this.generation)
|
|
325
|
+
return;
|
|
326
|
+
let sabAttempted = false;
|
|
327
|
+
let delivered = false;
|
|
328
|
+
if (typeof SharedArrayBuffer !== 'undefined' &&
|
|
329
|
+
replyBuffer instanceof SharedArrayBuffer &&
|
|
330
|
+
replyBuffer.byteLength >= SAB_HEADER_BYTES) {
|
|
331
|
+
sabAttempted = true;
|
|
332
|
+
try {
|
|
333
|
+
const view = wrapGlueSab(replyBuffer);
|
|
334
|
+
const envelope = message.ok
|
|
335
|
+
? { ok: true, data: message.data }
|
|
336
|
+
: { ok: false, error: message.error };
|
|
337
|
+
delivered = writeGlueResult(view, envelope, id);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// The async postMessage result above is still delivered. A forged or
|
|
341
|
+
// undersized SAB must not turn a denied worker request into an
|
|
342
|
+
// unhandled page-side rejection.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (!sabAttempted || delivered) {
|
|
346
|
+
try {
|
|
347
|
+
sourceWorker.postMessage(message);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// The SAB is authoritative in production. An async mirror that cannot
|
|
351
|
+
// be cloned must not strand a successfully delivered synchronous reply.
|
|
352
|
+
}
|
|
225
353
|
}
|
|
354
|
+
if (delivered)
|
|
355
|
+
this.resumeDispatchWatchdog(generation);
|
|
356
|
+
}
|
|
357
|
+
spawnWorker() {
|
|
358
|
+
if (!this.artifact || this.circuitOpen)
|
|
359
|
+
return;
|
|
360
|
+
const factory = this.options.workerFactory ??
|
|
361
|
+
((url) => new Worker(url, { type: 'module' }));
|
|
362
|
+
const worker = factory(this.options.workerUrl);
|
|
363
|
+
const generation = ++this.generation;
|
|
364
|
+
const listener = (event) => {
|
|
365
|
+
void this.handleMessage(event.data, generation, worker);
|
|
366
|
+
};
|
|
367
|
+
this.worker = worker;
|
|
368
|
+
this.workerListener = listener;
|
|
369
|
+
this.workerReady = false;
|
|
370
|
+
worker.addEventListener('message', listener);
|
|
371
|
+
const workerArtifact = this.artifact.slice(0);
|
|
372
|
+
try {
|
|
373
|
+
worker.postMessage({
|
|
374
|
+
type: 'init',
|
|
375
|
+
artifact: workerArtifact,
|
|
376
|
+
authority: 'player',
|
|
377
|
+
fuelPerDispatch: this.options.fuelPerDispatch?.toString(),
|
|
378
|
+
hostCallTimeoutMs: this.options.hostCallTimeoutMs ?? GLUE_HOST_CALL_TIMEOUT_MS,
|
|
379
|
+
tickIntervalMs: this.options.tickIntervalMs ?? 0,
|
|
380
|
+
}, [workerArtifact]);
|
|
381
|
+
this.armStartupTimer(generation);
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
this.stopWorker();
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
stopWorker() {
|
|
389
|
+
this.clearStartupTimer();
|
|
390
|
+
this.clearDispatchTimer();
|
|
391
|
+
this.activeDispatch = null;
|
|
392
|
+
this.workerReady = false;
|
|
393
|
+
const worker = this.worker;
|
|
394
|
+
const listener = this.workerListener;
|
|
395
|
+
this.worker = null;
|
|
396
|
+
this.workerListener = null;
|
|
397
|
+
if (!worker)
|
|
398
|
+
return;
|
|
399
|
+
if (listener)
|
|
400
|
+
worker.removeEventListener('message', listener);
|
|
401
|
+
worker.terminate();
|
|
402
|
+
}
|
|
403
|
+
armStartupTimer(generation) {
|
|
404
|
+
this.clearStartupTimer();
|
|
405
|
+
const timeoutMs = this.options.startupWatchdogMs ??
|
|
406
|
+
Math.max(5000, (this.options.dispatchWatchdogMs ?? 250) * 4);
|
|
407
|
+
this.startupTimer = setTimeout(() => {
|
|
408
|
+
if (generation === this.generation &&
|
|
409
|
+
this.worker &&
|
|
410
|
+
!this.workerReady) {
|
|
411
|
+
this.recycleWorker('startup-watchdog');
|
|
412
|
+
}
|
|
413
|
+
}, timeoutMs);
|
|
414
|
+
}
|
|
415
|
+
clearStartupTimer() {
|
|
416
|
+
if (this.startupTimer)
|
|
417
|
+
clearTimeout(this.startupTimer);
|
|
418
|
+
this.startupTimer = null;
|
|
419
|
+
}
|
|
420
|
+
armDispatchTimer(generation, id) {
|
|
421
|
+
this.clearDispatchTimer();
|
|
422
|
+
this.dispatchTimer = setTimeout(() => {
|
|
423
|
+
if (this.activeDispatch?.generation === generation &&
|
|
424
|
+
this.activeDispatch.id === id) {
|
|
425
|
+
this.recycleWorker('dispatch-watchdog');
|
|
426
|
+
}
|
|
427
|
+
}, this.options.dispatchWatchdogMs ?? 250);
|
|
428
|
+
}
|
|
429
|
+
armHostCallTimer(generation) {
|
|
430
|
+
if (this.activeDispatch?.generation !== generation ||
|
|
431
|
+
!this.worker) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const id = this.activeDispatch.id;
|
|
435
|
+
this.clearDispatchTimer();
|
|
436
|
+
this.dispatchTimer = setTimeout(() => {
|
|
437
|
+
if (this.activeDispatch?.generation === generation &&
|
|
438
|
+
this.activeDispatch.id === id) {
|
|
439
|
+
this.recycleWorker('hostcall-timeout');
|
|
440
|
+
}
|
|
441
|
+
}, this.options.hostCallTimeoutMs ?? GLUE_HOST_CALL_TIMEOUT_MS);
|
|
442
|
+
}
|
|
443
|
+
resumeDispatchWatchdog(generation) {
|
|
444
|
+
if (this.activeDispatch?.generation !== generation)
|
|
445
|
+
return;
|
|
446
|
+
this.armDispatchTimer(generation, this.activeDispatch.id);
|
|
447
|
+
}
|
|
448
|
+
finishDispatch(generation, id) {
|
|
449
|
+
if (!Number.isSafeInteger(id) ||
|
|
450
|
+
this.activeDispatch?.generation !== generation ||
|
|
451
|
+
this.activeDispatch.id !== id) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
this.clearDispatchTimer();
|
|
455
|
+
this.activeDispatch = null;
|
|
456
|
+
}
|
|
457
|
+
clearDispatchTimer() {
|
|
458
|
+
if (this.dispatchTimer)
|
|
459
|
+
clearTimeout(this.dispatchTimer);
|
|
460
|
+
this.dispatchTimer = null;
|
|
461
|
+
}
|
|
462
|
+
recycleWorker(reason) {
|
|
463
|
+
if (!this.worker || this.circuitOpen)
|
|
464
|
+
return;
|
|
465
|
+
this.hardTimeouts += 1;
|
|
466
|
+
this.consecutiveTraps += 1;
|
|
467
|
+
this.stopWorker();
|
|
468
|
+
if (this.hardTimeouts >= CIRCUIT_TRIP_THRESHOLD) {
|
|
469
|
+
this.openCircuit(reason);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.spawnWorker();
|
|
473
|
+
}
|
|
474
|
+
openCircuit(reason) {
|
|
475
|
+
if (this.circuitOpen)
|
|
476
|
+
return;
|
|
477
|
+
this.circuitOpen = true;
|
|
478
|
+
this.stopWorker();
|
|
479
|
+
this.options.onCircuitOpen?.(reason);
|
|
480
|
+
}
|
|
481
|
+
enforceGlobalRate() {
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
this.globalCallBucket = this.globalCallBucket.filter((timestamp) => now - timestamp < RATE_WINDOW_MS);
|
|
484
|
+
if (this.globalCallBucket.length >= GLOBAL_HOST_CALL_CAP) {
|
|
485
|
+
this.openCircuit('global host-call rate exceeded');
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
this.globalCallBucket.push(now);
|
|
489
|
+
return true;
|
|
226
490
|
}
|
|
227
491
|
enforceRate(group, fn) {
|
|
228
492
|
const cap = RATE_CAPS[group] ?? 60;
|
|
@@ -237,9 +501,7 @@ export class PlayerCodeBroker {
|
|
|
237
501
|
recordTrap(reason) {
|
|
238
502
|
this.consecutiveTraps += 1;
|
|
239
503
|
if (this.consecutiveTraps >= CIRCUIT_TRIP_THRESHOLD && !this.circuitOpen) {
|
|
240
|
-
this.
|
|
241
|
-
this.stop();
|
|
242
|
-
this.options.onCircuitOpen?.(reason);
|
|
504
|
+
this.openCircuit(reason);
|
|
243
505
|
}
|
|
244
506
|
}
|
|
245
507
|
assertGridScope(fn, args) {
|
|
@@ -276,7 +538,26 @@ export class PlayerCodeBroker {
|
|
|
276
538
|
function isRecord(value) {
|
|
277
539
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
278
540
|
}
|
|
541
|
+
function isPlainRecord(value) {
|
|
542
|
+
if (!isRecord(value))
|
|
543
|
+
return false;
|
|
544
|
+
const prototype = Object.getPrototypeOf(value);
|
|
545
|
+
return prototype === Object.prototype || prototype === null;
|
|
546
|
+
}
|
|
279
547
|
function toBigInt(value) {
|
|
548
|
+
if (typeof value === 'string' &&
|
|
549
|
+
!/^-?(0|[1-9][0-9]*)$/.test(value)) {
|
|
550
|
+
throw new Error('chunk coordinates must be decimal integer strings');
|
|
551
|
+
}
|
|
552
|
+
if (typeof value === 'number' &&
|
|
553
|
+
(!Number.isSafeInteger(value) || !Number.isFinite(value))) {
|
|
554
|
+
throw new Error('chunk coordinates must be safe integers or decimal strings');
|
|
555
|
+
}
|
|
556
|
+
if (typeof value !== 'string' &&
|
|
557
|
+
typeof value !== 'number' &&
|
|
558
|
+
typeof value !== 'bigint') {
|
|
559
|
+
throw new Error('chunk coordinates must be integers');
|
|
560
|
+
}
|
|
280
561
|
try {
|
|
281
562
|
return BigInt(value);
|
|
282
563
|
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* - `ck.host_call` blocks the worker on a SharedArrayBuffer while the
|
|
14
14
|
* page-side broker services the (async) server-authorized call and writes
|
|
15
15
|
* the reply back, so the guest sees a synchronous gateway,
|
|
16
|
-
* - it
|
|
17
|
-
*
|
|
16
|
+
* - it announces each dispatch before entering WASM; the page-side broker
|
|
17
|
+
* owns the hard watchdog and can terminate this worker if WASM never
|
|
18
|
+
* returns. The local elapsed-time helper only classifies completed calls.
|
|
18
19
|
*
|
|
19
20
|
* Re-exports the pure helpers + the runtime so tests and bundlers can use
|
|
20
21
|
* them without touching worker globals.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"player-glue-worker.d.ts","sourceRoot":"","sources":["../../src/player-runtime/player-glue-worker.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"player-glue-worker.d.ts","sourceRoot":"","sources":["../../src/player-runtime/player-glue-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAS3B,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,KAAK,eAAe,EACpB,KAAK,kBAAkB,GACxB,CAAC;AAOF;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,gBAAgB,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,KAAK,IAAI,CAAC;IACrE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KAAK,IAAI,CAAC;IACrD,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACzC,GAAG,IAAI,CAkJP"}
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
* - `ck.host_call` blocks the worker on a SharedArrayBuffer while the
|
|
14
14
|
* page-side broker services the (async) server-authorized call and writes
|
|
15
15
|
* the reply back, so the guest sees a synchronous gateway,
|
|
16
|
-
* - it
|
|
17
|
-
*
|
|
16
|
+
* - it announces each dispatch before entering WASM; the page-side broker
|
|
17
|
+
* owns the hard watchdog and can terminate this worker if WASM never
|
|
18
|
+
* returns. The local elapsed-time helper only classifies completed calls.
|
|
18
19
|
*
|
|
19
20
|
* Re-exports the pure helpers + the runtime so tests and bundlers can use
|
|
20
21
|
* them without touching worker globals.
|
|
21
22
|
*/
|
|
22
23
|
import { GlueRuntime, GLUE_HOST_FUNCTIONS, parseFuelBudget, runWithWatchdog, } from './glue-runtime.js';
|
|
23
|
-
import { createGlueSab, armGlueRequest, waitAndReadGlueReply, } from './glue-sab.js';
|
|
24
|
+
import { createGlueSab, armGlueRequest, GLUE_HOST_CALL_TIMEOUT_MS, waitAndReadGlueReply, } from './glue-sab.js';
|
|
24
25
|
export { GlueRuntime, GLUE_HOST_FUNCTIONS, parseFuelBudget, runWithWatchdog, };
|
|
25
26
|
/**
|
|
26
27
|
* Wire the glue runtime to a worker-like message port. Exported (not just
|
|
@@ -31,7 +32,8 @@ export function startGlueWorker(port) {
|
|
|
31
32
|
let runtime = null;
|
|
32
33
|
let sab = null;
|
|
33
34
|
let hostCallId = 0;
|
|
34
|
-
let
|
|
35
|
+
let dispatchId = 0;
|
|
36
|
+
let hostCallTimeoutMs = GLUE_HOST_CALL_TIMEOUT_MS;
|
|
35
37
|
let ticking = false;
|
|
36
38
|
const post = (message) => port.postMessage(message);
|
|
37
39
|
// Synchronous gateway: post the request (so the page can see it), then
|
|
@@ -46,8 +48,9 @@ export function startGlueWorker(port) {
|
|
|
46
48
|
catch {
|
|
47
49
|
throw new Error('host_call request was not valid JSON');
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
hostCallId = (hostCallId % 0x7ffffffe) + 1;
|
|
52
|
+
const id = hostCallId;
|
|
53
|
+
armGlueRequest(sab, id);
|
|
51
54
|
post({
|
|
52
55
|
type: 'hostcall',
|
|
53
56
|
id,
|
|
@@ -55,34 +58,59 @@ export function startGlueWorker(port) {
|
|
|
55
58
|
args: parsed.args ?? {},
|
|
56
59
|
reply: sab.sab,
|
|
57
60
|
});
|
|
58
|
-
return waitAndReadGlueReply(sab);
|
|
61
|
+
return waitAndReadGlueReply(sab, id, hostCallTimeoutMs);
|
|
59
62
|
};
|
|
63
|
+
const beginDispatch = (kind) => {
|
|
64
|
+
dispatchId = (dispatchId % 0x7ffffffe) + 1;
|
|
65
|
+
post({ type: 'dispatch-start', id: dispatchId, kind });
|
|
66
|
+
return dispatchId;
|
|
67
|
+
};
|
|
68
|
+
const report = (id, kind, result) => {
|
|
69
|
+
if (result.ok)
|
|
70
|
+
post({ type: 'dispatch-ok', id, kind });
|
|
71
|
+
else {
|
|
72
|
+
post({
|
|
73
|
+
type: 'trap',
|
|
74
|
+
id,
|
|
75
|
+
kind,
|
|
76
|
+
reason: result.reason,
|
|
77
|
+
detail: result.detail,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
// Trap classification remains local, but wall-clock enforcement is entirely
|
|
82
|
+
// page-side so time blocked on an authorized async host call is excluded.
|
|
83
|
+
const runDispatch = (dispatch) => runWithWatchdog(dispatch, Number.POSITIVE_INFINITY);
|
|
60
84
|
const onInit = async (init) => {
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
hostCallTimeoutMs =
|
|
86
|
+
init.hostCallTimeoutMs ?? GLUE_HOST_CALL_TIMEOUT_MS;
|
|
63
87
|
sab = createGlueSab();
|
|
88
|
+
const fuelPerDispatch = parseFuelBudget(init.fuelPerDispatch);
|
|
64
89
|
runtime = new GlueRuntime({
|
|
65
90
|
hostCallSync,
|
|
66
91
|
onLog: (level, message) => post({ type: 'log', level, message }),
|
|
92
|
+
fuelPerDispatch,
|
|
67
93
|
});
|
|
68
94
|
try {
|
|
69
95
|
await runtime.instantiate(init.artifact);
|
|
70
|
-
const
|
|
71
|
-
|
|
96
|
+
const id = beginDispatch('init');
|
|
97
|
+
const initResult = await runDispatch(() => runtime.init());
|
|
98
|
+
report(id, 'init', initResult);
|
|
99
|
+
if (!initResult.ok)
|
|
100
|
+
return;
|
|
72
101
|
if ((init.tickIntervalMs ?? 0) > 0)
|
|
73
102
|
startTicking(init.tickIntervalMs);
|
|
74
103
|
post({ type: 'ready' });
|
|
75
104
|
}
|
|
76
105
|
catch (err) {
|
|
77
|
-
post({
|
|
106
|
+
post({
|
|
107
|
+
type: 'trap',
|
|
108
|
+
kind: 'startup',
|
|
109
|
+
reason: 'trap',
|
|
110
|
+
detail: err.message,
|
|
111
|
+
});
|
|
78
112
|
}
|
|
79
113
|
};
|
|
80
|
-
const report = (result) => {
|
|
81
|
-
if (result.ok)
|
|
82
|
-
post({ type: 'dispatch-ok' });
|
|
83
|
-
else
|
|
84
|
-
post({ type: 'trap', reason: result.reason, detail: result.detail });
|
|
85
|
-
};
|
|
86
114
|
const startTicking = (intervalMs) => {
|
|
87
115
|
if (ticking)
|
|
88
116
|
return;
|
|
@@ -94,7 +122,8 @@ export function startGlueWorker(port) {
|
|
|
94
122
|
const nowT = Date.now();
|
|
95
123
|
const dt = nowT - last;
|
|
96
124
|
last = nowT;
|
|
97
|
-
|
|
125
|
+
const id = beginDispatch('tick');
|
|
126
|
+
void runDispatch(() => runtime.tick(dt)).then((result) => report(id, 'tick', result));
|
|
98
127
|
setTimeout(loop, intervalMs);
|
|
99
128
|
};
|
|
100
129
|
setTimeout(loop, intervalMs);
|
|
@@ -106,22 +135,29 @@ export function startGlueWorker(port) {
|
|
|
106
135
|
if (msg.type === 'init')
|
|
107
136
|
void onInit(msg);
|
|
108
137
|
else if (msg.type === 'tick' && runtime) {
|
|
109
|
-
|
|
138
|
+
const id = beginDispatch('tick');
|
|
139
|
+
void runDispatch(() => runtime.tick(typeof msg.dtMs === 'number' ? msg.dtMs : 0)).then((result) => report(id, 'tick', result));
|
|
110
140
|
}
|
|
111
141
|
else if (msg.type === 'invoke' && runtime) {
|
|
112
142
|
const payload = msg.payload instanceof Uint8Array ? msg.payload : new Uint8Array(0);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
id: msg.id,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
const id = beginDispatch('invoke');
|
|
144
|
+
let out = new Uint8Array(0);
|
|
145
|
+
void runDispatch(() => {
|
|
146
|
+
out = runtime.invoke(payload);
|
|
147
|
+
}).then((result) => {
|
|
148
|
+
report(id, 'invoke', result);
|
|
149
|
+
if (result.ok) {
|
|
150
|
+
post({ type: 'invoke-result', id: msg.id, ok: true, payload: out });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
post({
|
|
154
|
+
type: 'invoke-result',
|
|
155
|
+
id: msg.id,
|
|
156
|
+
ok: false,
|
|
157
|
+
error: result.detail ?? result.reason,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
125
161
|
}
|
|
126
162
|
else if (msg.type === 'stop') {
|
|
127
163
|
ticking = false;
|