@rezi-ui/node 0.1.0-alpha.1

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.
Files changed (42) hide show
  1. package/README.md +17 -0
  2. package/dist/__e2e__/fixtures/terminal-app.d.ts +2 -0
  3. package/dist/__e2e__/fixtures/terminal-app.d.ts.map +1 -0
  4. package/dist/__e2e__/fixtures/terminal-app.js +42 -0
  5. package/dist/__e2e__/fixtures/terminal-app.js.map +1 -0
  6. package/dist/__e2e__/terminal-render.e2e.test.d.ts +2 -0
  7. package/dist/__e2e__/terminal-render.e2e.test.d.ts.map +1 -0
  8. package/dist/__e2e__/terminal-render.e2e.test.js +125 -0
  9. package/dist/__e2e__/terminal-render.e2e.test.js.map +1 -0
  10. package/dist/__tests__/worker_integration.test.d.ts +2 -0
  11. package/dist/__tests__/worker_integration.test.d.ts.map +1 -0
  12. package/dist/__tests__/worker_integration.test.js +569 -0
  13. package/dist/__tests__/worker_integration.test.js.map +1 -0
  14. package/dist/backend/nodeBackend.d.ts +62 -0
  15. package/dist/backend/nodeBackend.d.ts.map +1 -0
  16. package/dist/backend/nodeBackend.js +942 -0
  17. package/dist/backend/nodeBackend.js.map +1 -0
  18. package/dist/backend/nodeBackendInline.d.ts +10 -0
  19. package/dist/backend/nodeBackendInline.d.ts.map +1 -0
  20. package/dist/backend/nodeBackendInline.js +702 -0
  21. package/dist/backend/nodeBackendInline.js.map +1 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +5 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/worker/engineWorker.d.ts +9 -0
  27. package/dist/worker/engineWorker.d.ts.map +1 -0
  28. package/dist/worker/engineWorker.js +1001 -0
  29. package/dist/worker/engineWorker.js.map +1 -0
  30. package/dist/worker/protocol.d.ts +242 -0
  31. package/dist/worker/protocol.d.ts.map +1 -0
  32. package/dist/worker/protocol.js +23 -0
  33. package/dist/worker/protocol.js.map +1 -0
  34. package/dist/worker/testShims/mockNative.d.ts +48 -0
  35. package/dist/worker/testShims/mockNative.d.ts.map +1 -0
  36. package/dist/worker/testShims/mockNative.js +216 -0
  37. package/dist/worker/testShims/mockNative.js.map +1 -0
  38. package/dist/worker/testShims/targetFpsNative.d.ts +22 -0
  39. package/dist/worker/testShims/targetFpsNative.d.ts.map +1 -0
  40. package/dist/worker/testShims/targetFpsNative.js +74 -0
  41. package/dist/worker/testShims/targetFpsNative.js.map +1 -0
  42. package/package.json +37 -0
@@ -0,0 +1,942 @@
1
+ /**
2
+ * Node RuntimeBackend implementation (LOCKED behavior).
3
+ * @see docs/backend/worker-model.md
4
+ * @see docs/guide/lifecycle-and-updates.md
5
+ * @see docs/backend/node.md
6
+ * @see docs/dev/style-guide.md
7
+ * @see docs/backend/native.md
8
+ */
9
+ import { Worker } from "node:worker_threads";
10
+ import { DEFAULT_TERMINAL_CAPS, FRAME_ACCEPTED_ACK_MARKER } from "@rezi-ui/core";
11
+ import { ZR_DRAWLIST_VERSION_V1, ZR_DRAWLIST_VERSION_V2, ZR_ENGINE_ABI_MAJOR, ZR_ENGINE_ABI_MINOR, ZR_ENGINE_ABI_PATCH, ZR_EVENT_BATCH_VERSION_V1, ZrUiError, severityToNum, } from "@rezi-ui/core";
12
+ import { FRAME_SAB_CONTROL_CONSUMED_SEQ_WORD, FRAME_SAB_CONTROL_HEADER_WORDS, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, FRAME_SAB_CONTROL_PUBLISHED_TOKEN_WORD, FRAME_SAB_CONTROL_WORDS_PER_SLOT, FRAME_SAB_SLOT_STATE_FREE, FRAME_SAB_SLOT_STATE_READY, FRAME_SAB_SLOT_STATE_WRITING, FRAME_TRANSPORT_SAB_V1, FRAME_TRANSPORT_TRANSFER_V1, FRAME_TRANSPORT_VERSION, } from "../worker/protocol.js";
13
+ import { createNodeBackendInlineInternal } from "./nodeBackendInline.js";
14
+ function deferred() {
15
+ let resolve;
16
+ let reject;
17
+ const promise = new Promise((res, rej) => {
18
+ resolve = res;
19
+ reject = (err) => rej(err instanceof Error ? err : new Error(String(err)));
20
+ });
21
+ return { promise, resolve, reject };
22
+ }
23
+ function parsePositiveIntOr(n, fallback) {
24
+ if (typeof n !== "number")
25
+ return fallback;
26
+ if (!Number.isFinite(n))
27
+ return fallback;
28
+ if (!Number.isInteger(n))
29
+ return fallback;
30
+ if (n <= 0)
31
+ return fallback;
32
+ return n;
33
+ }
34
+ function parsePositiveInt(n) {
35
+ if (typeof n !== "number")
36
+ return null;
37
+ if (!Number.isFinite(n))
38
+ return null;
39
+ if (!Number.isInteger(n))
40
+ return null;
41
+ if (n <= 0)
42
+ return null;
43
+ return n;
44
+ }
45
+ function readNativeTargetFps(cfg) {
46
+ const targetFpsCfg = cfg;
47
+ return parsePositiveInt(targetFpsCfg.targetFps) ?? parsePositiveInt(targetFpsCfg.target_fps);
48
+ }
49
+ function safeErr(err) {
50
+ return err instanceof Error ? err : new Error(String(err));
51
+ }
52
+ const DEBUG_QUERY_DEFAULT_RECORDS = 4096;
53
+ const DEBUG_QUERY_MAX_RECORDS = 16384;
54
+ const FRAME_SAB_SLOT_COUNT_DEFAULT = 8;
55
+ const FRAME_SAB_SLOT_BYTES_DEFAULT = 1 << 20;
56
+ function copyInto(buf, bytes) {
57
+ new Uint8Array(buf, 0, bytes.byteLength).set(bytes);
58
+ }
59
+ function frameSeqToSlotToken(frameSeq) {
60
+ const token = frameSeq & 0x7fff_ffff;
61
+ return token === 0 ? 1 : token;
62
+ }
63
+ function createSabFrameTransport(slotCount, slotBytes) {
64
+ if (typeof SharedArrayBuffer !== "function")
65
+ return null;
66
+ const control = new SharedArrayBuffer((FRAME_SAB_CONTROL_HEADER_WORDS + slotCount * FRAME_SAB_CONTROL_WORDS_PER_SLOT) *
67
+ Int32Array.BYTES_PER_ELEMENT);
68
+ const controlHeader = new Int32Array(control, 0, FRAME_SAB_CONTROL_HEADER_WORDS);
69
+ const states = new Int32Array(control, FRAME_SAB_CONTROL_HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT, slotCount);
70
+ const tokens = new Int32Array(control, (FRAME_SAB_CONTROL_HEADER_WORDS + slotCount) * Int32Array.BYTES_PER_ELEMENT, slotCount);
71
+ Atomics.store(controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 0);
72
+ Atomics.store(controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, 0);
73
+ Atomics.store(controlHeader, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, 0);
74
+ Atomics.store(controlHeader, FRAME_SAB_CONTROL_PUBLISHED_TOKEN_WORD, 0);
75
+ Atomics.store(controlHeader, FRAME_SAB_CONTROL_CONSUMED_SEQ_WORD, 0);
76
+ for (let i = 0; i < slotCount; i++) {
77
+ Atomics.store(states, i, FRAME_SAB_SLOT_STATE_FREE);
78
+ Atomics.store(tokens, i, 0);
79
+ }
80
+ const data = new SharedArrayBuffer(slotCount * slotBytes);
81
+ return {
82
+ control,
83
+ data,
84
+ slotCount,
85
+ slotBytes,
86
+ controlHeader,
87
+ states,
88
+ tokens,
89
+ dataBytes: new Uint8Array(data),
90
+ nextSlot: { value: 0 },
91
+ };
92
+ }
93
+ function resetSabFrameTransport(t) {
94
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 0);
95
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, 0);
96
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, 0);
97
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_TOKEN_WORD, 0);
98
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_CONSUMED_SEQ_WORD, 0);
99
+ for (let i = 0; i < t.slotCount; i++) {
100
+ Atomics.store(t.states, i, FRAME_SAB_SLOT_STATE_FREE);
101
+ Atomics.store(t.tokens, i, 0);
102
+ }
103
+ t.nextSlot.value = 0;
104
+ }
105
+ function acquireSabSlot(t) {
106
+ const start = t.nextSlot.value % t.slotCount;
107
+ for (let i = 0; i < t.slotCount; i++) {
108
+ const slot = (start + i) % t.slotCount;
109
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_FREE, FRAME_SAB_SLOT_STATE_WRITING);
110
+ if (prev === FRAME_SAB_SLOT_STATE_FREE) {
111
+ t.nextSlot.value = (slot + 1) % t.slotCount;
112
+ return slot;
113
+ }
114
+ }
115
+ // Latest-wins semantics allow reclaiming stale READY slots instead of
116
+ // falling back to transfer under pressure.
117
+ for (let i = 0; i < t.slotCount; i++) {
118
+ const slot = (start + i) % t.slotCount;
119
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_READY, FRAME_SAB_SLOT_STATE_WRITING);
120
+ if (prev === FRAME_SAB_SLOT_STATE_READY) {
121
+ t.nextSlot.value = (slot + 1) % t.slotCount;
122
+ return slot;
123
+ }
124
+ }
125
+ return -1;
126
+ }
127
+ function publishSabFrame(t, frameSeq, slotIndex, slotToken, byteLen) {
128
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, slotIndex);
129
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, byteLen);
130
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_TOKEN_WORD, slotToken);
131
+ Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, frameSeq);
132
+ }
133
+ export function createNodeBackendInternal(opts = {}) {
134
+ const cfg = opts.config ?? {};
135
+ const fpsCap = parsePositiveIntOr(cfg.fpsCap, 60);
136
+ const requestedExecutionMode = cfg.executionMode ?? "auto";
137
+ const executionMode = requestedExecutionMode === "inline"
138
+ ? "inline"
139
+ : requestedExecutionMode === "worker"
140
+ ? "worker"
141
+ : fpsCap <= 30
142
+ ? "inline"
143
+ : "worker";
144
+ if (executionMode === "inline") {
145
+ return createNodeBackendInlineInternal(opts);
146
+ }
147
+ const maxEventBytes = parsePositiveIntOr(cfg.maxEventBytes, 1 << 20);
148
+ const useDrawlistV2 = cfg.useDrawlistV2 === true;
149
+ const frameTransportMode = cfg.frameTransport === "transfer" || cfg.frameTransport === "sab" ? cfg.frameTransport : "auto";
150
+ const frameSabSlotCount = parsePositiveIntOr(cfg.frameSabSlotCount, FRAME_SAB_SLOT_COUNT_DEFAULT);
151
+ const frameSabSlotBytes = parsePositiveIntOr(cfg.frameSabSlotBytes, FRAME_SAB_SLOT_BYTES_DEFAULT);
152
+ const sabFrameTransport = frameTransportMode === "transfer"
153
+ ? null
154
+ : createSabFrameTransport(frameSabSlotCount, frameSabSlotBytes);
155
+ const frameTransportWire = sabFrameTransport !== null
156
+ ? {
157
+ kind: FRAME_TRANSPORT_SAB_V1,
158
+ version: FRAME_TRANSPORT_VERSION,
159
+ slotCount: sabFrameTransport.slotCount,
160
+ slotBytes: sabFrameTransport.slotBytes,
161
+ control: sabFrameTransport.control,
162
+ data: sabFrameTransport.data,
163
+ }
164
+ : {
165
+ kind: FRAME_TRANSPORT_TRANSFER_V1,
166
+ version: FRAME_TRANSPORT_VERSION,
167
+ };
168
+ const nativeConfig = typeof cfg.nativeConfig === "object" &&
169
+ cfg.nativeConfig !== null &&
170
+ !Array.isArray(cfg.nativeConfig)
171
+ ? cfg.nativeConfig
172
+ : Object.freeze({});
173
+ const nativeTargetFps = readNativeTargetFps(nativeConfig) ?? fpsCap;
174
+ const initConfig = {
175
+ ...nativeConfig,
176
+ // Keep native tick generation aligned with app/backend fpsCap unless
177
+ // explicitly overridden in nativeConfig.
178
+ targetFps: nativeTargetFps,
179
+ // Negotiation pins (docs/16 + docs/01)
180
+ requestedEngineAbiMajor: ZR_ENGINE_ABI_MAJOR,
181
+ requestedEngineAbiMinor: ZR_ENGINE_ABI_MINOR,
182
+ requestedEngineAbiPatch: ZR_ENGINE_ABI_PATCH,
183
+ requestedDrawlistVersion: useDrawlistV2 ? ZR_DRAWLIST_VERSION_V2 : ZR_DRAWLIST_VERSION_V1,
184
+ requestedEventBatchVersion: ZR_EVENT_BATCH_VERSION_V1,
185
+ // Node worker runtime caps
186
+ fpsCap,
187
+ maxEventBytes,
188
+ frameTransport: frameTransportWire,
189
+ };
190
+ let worker = null;
191
+ let disposed = false;
192
+ let started = false;
193
+ let fatal = null;
194
+ let startDef = null;
195
+ let startSettled = false;
196
+ let stopDef = null;
197
+ let stopSettled = false;
198
+ let exitDef = null;
199
+ let nextFrameSeq = 1;
200
+ const frameAcceptedWaiters = new Map();
201
+ const frameCompletionWaiters = new Map();
202
+ const eventQueue = [];
203
+ const eventWaiters = [];
204
+ const capsWaiters = [];
205
+ let cachedCaps = null;
206
+ let stopRequested = false;
207
+ // =============================================================================
208
+ // Debug request serialization (no request IDs in protocol)
209
+ // =============================================================================
210
+ let debugChain = Promise.resolve();
211
+ let debugEnableDef = null;
212
+ let debugDisableDef = null;
213
+ let debugQueryDef = null;
214
+ let debugGetPayloadDef = null;
215
+ let debugGetStatsDef = null;
216
+ let debugExportDef = null;
217
+ let debugResetDef = null;
218
+ let perfSnapshotDef = null;
219
+ function rejectDebugWaiters(err) {
220
+ debugEnableDef?.reject(err);
221
+ debugEnableDef = null;
222
+ debugDisableDef?.reject(err);
223
+ debugDisableDef = null;
224
+ debugQueryDef?.reject(err);
225
+ debugQueryDef = null;
226
+ debugGetPayloadDef?.reject(err);
227
+ debugGetPayloadDef = null;
228
+ debugGetStatsDef?.reject(err);
229
+ debugGetStatsDef = null;
230
+ debugExportDef?.reject(err);
231
+ debugExportDef = null;
232
+ debugResetDef?.reject(err);
233
+ debugResetDef = null;
234
+ perfSnapshotDef?.reject(err);
235
+ perfSnapshotDef = null;
236
+ }
237
+ function enqueueDebug(fn) {
238
+ const p = debugChain.then(fn, fn);
239
+ debugChain = p.then(() => undefined, () => undefined);
240
+ return p;
241
+ }
242
+ function rejectFrameWaiters(err) {
243
+ for (const waiter of frameAcceptedWaiters.values()) {
244
+ waiter.reject(err);
245
+ }
246
+ frameAcceptedWaiters.clear();
247
+ for (const waiter of frameCompletionWaiters.values()) {
248
+ waiter.reject(err);
249
+ }
250
+ frameCompletionWaiters.clear();
251
+ }
252
+ function resolveAcceptedFramesUpTo(acceptedSeq) {
253
+ if (!Number.isInteger(acceptedSeq) || acceptedSeq <= 0)
254
+ return;
255
+ for (const [seq, waiter] of frameAcceptedWaiters.entries()) {
256
+ if (seq > acceptedSeq)
257
+ continue;
258
+ frameAcceptedWaiters.delete(seq);
259
+ waiter.resolve(undefined);
260
+ }
261
+ }
262
+ function resolveCoalescedCompletionFramesUpTo(acceptedSeq) {
263
+ if (!Number.isInteger(acceptedSeq) || acceptedSeq <= 0)
264
+ return;
265
+ for (const [seq, waiter] of frameCompletionWaiters.entries()) {
266
+ if (seq >= acceptedSeq)
267
+ continue;
268
+ frameCompletionWaiters.delete(seq);
269
+ waiter.resolve(undefined);
270
+ }
271
+ }
272
+ function settleCompletedFrame(frameSeq, completedResult) {
273
+ const waiter = frameCompletionWaiters.get(frameSeq);
274
+ if (waiter === undefined)
275
+ return;
276
+ frameCompletionWaiters.delete(frameSeq);
277
+ if (completedResult < 0) {
278
+ waiter.reject(new ZrUiError("ZRUI_BACKEND_ERROR", `engine frame completion failed: seq=${String(frameSeq)} code=${String(completedResult)}`));
279
+ return;
280
+ }
281
+ waiter.resolve(undefined);
282
+ }
283
+ function failAll(err) {
284
+ while (eventWaiters.length > 0)
285
+ eventWaiters.shift()?.reject(err);
286
+ while (capsWaiters.length > 0)
287
+ capsWaiters.shift()?.reject(err);
288
+ eventQueue.length = 0;
289
+ rejectFrameWaiters(err);
290
+ rejectDebugWaiters(err);
291
+ if (startDef !== null && !startSettled) {
292
+ startSettled = true;
293
+ startDef.reject(err);
294
+ }
295
+ if (stopDef !== null && !stopSettled) {
296
+ stopSettled = true;
297
+ stopDef.reject(err);
298
+ }
299
+ }
300
+ function rejectPending(err) {
301
+ while (eventWaiters.length > 0)
302
+ eventWaiters.shift()?.reject(err);
303
+ while (capsWaiters.length > 0)
304
+ capsWaiters.shift()?.reject(err);
305
+ eventQueue.length = 0;
306
+ rejectFrameWaiters(err);
307
+ rejectDebugWaiters(err);
308
+ if (startDef !== null && !startSettled) {
309
+ startSettled = true;
310
+ startDef.reject(err);
311
+ }
312
+ }
313
+ function send(msg, transfer) {
314
+ if (worker === null)
315
+ return;
316
+ if (transfer !== undefined) {
317
+ worker.postMessage(msg, transfer);
318
+ return;
319
+ }
320
+ worker.postMessage(msg);
321
+ }
322
+ function handleWorkerMessage(m) {
323
+ if (fatal !== null)
324
+ return;
325
+ if (typeof m !== "object" || m === null)
326
+ return;
327
+ const type = m.type;
328
+ if (typeof type !== "string")
329
+ return;
330
+ const msg = m;
331
+ switch (msg.type) {
332
+ case "ready": {
333
+ started = true;
334
+ stopRequested = false;
335
+ if (startDef !== null && !startSettled) {
336
+ startSettled = true;
337
+ startDef.resolve();
338
+ }
339
+ return;
340
+ }
341
+ case "frameStatus": {
342
+ if (!Number.isInteger(msg.acceptedSeq) || msg.acceptedSeq <= 0) {
343
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `invalid frameStatus.acceptedSeq: ${String(msg.acceptedSeq)}`);
344
+ failAll(fatal);
345
+ return;
346
+ }
347
+ resolveAcceptedFramesUpTo(msg.acceptedSeq);
348
+ resolveCoalescedCompletionFramesUpTo(msg.acceptedSeq);
349
+ if (msg.completedSeq !== undefined) {
350
+ if (!Number.isInteger(msg.completedSeq) || msg.completedSeq <= 0) {
351
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `invalid frameStatus.completedSeq: ${String(msg.completedSeq)}`);
352
+ failAll(fatal);
353
+ return;
354
+ }
355
+ const completedResult = msg.completedResult ?? 0;
356
+ if (!Number.isInteger(completedResult)) {
357
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `invalid frameStatus.completedResult: ${String(msg.completedResult)}`);
358
+ failAll(fatal);
359
+ return;
360
+ }
361
+ settleCompletedFrame(msg.completedSeq, completedResult);
362
+ if (completedResult < 0) {
363
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `engine frame failed: seq=${String(msg.completedSeq)} code=${String(completedResult)}`);
364
+ failAll(fatal);
365
+ return;
366
+ }
367
+ return;
368
+ }
369
+ return;
370
+ }
371
+ case "events": {
372
+ const waiter = eventWaiters.shift();
373
+ if (waiter !== undefined) {
374
+ const buf = msg.batch;
375
+ const bytes = new Uint8Array(buf, 0, msg.byteLen);
376
+ let released = false;
377
+ waiter.resolve({
378
+ bytes,
379
+ droppedBatches: msg.droppedSinceLast,
380
+ release: () => {
381
+ if (released)
382
+ return;
383
+ released = true;
384
+ if (disposed)
385
+ return;
386
+ send({ type: "eventsAck", buffer: buf }, [buf]);
387
+ },
388
+ });
389
+ return;
390
+ }
391
+ eventQueue.push({
392
+ batch: msg.batch,
393
+ byteLen: msg.byteLen,
394
+ droppedSinceLast: msg.droppedSinceLast,
395
+ });
396
+ return;
397
+ }
398
+ case "fatal": {
399
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `worker fatal: ${msg.where} (${msg.code}): ${msg.detail}`);
400
+ failAll(fatal);
401
+ return;
402
+ }
403
+ case "debug:enableResult": {
404
+ debugEnableDef?.resolve(msg.result);
405
+ debugEnableDef = null;
406
+ return;
407
+ }
408
+ case "debug:disableResult": {
409
+ debugDisableDef?.resolve(msg.result);
410
+ debugDisableDef = null;
411
+ return;
412
+ }
413
+ case "debug:queryResult": {
414
+ try {
415
+ if (!Number.isInteger(msg.headersByteLen) || msg.headersByteLen < 0) {
416
+ throw new Error(`debug:queryResult: invalid headersByteLen=${String(msg.headersByteLen)}`);
417
+ }
418
+ if (msg.headersByteLen > msg.headers.byteLength) {
419
+ throw new Error(`debug:queryResult: headersByteLen=${String(msg.headersByteLen)} exceeds buffer=${String(msg.headers.byteLength)}`);
420
+ }
421
+ const headers = new Uint8Array(msg.headers, 0, msg.headersByteLen);
422
+ const result = {
423
+ recordsReturned: msg.result.recordsReturned,
424
+ recordsAvailable: msg.result.recordsAvailable,
425
+ oldestRecordId: BigInt(msg.result.oldestRecordId),
426
+ newestRecordId: BigInt(msg.result.newestRecordId),
427
+ recordsDropped: msg.result.recordsDropped,
428
+ };
429
+ debugQueryDef?.resolve({ headers, result });
430
+ debugQueryDef = null;
431
+ return;
432
+ }
433
+ catch (err) {
434
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", safeErr(err).message);
435
+ failAll(fatal);
436
+ return;
437
+ }
438
+ }
439
+ case "debug:getPayloadResult": {
440
+ if (msg.result <= 0 || msg.payloadByteLen <= 0) {
441
+ debugGetPayloadDef?.resolve(null);
442
+ debugGetPayloadDef = null;
443
+ return;
444
+ }
445
+ try {
446
+ if (!Number.isInteger(msg.payloadByteLen) || msg.payloadByteLen < 0) {
447
+ throw new Error(`debug:getPayloadResult: invalid payloadByteLen=${String(msg.payloadByteLen)}`);
448
+ }
449
+ if (msg.payloadByteLen > msg.payload.byteLength) {
450
+ throw new Error(`debug:getPayloadResult: payloadByteLen=${String(msg.payloadByteLen)} exceeds buffer=${String(msg.payload.byteLength)}`);
451
+ }
452
+ debugGetPayloadDef?.resolve(new Uint8Array(msg.payload, 0, msg.payloadByteLen));
453
+ debugGetPayloadDef = null;
454
+ return;
455
+ }
456
+ catch (err) {
457
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", safeErr(err).message);
458
+ failAll(fatal);
459
+ return;
460
+ }
461
+ }
462
+ case "debug:getStatsResult": {
463
+ try {
464
+ const stats = {
465
+ totalRecords: BigInt(msg.stats.totalRecords),
466
+ totalDropped: BigInt(msg.stats.totalDropped),
467
+ errorCount: msg.stats.errorCount,
468
+ warnCount: msg.stats.warnCount,
469
+ currentRingUsage: msg.stats.currentRingUsage,
470
+ ringCapacity: msg.stats.ringCapacity,
471
+ };
472
+ debugGetStatsDef?.resolve(stats);
473
+ debugGetStatsDef = null;
474
+ return;
475
+ }
476
+ catch (err) {
477
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", safeErr(err).message);
478
+ failAll(fatal);
479
+ return;
480
+ }
481
+ }
482
+ case "debug:exportResult": {
483
+ try {
484
+ if (!Number.isInteger(msg.bufferByteLen) || msg.bufferByteLen < 0) {
485
+ throw new Error(`debug:exportResult: invalid bufferByteLen=${String(msg.bufferByteLen)}`);
486
+ }
487
+ if (msg.bufferByteLen > msg.buffer.byteLength) {
488
+ throw new Error(`debug:exportResult: bufferByteLen=${String(msg.bufferByteLen)} exceeds buffer=${String(msg.buffer.byteLength)}`);
489
+ }
490
+ debugExportDef?.resolve(new Uint8Array(msg.buffer, 0, msg.bufferByteLen));
491
+ debugExportDef = null;
492
+ return;
493
+ }
494
+ catch (err) {
495
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", safeErr(err).message);
496
+ failAll(fatal);
497
+ return;
498
+ }
499
+ }
500
+ case "debug:resetResult": {
501
+ debugResetDef?.resolve(msg.result);
502
+ debugResetDef = null;
503
+ return;
504
+ }
505
+ case "perf:snapshotResult": {
506
+ perfSnapshotDef?.resolve(msg.snapshot);
507
+ perfSnapshotDef = null;
508
+ return;
509
+ }
510
+ case "shutdownComplete": {
511
+ rejectPending(new Error("NodeBackend: stopped"));
512
+ if (stopDef !== null && !stopSettled) {
513
+ stopSettled = true;
514
+ stopDef.resolve();
515
+ }
516
+ return;
517
+ }
518
+ case "caps": {
519
+ const caps = {
520
+ colorMode: msg.colorMode,
521
+ supportsMouse: msg.supportsMouse,
522
+ supportsBracketedPaste: msg.supportsBracketedPaste,
523
+ supportsFocusEvents: msg.supportsFocusEvents,
524
+ supportsOsc52: msg.supportsOsc52,
525
+ supportsSyncUpdate: msg.supportsSyncUpdate,
526
+ supportsScrollRegion: msg.supportsScrollRegion,
527
+ supportsCursorShape: msg.supportsCursorShape,
528
+ supportsOutputWaitWritable: msg.supportsOutputWaitWritable,
529
+ sgrAttrsSupported: msg.sgrAttrsSupported,
530
+ };
531
+ cachedCaps = caps;
532
+ const waiter = capsWaiters.shift();
533
+ if (waiter !== undefined) {
534
+ waiter.resolve(caps);
535
+ }
536
+ return;
537
+ }
538
+ }
539
+ }
540
+ function handleWorkerExit(code) {
541
+ if (disposed)
542
+ return;
543
+ if (stopRequested) {
544
+ rejectPending(new Error("NodeBackend: stopped"));
545
+ }
546
+ if (code !== 0 && fatal === null) {
547
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `worker exited unexpectedly: code=${String(code)}`);
548
+ failAll(fatal);
549
+ }
550
+ if (code === 0 && !started && fatal === null && startDef !== null && !startSettled) {
551
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", "worker exited before ready handshake");
552
+ failAll(fatal);
553
+ }
554
+ if (stopDef !== null && !stopSettled) {
555
+ stopSettled = true;
556
+ stopDef.resolve();
557
+ }
558
+ if (exitDef !== null)
559
+ exitDef.resolve();
560
+ worker = null;
561
+ started = false;
562
+ }
563
+ const backend = {
564
+ async start() {
565
+ if (disposed)
566
+ throw new Error("NodeBackend: disposed");
567
+ if (fatal !== null)
568
+ throw fatal;
569
+ if (started)
570
+ return;
571
+ if (worker === null) {
572
+ startDef = deferred();
573
+ startSettled = false;
574
+ stopDef = null;
575
+ stopSettled = false;
576
+ stopRequested = false;
577
+ if (sabFrameTransport !== null)
578
+ resetSabFrameTransport(sabFrameTransport);
579
+ const entry = new URL("../worker/engineWorker.js", import.meta.url);
580
+ const workerData = opts.nativeShimModule === undefined
581
+ ? undefined
582
+ : { nativeShimModule: opts.nativeShimModule };
583
+ worker = new Worker(entry, { workerData });
584
+ exitDef = deferred();
585
+ worker.on("message", handleWorkerMessage);
586
+ worker.on("error", (err) => {
587
+ if (fatal !== null)
588
+ return;
589
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", safeErr(err).message);
590
+ failAll(fatal);
591
+ });
592
+ worker.on("exit", (code) => {
593
+ handleWorkerExit(code);
594
+ });
595
+ send({ type: "init", config: initConfig });
596
+ }
597
+ if (startDef === null)
598
+ throw new Error("NodeBackend: invariant violated (startDef is null)");
599
+ await startDef.promise;
600
+ },
601
+ async stop() {
602
+ if (disposed)
603
+ return;
604
+ if (fatal !== null)
605
+ throw fatal;
606
+ if (worker === null)
607
+ return;
608
+ if (stopDef !== null) {
609
+ await stopDef.promise;
610
+ return;
611
+ }
612
+ stopDef = deferred();
613
+ stopSettled = false;
614
+ stopRequested = true;
615
+ send({ type: "shutdown" });
616
+ await stopDef.promise;
617
+ if (exitDef !== null)
618
+ await exitDef.promise;
619
+ },
620
+ dispose() {
621
+ if (disposed)
622
+ return;
623
+ disposed = true;
624
+ if (worker !== null) {
625
+ worker.terminate().catch(() => { });
626
+ worker = null;
627
+ }
628
+ const err = new Error("NodeBackend: disposed");
629
+ while (eventWaiters.length > 0)
630
+ eventWaiters.shift()?.reject(err);
631
+ while (capsWaiters.length > 0)
632
+ capsWaiters.shift()?.reject(err);
633
+ eventQueue.length = 0;
634
+ rejectFrameWaiters(err);
635
+ rejectDebugWaiters(err);
636
+ if (startDef !== null && !startSettled) {
637
+ startSettled = true;
638
+ startDef.reject(err);
639
+ }
640
+ if (stopDef !== null && !stopSettled) {
641
+ stopSettled = true;
642
+ stopDef.reject(err);
643
+ }
644
+ },
645
+ requestFrame(drawlist) {
646
+ if (disposed)
647
+ return Promise.reject(new Error("NodeBackend: disposed"));
648
+ if (fatal !== null)
649
+ return Promise.reject(fatal);
650
+ if (stopRequested)
651
+ return Promise.reject(new Error("NodeBackend: stopped"));
652
+ if (!started) {
653
+ return backend.start().then(() => backend.requestFrame(drawlist));
654
+ }
655
+ if (worker === null)
656
+ return Promise.reject(new Error("NodeBackend: worker not available"));
657
+ const frameSeq = nextFrameSeq++;
658
+ const frameAcceptedDef = deferred();
659
+ frameAcceptedWaiters.set(frameSeq, frameAcceptedDef);
660
+ const frameCompletionDef = deferred();
661
+ frameCompletionWaiters.set(frameSeq, frameCompletionDef);
662
+ const framePromise = frameCompletionDef.promise;
663
+ Object.defineProperty(framePromise, FRAME_ACCEPTED_ACK_MARKER, {
664
+ value: frameAcceptedDef.promise,
665
+ configurable: false,
666
+ enumerable: false,
667
+ writable: false,
668
+ });
669
+ if (sabFrameTransport !== null && drawlist.byteLength <= sabFrameTransport.slotBytes) {
670
+ const slotIndex = acquireSabSlot(sabFrameTransport);
671
+ if (slotIndex >= 0) {
672
+ const slotToken = frameSeqToSlotToken(frameSeq);
673
+ const slotOffset = slotIndex * sabFrameTransport.slotBytes;
674
+ sabFrameTransport.dataBytes.set(drawlist, slotOffset);
675
+ Atomics.store(sabFrameTransport.tokens, slotIndex, slotToken);
676
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_READY);
677
+ publishSabFrame(sabFrameTransport, frameSeq, slotIndex, slotToken, drawlist.byteLength);
678
+ // SAB consumers wake on futex notify instead of per-frame
679
+ // MessagePort frameKick round-trips.
680
+ Atomics.notify(sabFrameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 1);
681
+ return framePromise;
682
+ }
683
+ }
684
+ // Transfer fallback participates in the same ACK model:
685
+ // - accepted ACK (hidden marker) can unblock app scheduling early
686
+ // - completion promise settles on worker completion/coalescing status
687
+ const buf = new ArrayBuffer(drawlist.byteLength);
688
+ copyInto(buf, drawlist);
689
+ try {
690
+ send({
691
+ type: "frame",
692
+ frameSeq,
693
+ byteLen: drawlist.byteLength,
694
+ transport: FRAME_TRANSPORT_TRANSFER_V1,
695
+ drawlist: buf,
696
+ }, [buf]);
697
+ }
698
+ catch (err) {
699
+ frameAcceptedWaiters.delete(frameSeq);
700
+ frameCompletionWaiters.delete(frameSeq);
701
+ return Promise.reject(safeErr(err));
702
+ }
703
+ return framePromise;
704
+ },
705
+ pollEvents() {
706
+ if (disposed)
707
+ return Promise.reject(new Error("NodeBackend: disposed"));
708
+ if (fatal !== null)
709
+ return Promise.reject(fatal);
710
+ if (stopRequested)
711
+ return Promise.reject(new Error("NodeBackend: stopped"));
712
+ const queued = eventQueue.shift();
713
+ if (queued !== undefined) {
714
+ const buf = queued.batch;
715
+ const bytes = new Uint8Array(buf, 0, queued.byteLen);
716
+ let released = false;
717
+ return Promise.resolve({
718
+ bytes,
719
+ droppedBatches: queued.droppedSinceLast,
720
+ release: () => {
721
+ if (released)
722
+ return;
723
+ released = true;
724
+ if (disposed)
725
+ return;
726
+ send({ type: "eventsAck", buffer: buf }, [buf]);
727
+ },
728
+ });
729
+ }
730
+ const d = deferred();
731
+ eventWaiters.push(d);
732
+ return d.promise;
733
+ },
734
+ postUserEvent(tag, payload) {
735
+ if (disposed)
736
+ throw new Error("NodeBackend: disposed");
737
+ if (fatal !== null)
738
+ throw fatal;
739
+ if (worker === null)
740
+ throw new Error("NodeBackend: not started");
741
+ if (stopRequested)
742
+ throw new Error("NodeBackend: stopped");
743
+ const buf = new ArrayBuffer(payload.byteLength);
744
+ copyInto(buf, payload);
745
+ send({ type: "postUserEvent", tag, payload: buf, byteLen: payload.byteLength }, [buf]);
746
+ },
747
+ async getCaps() {
748
+ if (disposed)
749
+ throw new Error("NodeBackend: disposed");
750
+ if (fatal !== null)
751
+ throw fatal;
752
+ // Return cached caps if available
753
+ if (cachedCaps !== null) {
754
+ return cachedCaps;
755
+ }
756
+ // If not started, return default caps
757
+ if (!started || worker === null) {
758
+ return DEFAULT_TERMINAL_CAPS;
759
+ }
760
+ // Request caps from worker
761
+ const d = deferred();
762
+ capsWaiters.push(d);
763
+ send({ type: "getCaps" });
764
+ return d.promise;
765
+ },
766
+ };
767
+ const debug = {
768
+ debugEnable: (config) => enqueueDebug(async () => {
769
+ if (disposed)
770
+ throw new Error("NodeBackend: disposed");
771
+ if (fatal !== null)
772
+ throw fatal;
773
+ await backend.start();
774
+ if (worker === null)
775
+ throw new Error("NodeBackend: worker not available");
776
+ if (debugEnableDef !== null)
777
+ throw new Error("NodeBackend: debugEnable already in-flight");
778
+ debugEnableDef = deferred();
779
+ const minSeverity = config.minSeverity !== undefined ? severityToNum(config.minSeverity) : null;
780
+ const configWire = {
781
+ enabled: true,
782
+ ...(config.ringCapacity !== undefined ? { ringCapacity: config.ringCapacity } : {}),
783
+ ...(minSeverity !== null ? { minSeverity } : {}),
784
+ ...(config.categoryMask !== undefined ? { categoryMask: config.categoryMask } : {}),
785
+ ...(config.captureRawEvents !== undefined
786
+ ? { captureRawEvents: config.captureRawEvents }
787
+ : {}),
788
+ ...(config.captureDrawlistBytes !== undefined
789
+ ? { captureDrawlistBytes: config.captureDrawlistBytes }
790
+ : {}),
791
+ };
792
+ send({ type: "debug:enable", config: configWire });
793
+ const rc = await debugEnableDef.promise;
794
+ debugEnableDef = null;
795
+ if (rc < 0) {
796
+ throw new ZrUiError("ZRUI_BACKEND_ERROR", `engineDebugEnable failed: code=${String(rc)}`);
797
+ }
798
+ }),
799
+ debugDisable: () => enqueueDebug(async () => {
800
+ if (disposed)
801
+ throw new Error("NodeBackend: disposed");
802
+ if (fatal !== null)
803
+ throw fatal;
804
+ await backend.start();
805
+ if (worker === null)
806
+ throw new Error("NodeBackend: worker not available");
807
+ if (debugDisableDef !== null)
808
+ throw new Error("NodeBackend: debugDisable already in-flight");
809
+ debugDisableDef = deferred();
810
+ send({ type: "debug:disable" });
811
+ const rc = await debugDisableDef.promise;
812
+ debugDisableDef = null;
813
+ if (rc < 0) {
814
+ throw new ZrUiError("ZRUI_BACKEND_ERROR", `engineDebugDisable failed: code=${String(rc)}`);
815
+ }
816
+ }),
817
+ debugQuery: (query) => enqueueDebug(async () => {
818
+ if (disposed)
819
+ throw new Error("NodeBackend: disposed");
820
+ if (fatal !== null)
821
+ throw fatal;
822
+ await backend.start();
823
+ if (worker === null)
824
+ throw new Error("NodeBackend: worker not available");
825
+ if (debugQueryDef !== null)
826
+ throw new Error("NodeBackend: debugQuery already in-flight");
827
+ debugQueryDef = deferred();
828
+ const minSeverity = query.minSeverity !== undefined ? severityToNum(query.minSeverity) : null;
829
+ const maxRecords = parsePositiveInt(query.maxRecords);
830
+ const recordsCap = maxRecords !== null
831
+ ? Math.min(maxRecords, DEBUG_QUERY_MAX_RECORDS)
832
+ : DEBUG_QUERY_DEFAULT_RECORDS;
833
+ const headersCap = Math.max(40, recordsCap * 40);
834
+ const queryWire = {
835
+ ...(query.minRecordId !== undefined ? { minRecordId: String(query.minRecordId) } : {}),
836
+ ...(query.maxRecordId !== undefined ? { maxRecordId: String(query.maxRecordId) } : {}),
837
+ ...(query.minFrameId !== undefined ? { minFrameId: String(query.minFrameId) } : {}),
838
+ ...(query.maxFrameId !== undefined ? { maxFrameId: String(query.maxFrameId) } : {}),
839
+ ...(query.categoryMask !== undefined ? { categoryMask: query.categoryMask } : {}),
840
+ ...(minSeverity !== null ? { minSeverity } : {}),
841
+ ...(maxRecords !== null ? { maxRecords: recordsCap } : {}),
842
+ };
843
+ send({ type: "debug:query", query: queryWire, headersCap });
844
+ const out = await debugQueryDef.promise;
845
+ debugQueryDef = null;
846
+ return out;
847
+ }),
848
+ debugGetPayload: (recordId) => enqueueDebug(async () => {
849
+ if (disposed)
850
+ throw new Error("NodeBackend: disposed");
851
+ if (fatal !== null)
852
+ throw fatal;
853
+ await backend.start();
854
+ if (worker === null)
855
+ throw new Error("NodeBackend: worker not available");
856
+ if (debugGetPayloadDef !== null) {
857
+ throw new Error("NodeBackend: debugGetPayload already in-flight");
858
+ }
859
+ debugGetPayloadDef = deferred();
860
+ // Payloads can include raw drawlist bytes. Default to 4 MiB.
861
+ const payloadCap = 1 << 22;
862
+ send({
863
+ type: "debug:getPayload",
864
+ recordId: String(recordId),
865
+ payloadCap,
866
+ });
867
+ const bytes = await debugGetPayloadDef.promise;
868
+ debugGetPayloadDef = null;
869
+ return bytes;
870
+ }),
871
+ debugGetStats: () => enqueueDebug(async () => {
872
+ if (disposed)
873
+ throw new Error("NodeBackend: disposed");
874
+ if (fatal !== null)
875
+ throw fatal;
876
+ await backend.start();
877
+ if (worker === null)
878
+ throw new Error("NodeBackend: worker not available");
879
+ if (debugGetStatsDef !== null)
880
+ throw new Error("NodeBackend: debugGetStats already in-flight");
881
+ debugGetStatsDef = deferred();
882
+ send({ type: "debug:getStats" });
883
+ const stats = await debugGetStatsDef.promise;
884
+ debugGetStatsDef = null;
885
+ return stats;
886
+ }),
887
+ debugExport: () => enqueueDebug(async () => {
888
+ if (disposed)
889
+ throw new Error("NodeBackend: disposed");
890
+ if (fatal !== null)
891
+ throw fatal;
892
+ await backend.start();
893
+ if (worker === null)
894
+ throw new Error("NodeBackend: worker not available");
895
+ if (debugExportDef !== null)
896
+ throw new Error("NodeBackend: debugExport already in-flight");
897
+ debugExportDef = deferred();
898
+ send({ type: "debug:export", bufferCap: 1 << 23 }); // 8 MiB
899
+ const bytes = await debugExportDef.promise;
900
+ debugExportDef = null;
901
+ return bytes;
902
+ }),
903
+ debugReset: () => enqueueDebug(async () => {
904
+ if (disposed)
905
+ throw new Error("NodeBackend: disposed");
906
+ if (fatal !== null)
907
+ throw fatal;
908
+ await backend.start();
909
+ if (worker === null)
910
+ throw new Error("NodeBackend: worker not available");
911
+ if (debugResetDef !== null)
912
+ throw new Error("NodeBackend: debugReset already in-flight");
913
+ debugResetDef = deferred();
914
+ send({ type: "debug:reset" });
915
+ const rc = await debugResetDef.promise;
916
+ debugResetDef = null;
917
+ if (rc < 0) {
918
+ throw new ZrUiError("ZRUI_BACKEND_ERROR", `engineDebugReset failed: code=${String(rc)}`);
919
+ }
920
+ }),
921
+ };
922
+ const perf = {
923
+ perfSnapshot: () => enqueueDebug(async () => {
924
+ if (disposed)
925
+ throw new Error("NodeBackend: disposed");
926
+ if (fatal !== null)
927
+ throw fatal;
928
+ await backend.start();
929
+ if (worker === null)
930
+ throw new Error("NodeBackend: worker not available");
931
+ if (perfSnapshotDef !== null)
932
+ throw new Error("NodeBackend: perfSnapshot already in-flight");
933
+ perfSnapshotDef = deferred();
934
+ send({ type: "perf:snapshot" });
935
+ const snapshot = await perfSnapshotDef.promise;
936
+ perfSnapshotDef = null;
937
+ return snapshot;
938
+ }),
939
+ };
940
+ return Object.assign(backend, { debug, perf });
941
+ }
942
+ //# sourceMappingURL=nodeBackend.js.map