@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,1001 @@
1
+ /**
2
+ * Node worker-thread entrypoint owning the native engine (LOCKED).
3
+ * @see docs/backend/worker-model.md
4
+ * @see docs/backend/node.md
5
+ * @see docs/dev/style-guide.md
6
+ * @see docs/backend/native.md
7
+ */
8
+ import { performance } from "node:perf_hooks";
9
+ import { parentPort, workerData } from "node:worker_threads";
10
+ import { EVENT_POOL_SIZE, 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_IN_USE, FRAME_SAB_SLOT_STATE_READY, FRAME_TRANSPORT_SAB_V1, FRAME_TRANSPORT_TRANSFER_V1, FRAME_TRANSPORT_VERSION, MAX_POLL_DRAIN_ITERS, } from "./protocol.js";
11
+ /**
12
+ * Perf tracking for worker-side event polling.
13
+ * Only active when REZI_PERF=1.
14
+ */
15
+ const PERF_ENABLED = process.env.REZI_PERF === "1";
16
+ const perfSamples = [];
17
+ const PERF_MAX_SAMPLES = 1024;
18
+ function perfRecord(phase, durationMs) {
19
+ if (!PERF_ENABLED)
20
+ return;
21
+ if (perfSamples.length >= PERF_MAX_SAMPLES) {
22
+ perfSamples.shift();
23
+ }
24
+ perfSamples.push({ phase, durationMs });
25
+ }
26
+ function perfSnapshot() {
27
+ const byPhase = new Map();
28
+ for (const s of perfSamples) {
29
+ let arr = byPhase.get(s.phase);
30
+ if (!arr) {
31
+ arr = [];
32
+ byPhase.set(s.phase, arr);
33
+ }
34
+ arr.push(s.durationMs);
35
+ }
36
+ const phases = {};
37
+ for (const [phase, samples] of byPhase) {
38
+ const sorted = [...samples].sort((a, b) => a - b);
39
+ const sum = sorted.reduce((acc, v) => acc + v, 0);
40
+ const p50Idx = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.5));
41
+ const p95Idx = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95));
42
+ const p99Idx = Math.min(sorted.length - 1, Math.floor(sorted.length * 0.99));
43
+ const worst10Start = Math.max(0, sorted.length - 10);
44
+ const worst10 = sorted.slice(worst10Start).reverse();
45
+ phases[phase] = {
46
+ count: sorted.length,
47
+ avg: sorted.length > 0 ? sum / sorted.length : 0,
48
+ p50: sorted[p50Idx] ?? 0,
49
+ p95: sorted[p95Idx] ?? 0,
50
+ p99: sorted[p99Idx] ?? 0,
51
+ max: sorted[sorted.length - 1] ?? 0,
52
+ worst10,
53
+ };
54
+ }
55
+ return { phases };
56
+ }
57
+ function postToMain(msg, transfer) {
58
+ if (parentPort === null)
59
+ return;
60
+ if (transfer !== undefined) {
61
+ parentPort.postMessage(msg, transfer);
62
+ return;
63
+ }
64
+ parentPort.postMessage(msg);
65
+ }
66
+ function safeDetail(err) {
67
+ if (err instanceof Error)
68
+ return `${err.name}: ${err.message}`;
69
+ return String(err);
70
+ }
71
+ function parsePositiveInt(n) {
72
+ if (typeof n !== "number")
73
+ return null;
74
+ if (!Number.isFinite(n))
75
+ return null;
76
+ if (!Number.isInteger(n))
77
+ return null;
78
+ if (n <= 0)
79
+ return null;
80
+ return n;
81
+ }
82
+ function parseFrameTransportConfig(cfg) {
83
+ if (typeof cfg !== "object" || cfg === null) {
84
+ return Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
85
+ }
86
+ const wire = cfg;
87
+ if (wire.kind !== FRAME_TRANSPORT_SAB_V1) {
88
+ return Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
89
+ }
90
+ if (wire.version !== FRAME_TRANSPORT_VERSION) {
91
+ return Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
92
+ }
93
+ const slotCount = parsePositiveInt(wire.slotCount);
94
+ const slotBytes = parsePositiveInt(wire.slotBytes);
95
+ if (slotCount === null || slotBytes === null) {
96
+ return Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
97
+ }
98
+ if (!(wire.control instanceof SharedArrayBuffer) || !(wire.data instanceof SharedArrayBuffer)) {
99
+ return Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
100
+ }
101
+ const control = new Int32Array(wire.control);
102
+ if (control.length <
103
+ FRAME_SAB_CONTROL_HEADER_WORDS + slotCount * FRAME_SAB_CONTROL_WORDS_PER_SLOT) {
104
+ return Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
105
+ }
106
+ const controlHeader = new Int32Array(wire.control, 0, FRAME_SAB_CONTROL_HEADER_WORDS);
107
+ const states = new Int32Array(wire.control, FRAME_SAB_CONTROL_HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT, slotCount);
108
+ const tokens = new Int32Array(wire.control, (FRAME_SAB_CONTROL_HEADER_WORDS + slotCount) * Int32Array.BYTES_PER_ELEMENT, slotCount);
109
+ const data = new Uint8Array(wire.data);
110
+ if (data.byteLength < slotCount * slotBytes) {
111
+ return Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
112
+ }
113
+ return Object.freeze({
114
+ kind: FRAME_TRANSPORT_SAB_V1,
115
+ slotCount,
116
+ slotBytes,
117
+ controlHeader,
118
+ states,
119
+ tokens,
120
+ data,
121
+ });
122
+ }
123
+ async function loadNative() {
124
+ const wd = workerData && typeof workerData === "object" ? workerData : Object.freeze({});
125
+ const shim = typeof wd.nativeShimModule === "string" ? wd.nativeShimModule : null;
126
+ const unwrap = (m) => {
127
+ if (typeof m === "object" && m !== null) {
128
+ const rec = m;
129
+ const candidate = (rec.native ?? rec.default ?? rec);
130
+ return candidate;
131
+ }
132
+ return m;
133
+ };
134
+ if (shim !== null && shim.length > 0) {
135
+ return unwrap((await import(shim)));
136
+ }
137
+ try {
138
+ return unwrap((await import("@rezi-ui/native")));
139
+ }
140
+ catch (err) {
141
+ const detail = safeDetail(err);
142
+ throw new Error(`Failed to load @rezi-ui/native.\n\nThis usually means the native addon was not built or not installed for this platform.\n\n${detail}`);
143
+ }
144
+ }
145
+ const native = await loadNative();
146
+ // Little-endian u32 magic for bytes "ZREV".
147
+ const ZREV_MAGIC = 0x5645525a;
148
+ const ZR_EVENT_BATCH_VERSION_V1 = 1;
149
+ const ZREV_RECORD_RESIZE = 5;
150
+ const DEBUG_HEADER_BYTES = 40;
151
+ const DEBUG_QUERY_MIN_HEADERS_CAP = DEBUG_HEADER_BYTES;
152
+ const DEBUG_QUERY_MAX_HEADERS_CAP = 1 << 20; // 1 MiB
153
+ const NO_RECYCLED_DRAWLISTS = Object.freeze([]);
154
+ let engineId = null;
155
+ let running = false;
156
+ let haveSubmittedDrawlist = false;
157
+ let pendingFrame = null;
158
+ let lastConsumedSabPublishedSeq = 0;
159
+ let frameTransport = Object.freeze({ kind: FRAME_TRANSPORT_TRANSFER_V1 });
160
+ let eventPool = [];
161
+ let discardBuffer = null;
162
+ let droppedSinceLast = 0;
163
+ let tickTimer = null;
164
+ let tickImmediate = null;
165
+ let tickIntervalMs = 16;
166
+ let idleDelayMs = 0;
167
+ let maxIdleDelayMs = 50;
168
+ const MAX_IDLE_BACKOFF_MS = 1;
169
+ const MAX_EVENT_POLL_INTERVAL_MS = 1;
170
+ let sabWakeArmed = false;
171
+ let sabWakeEpoch = 0;
172
+ function writeResizeBatchV1(buf, cols, rows) {
173
+ // Batch header (24) + RESIZE record (32) = 56 bytes.
174
+ const totalSize = 56;
175
+ if (buf.byteLength < totalSize)
176
+ return 0;
177
+ const dv = new DataView(buf);
178
+ const timeMs = (Date.now() >>> 0) & 0xffff_ffff;
179
+ dv.setUint32(0, ZREV_MAGIC, true);
180
+ dv.setUint32(4, ZR_EVENT_BATCH_VERSION_V1, true);
181
+ dv.setUint32(8, totalSize, true);
182
+ dv.setUint32(12, 1, true); // event_count
183
+ dv.setUint32(16, 0, true); // batch_flags
184
+ dv.setUint32(20, 0, true); // reserved0
185
+ dv.setUint32(24, ZREV_RECORD_RESIZE, true);
186
+ dv.setUint32(28, 32, true); // record_size
187
+ dv.setUint32(32, timeMs, true);
188
+ dv.setUint32(36, 0, true); // flags
189
+ dv.setUint32(40, cols >>> 0, true);
190
+ dv.setUint32(44, rows >>> 0, true);
191
+ dv.setUint32(48, 0, true);
192
+ dv.setUint32(52, 0, true);
193
+ return totalSize;
194
+ }
195
+ function maybeInjectInitialResize(maxEventBytes) {
196
+ const cols = typeof process.stdout.columns === "number" &&
197
+ Number.isInteger(process.stdout.columns) &&
198
+ process.stdout.columns > 0
199
+ ? process.stdout.columns
200
+ : 80;
201
+ const rows = typeof process.stdout.rows === "number" &&
202
+ Number.isInteger(process.stdout.rows) &&
203
+ process.stdout.rows > 0
204
+ ? process.stdout.rows
205
+ : 24;
206
+ const buf = eventPool.pop() ?? new ArrayBuffer(maxEventBytes);
207
+ const byteLen = writeResizeBatchV1(buf, cols, rows);
208
+ if (byteLen <= 0) {
209
+ eventPool.push(buf);
210
+ return;
211
+ }
212
+ postToMain({ type: "events", batch: buf, byteLen, droppedSinceLast: 0 }, [buf]);
213
+ }
214
+ function stopTickLoop() {
215
+ if (tickTimer !== null) {
216
+ clearTimeout(tickTimer);
217
+ tickTimer = null;
218
+ }
219
+ if (tickImmediate !== null) {
220
+ clearImmediate(tickImmediate);
221
+ tickImmediate = null;
222
+ }
223
+ sabWakeEpoch++;
224
+ sabWakeArmed = false;
225
+ }
226
+ function scheduleTickNow() {
227
+ if (!running)
228
+ return;
229
+ if (tickImmediate !== null)
230
+ return;
231
+ if (tickTimer !== null) {
232
+ clearTimeout(tickTimer);
233
+ tickTimer = null;
234
+ }
235
+ tickImmediate = setImmediate(() => {
236
+ tickImmediate = null;
237
+ tick();
238
+ });
239
+ }
240
+ function scheduleTick(delayMs) {
241
+ if (!running)
242
+ return;
243
+ const d = Math.max(0, delayMs);
244
+ // Avoid the `setTimeout(0)` ~1ms clamp: immediate work should be scheduled via
245
+ // `setImmediate` to keep input→frame latency tight.
246
+ if (d <= 0) {
247
+ scheduleTickNow();
248
+ return;
249
+ }
250
+ if (tickImmediate !== null || tickTimer !== null)
251
+ return;
252
+ tickTimer = setTimeout(() => {
253
+ tickTimer = null;
254
+ tick();
255
+ }, d);
256
+ }
257
+ function armSabFrameWake() {
258
+ if (!running)
259
+ return;
260
+ if (frameTransport.kind !== FRAME_TRANSPORT_SAB_V1)
261
+ return;
262
+ if (sabWakeArmed)
263
+ return;
264
+ const h = frameTransport.controlHeader;
265
+ const seq = Atomics.load(h, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD);
266
+ if (seq > lastConsumedSabPublishedSeq) {
267
+ scheduleTickNow();
268
+ return;
269
+ }
270
+ const epoch = sabWakeEpoch;
271
+ const timeoutMs = Math.max(1, tickIntervalMs);
272
+ const waitAsync = Atomics.waitAsync;
273
+ if (typeof waitAsync !== "function")
274
+ return;
275
+ const waiter = waitAsync(h, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, seq, timeoutMs);
276
+ if (typeof waiter !== "object" || waiter === null)
277
+ return;
278
+ if (waiter.async !== true) {
279
+ if (!running)
280
+ return;
281
+ if (epoch !== sabWakeEpoch)
282
+ return;
283
+ scheduleTickNow();
284
+ return;
285
+ }
286
+ sabWakeArmed = true;
287
+ const waiterPromise = waiter.value;
288
+ void waiterPromise.then(() => {
289
+ if (epoch !== sabWakeEpoch)
290
+ return;
291
+ sabWakeArmed = false;
292
+ if (!running)
293
+ return;
294
+ scheduleTickNow();
295
+ }, () => {
296
+ if (epoch !== sabWakeEpoch)
297
+ return;
298
+ sabWakeArmed = false;
299
+ });
300
+ }
301
+ function startTickLoop(fpsCap) {
302
+ stopTickLoop();
303
+ // We poll events (including input) on the worker tick. At low FPS caps (e.g.
304
+ // 60fps → ~16ms), this can inflate input-to-event latency into the multi-ms
305
+ // range under load. Cap the poll interval to keep interactive latency tight.
306
+ tickIntervalMs = Math.min(MAX_EVENT_POLL_INTERVAL_MS, Math.max(1, Math.floor(1000 / fpsCap)));
307
+ // Keep input-to-first-poll latency bounded even after long idle periods.
308
+ // For high fps caps (e.g. bench at 1000), we allow a small idle backoff
309
+ // without letting latency drift into tens of milliseconds.
310
+ maxIdleDelayMs = Math.max(tickIntervalMs, MAX_IDLE_BACKOFF_MS);
311
+ idleDelayMs = tickIntervalMs;
312
+ scheduleTickNow();
313
+ armSabFrameWake();
314
+ }
315
+ function fatal(where, code, detail) {
316
+ postToMain({ type: "fatal", where, code, detail });
317
+ }
318
+ function shutdownComplete() {
319
+ postToMain({ type: "shutdownComplete" });
320
+ }
321
+ function releaseSabSlot(slotIndex, slotToken, expectedState) {
322
+ if (frameTransport.kind !== FRAME_TRANSPORT_SAB_V1)
323
+ return;
324
+ if (slotIndex < 0 || slotIndex >= frameTransport.slotCount)
325
+ return;
326
+ if (Atomics.load(frameTransport.tokens, slotIndex) !== slotToken)
327
+ return;
328
+ Atomics.compareExchange(frameTransport.states, slotIndex, expectedState, FRAME_SAB_SLOT_STATE_FREE);
329
+ }
330
+ function releasePendingFrame(frame, expectedSabState) {
331
+ if (frame.transport === FRAME_TRANSPORT_TRANSFER_V1)
332
+ return;
333
+ releaseSabSlot(frame.slotIndex, frame.slotToken, expectedSabState);
334
+ }
335
+ function postFrameStatus(frameSeq, completedResult) {
336
+ if (!Number.isInteger(frameSeq) || frameSeq <= 0)
337
+ return;
338
+ postToMain({
339
+ type: "frameStatus",
340
+ acceptedSeq: frameSeq,
341
+ completedSeq: frameSeq,
342
+ completedResult,
343
+ recycledDrawlists: NO_RECYCLED_DRAWLISTS,
344
+ });
345
+ }
346
+ function postFrameAccepted(frameSeq) {
347
+ if (!Number.isInteger(frameSeq) || frameSeq <= 0)
348
+ return;
349
+ postToMain({
350
+ type: "frameStatus",
351
+ acceptedSeq: frameSeq,
352
+ recycledDrawlists: NO_RECYCLED_DRAWLISTS,
353
+ });
354
+ }
355
+ function readLatestSabFrame() {
356
+ if (frameTransport.kind !== FRAME_TRANSPORT_SAB_V1)
357
+ return null;
358
+ const h = frameTransport.controlHeader;
359
+ let stableSeq = 0;
360
+ let slotIndex = -1;
361
+ let byteLen = -1;
362
+ let slotToken = 0;
363
+ for (let attempt = 0; attempt < 4; attempt++) {
364
+ const seqBefore = Atomics.load(h, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD);
365
+ if (seqBefore <= lastConsumedSabPublishedSeq)
366
+ return null;
367
+ slotIndex = Atomics.load(h, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD);
368
+ byteLen = Atomics.load(h, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD);
369
+ slotToken = Atomics.load(h, FRAME_SAB_CONTROL_PUBLISHED_TOKEN_WORD);
370
+ const seqAfter = Atomics.load(h, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD);
371
+ if (seqBefore === seqAfter) {
372
+ stableSeq = seqAfter;
373
+ break;
374
+ }
375
+ }
376
+ if (stableSeq <= lastConsumedSabPublishedSeq)
377
+ return null;
378
+ if (!Number.isInteger(slotIndex) || slotIndex < 0 || slotIndex >= frameTransport.slotCount) {
379
+ fatal("frame", -1, `invalid SAB publish slot index: ${String(slotIndex)}`);
380
+ running = false;
381
+ return null;
382
+ }
383
+ if (!Number.isInteger(byteLen) || byteLen < 0 || byteLen > frameTransport.slotBytes) {
384
+ fatal("frame", -1, `invalid SAB publish byteLen: ${String(byteLen)}`);
385
+ running = false;
386
+ return null;
387
+ }
388
+ if (!Number.isInteger(slotToken) || slotToken <= 0) {
389
+ fatal("frame", -1, `invalid SAB publish slotToken: ${String(slotToken)}`);
390
+ running = false;
391
+ return null;
392
+ }
393
+ lastConsumedSabPublishedSeq = stableSeq;
394
+ return {
395
+ frameSeq: stableSeq,
396
+ transport: FRAME_TRANSPORT_SAB_V1,
397
+ slotIndex,
398
+ slotToken,
399
+ byteLen,
400
+ };
401
+ }
402
+ function syncPendingSabFrameFromMailbox() {
403
+ const latest = readLatestSabFrame();
404
+ if (latest === null)
405
+ return;
406
+ if (pendingFrame !== null) {
407
+ releasePendingFrame(pendingFrame, FRAME_SAB_SLOT_STATE_READY);
408
+ }
409
+ pendingFrame = latest;
410
+ }
411
+ function destroyEngineBestEffort() {
412
+ const id = engineId;
413
+ engineId = null;
414
+ if (id === null)
415
+ return;
416
+ try {
417
+ native.engineDestroy(id);
418
+ }
419
+ catch (err) {
420
+ fatal("engineDestroy", -1, `engine_destroy threw: ${safeDetail(err)}`);
421
+ }
422
+ }
423
+ function shutdownNow() {
424
+ running = false;
425
+ stopTickLoop();
426
+ if (pendingFrame !== null) {
427
+ releasePendingFrame(pendingFrame, FRAME_SAB_SLOT_STATE_READY);
428
+ pendingFrame = null;
429
+ }
430
+ destroyEngineBestEffort();
431
+ shutdownComplete();
432
+ // Let worker thread exit naturally once handles are cleared.
433
+ if (parentPort !== null)
434
+ parentPort.close();
435
+ }
436
+ function tick() {
437
+ if (!running)
438
+ return;
439
+ if (engineId === null)
440
+ return;
441
+ if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1) {
442
+ syncPendingSabFrameFromMailbox();
443
+ }
444
+ let didSubmitDrawlistThisTick = false;
445
+ let didFrameWork = false;
446
+ let didEventWork = false;
447
+ let submittedFrameSeq = null;
448
+ // 1) submit latest drawlist (if any)
449
+ if (pendingFrame !== null) {
450
+ const f = pendingFrame;
451
+ pendingFrame = null;
452
+ let res = -1;
453
+ let sabInUse = false;
454
+ let staleSabFrame = false;
455
+ try {
456
+ if (f.transport === FRAME_TRANSPORT_TRANSFER_V1) {
457
+ res = native.engineSubmitDrawlist(engineId, new Uint8Array(f.buf, 0, f.byteLen));
458
+ }
459
+ else {
460
+ if (frameTransport.kind !== FRAME_TRANSPORT_SAB_V1) {
461
+ throw new Error("SAB frame transport unavailable");
462
+ }
463
+ if (f.slotIndex < 0 || f.slotIndex >= frameTransport.slotCount) {
464
+ throw new Error(`invalid SAB frame slot index: ${String(f.slotIndex)}`);
465
+ }
466
+ const token = Atomics.load(frameTransport.tokens, f.slotIndex);
467
+ if (token !== f.slotToken) {
468
+ staleSabFrame = true;
469
+ }
470
+ else {
471
+ const prev = Atomics.compareExchange(frameTransport.states, f.slotIndex, FRAME_SAB_SLOT_STATE_READY, FRAME_SAB_SLOT_STATE_IN_USE);
472
+ if (prev !== FRAME_SAB_SLOT_STATE_READY) {
473
+ const tokenAfter = Atomics.load(frameTransport.tokens, f.slotIndex);
474
+ if (tokenAfter !== f.slotToken) {
475
+ staleSabFrame = true;
476
+ }
477
+ else {
478
+ throw new Error(`SAB frame slot ${String(f.slotIndex)} not ready (state=${String(prev)})`);
479
+ }
480
+ }
481
+ else {
482
+ sabInUse = true;
483
+ const offset = f.slotIndex * frameTransport.slotBytes;
484
+ const view = frameTransport.data.subarray(offset, offset + f.byteLen);
485
+ res = native.engineSubmitDrawlist(engineId, view);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ catch (err) {
491
+ releasePendingFrame(f, sabInUse ? FRAME_SAB_SLOT_STATE_IN_USE : FRAME_SAB_SLOT_STATE_READY);
492
+ postFrameStatus(f.frameSeq, -1);
493
+ fatal("engineSubmitDrawlist", -1, `engine_submit_drawlist threw: ${safeDetail(err)}`);
494
+ running = false;
495
+ return;
496
+ }
497
+ if (staleSabFrame) {
498
+ // This frame was superseded in the shared mailbox before submit.
499
+ // Keep latest-wins behavior without surfacing a fatal protocol error.
500
+ didFrameWork = true;
501
+ syncPendingSabFrameFromMailbox();
502
+ // Continue with present/event processing on this tick.
503
+ }
504
+ else {
505
+ didSubmitDrawlistThisTick = res === 0;
506
+ haveSubmittedDrawlist = haveSubmittedDrawlist || didSubmitDrawlistThisTick;
507
+ didFrameWork = true;
508
+ releasePendingFrame(f, FRAME_SAB_SLOT_STATE_IN_USE);
509
+ if (res < 0) {
510
+ postFrameStatus(f.frameSeq, res);
511
+ fatal("engineSubmitDrawlist", res, "engine_submit_drawlist failed");
512
+ running = false;
513
+ return;
514
+ }
515
+ // Frame accepted by worker transport+submit path.
516
+ postFrameAccepted(f.frameSeq);
517
+ submittedFrameSeq = f.frameSeq;
518
+ }
519
+ }
520
+ // 2) present (only when a new drawlist was submitted)
521
+ //
522
+ // Why: Presenting every tick can cause constant output even when the UI is idle
523
+ // (e.g. sync-update begin/end sequences), which can manifest as flicker in some
524
+ // terminals. Present should be driven by actual drawlist updates.
525
+ if (haveSubmittedDrawlist && didSubmitDrawlistThisTick) {
526
+ let pres = -1;
527
+ try {
528
+ pres = native.enginePresent(engineId);
529
+ }
530
+ catch (err) {
531
+ if (submittedFrameSeq !== null)
532
+ postFrameStatus(submittedFrameSeq, -1);
533
+ fatal("enginePresent", -1, `engine_present threw: ${safeDetail(err)}`);
534
+ running = false;
535
+ return;
536
+ }
537
+ if (pres < 0) {
538
+ if (submittedFrameSeq !== null)
539
+ postFrameStatus(submittedFrameSeq, pres);
540
+ fatal("enginePresent", pres, "engine_present failed");
541
+ running = false;
542
+ return;
543
+ }
544
+ }
545
+ if (submittedFrameSeq !== null) {
546
+ postFrameStatus(submittedFrameSeq, 0);
547
+ }
548
+ // 3) drain events (bounded)
549
+ const discard = discardBuffer;
550
+ if (discard === null) {
551
+ if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1)
552
+ armSabFrameWake();
553
+ return;
554
+ }
555
+ for (let i = 0; i < MAX_POLL_DRAIN_ITERS; i++) {
556
+ const outBuf = eventPool.length > 0 ? (eventPool.pop() ?? discard) : discard;
557
+ let written = -1;
558
+ const pollStart = PERF_ENABLED ? performance.now() : 0;
559
+ try {
560
+ written = native.enginePollEvents(engineId, 0, new Uint8Array(outBuf));
561
+ }
562
+ catch (err) {
563
+ fatal("enginePollEvents", -1, `engine_poll_events threw: ${safeDetail(err)}`);
564
+ running = false;
565
+ return;
566
+ }
567
+ if (PERF_ENABLED) {
568
+ perfRecord("event_poll", performance.now() - pollStart);
569
+ }
570
+ if (written < 0) {
571
+ if (outBuf !== discard)
572
+ eventPool.push(outBuf);
573
+ fatal("enginePollEvents", written, "engine_poll_events failed");
574
+ running = false;
575
+ return;
576
+ }
577
+ if (written === 0) {
578
+ if (outBuf !== discard)
579
+ eventPool.push(outBuf);
580
+ break;
581
+ }
582
+ if (outBuf === discard) {
583
+ droppedSinceLast++;
584
+ didEventWork = true;
585
+ continue;
586
+ }
587
+ postToMain({ type: "events", batch: outBuf, byteLen: written, droppedSinceLast }, [outBuf]);
588
+ droppedSinceLast = 0;
589
+ didEventWork = true;
590
+ }
591
+ if (!running)
592
+ return;
593
+ if (didFrameWork) {
594
+ idleDelayMs = tickIntervalMs;
595
+ // Keep a hot pump while frames are flowing to reduce submit jitter.
596
+ scheduleTickNow();
597
+ if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1)
598
+ armSabFrameWake();
599
+ return;
600
+ }
601
+ if (didEventWork) {
602
+ idleDelayMs = tickIntervalMs;
603
+ // Event-only traffic (notably engine ticks) should not force an immediate
604
+ // hot loop; staying timer-paced avoids main-thread contention spikes.
605
+ scheduleTick(tickIntervalMs);
606
+ if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1)
607
+ armSabFrameWake();
608
+ return;
609
+ }
610
+ idleDelayMs = Math.min(maxIdleDelayMs, Math.max(tickIntervalMs, idleDelayMs > 0 ? idleDelayMs * 2 : tickIntervalMs));
611
+ scheduleTick(idleDelayMs);
612
+ if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1)
613
+ armSabFrameWake();
614
+ }
615
+ function onMessage(msg) {
616
+ switch (msg.type) {
617
+ case "init": {
618
+ if (engineId !== null)
619
+ return;
620
+ const maxEventBytes = parsePositiveInt(msg.config.maxEventBytes);
621
+ if (maxEventBytes === null) {
622
+ fatal("init", -1, "config.maxEventBytes must be a positive integer");
623
+ shutdownNow();
624
+ return;
625
+ }
626
+ frameTransport = parseFrameTransportConfig(msg.config.frameTransport);
627
+ let id = 0;
628
+ try {
629
+ // Worker protocol includes Node-only keys (maxEventBytes, fpsCap). Strip
630
+ // those before handing config to the native addon so the addon can
631
+ // validate unknown keys strictly.
632
+ const { maxEventBytes: _maxEventBytes, fpsCap: _fpsCap, frameTransport: _frameTransport, ...nativeCfg } = msg.config;
633
+ id = native.engineCreate(nativeCfg);
634
+ }
635
+ catch (err) {
636
+ fatal("engineCreate", -1, `engine_create threw: ${safeDetail(err)}`);
637
+ shutdownNow();
638
+ return;
639
+ }
640
+ if (!Number.isInteger(id) || id <= 0) {
641
+ fatal("engineCreate", id, "engine_create failed");
642
+ shutdownNow();
643
+ return;
644
+ }
645
+ engineId = id;
646
+ haveSubmittedDrawlist = false;
647
+ running = true;
648
+ pendingFrame = null;
649
+ lastConsumedSabPublishedSeq = 0;
650
+ if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1) {
651
+ Atomics.store(frameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 0);
652
+ Atomics.store(frameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, 0);
653
+ Atomics.store(frameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, 0);
654
+ Atomics.store(frameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_TOKEN_WORD, 0);
655
+ Atomics.store(frameTransport.controlHeader, FRAME_SAB_CONTROL_CONSUMED_SEQ_WORD, 0);
656
+ for (let i = 0; i < frameTransport.slotCount; i++) {
657
+ Atomics.store(frameTransport.states, i, FRAME_SAB_SLOT_STATE_FREE);
658
+ Atomics.store(frameTransport.tokens, i, 0);
659
+ }
660
+ }
661
+ eventPool = [];
662
+ for (let i = 0; i < EVENT_POOL_SIZE; i++)
663
+ eventPool.push(new ArrayBuffer(maxEventBytes));
664
+ discardBuffer = new ArrayBuffer(maxEventBytes);
665
+ droppedSinceLast = 0;
666
+ // Some terminals (notably Rio/WSL) may not deliver an initial resize
667
+ // immediately. Emit a best-effort initial viewport so widget mode can
668
+ // render on first frame.
669
+ //
670
+ // Skip this when a test shim is injected, to keep worker tests fully
671
+ // deterministic (and avoid consuming buffers without ACKs).
672
+ const wd = workerData && typeof workerData === "object"
673
+ ? workerData
674
+ : Object.freeze({});
675
+ const shim = typeof wd.nativeShimModule === "string" ? wd.nativeShimModule : null;
676
+ if (shim === null || shim.length === 0) {
677
+ maybeInjectInitialResize(maxEventBytes);
678
+ }
679
+ postToMain({ type: "ready", engineId: id });
680
+ const fpsCap = parsePositiveInt(msg.config.fpsCap) ?? 60;
681
+ startTickLoop(fpsCap);
682
+ return;
683
+ }
684
+ case "frame": {
685
+ if (engineId === null)
686
+ return;
687
+ // latest-wins overwrite for transfer-path fallback.
688
+ if (pendingFrame !== null) {
689
+ releasePendingFrame(pendingFrame, FRAME_SAB_SLOT_STATE_READY);
690
+ }
691
+ const frameTransportTag = msg.transport ?? FRAME_TRANSPORT_TRANSFER_V1;
692
+ if (frameTransportTag === FRAME_TRANSPORT_SAB_V1) {
693
+ if (frameTransport.kind !== FRAME_TRANSPORT_SAB_V1) {
694
+ fatal("frame", -1, "received SAB frame while SAB transport is disabled");
695
+ running = false;
696
+ return;
697
+ }
698
+ if (!Number.isInteger(msg.slotIndex) ||
699
+ msg.slotIndex < 0 ||
700
+ msg.slotIndex >= frameTransport.slotCount) {
701
+ fatal("frame", -1, `invalid SAB frame slot: ${String(msg.slotIndex)}`);
702
+ running = false;
703
+ return;
704
+ }
705
+ if (!Number.isInteger(msg.byteLen) ||
706
+ msg.byteLen < 0 ||
707
+ msg.byteLen > frameTransport.slotBytes) {
708
+ fatal("frame", -1, `invalid SAB frame byteLen: ${String(msg.byteLen)}`);
709
+ running = false;
710
+ return;
711
+ }
712
+ if (!Number.isInteger(msg.slotToken) || msg.slotToken <= 0) {
713
+ fatal("frame", -1, `invalid SAB frame slotToken: ${String(msg.slotToken)}`);
714
+ running = false;
715
+ return;
716
+ }
717
+ pendingFrame = {
718
+ frameSeq: msg.frameSeq,
719
+ transport: FRAME_TRANSPORT_SAB_V1,
720
+ slotIndex: msg.slotIndex,
721
+ slotToken: msg.slotToken,
722
+ byteLen: msg.byteLen,
723
+ };
724
+ }
725
+ else {
726
+ if (!(msg.drawlist instanceof ArrayBuffer)) {
727
+ fatal("frame", -1, "invalid transfer frame payload: missing drawlist");
728
+ running = false;
729
+ return;
730
+ }
731
+ if (!Number.isInteger(msg.byteLen) ||
732
+ msg.byteLen < 0 ||
733
+ msg.byteLen > msg.drawlist.byteLength) {
734
+ fatal("frame", -1, `invalid transfer frame byteLen: ${String(msg.byteLen)}`);
735
+ running = false;
736
+ return;
737
+ }
738
+ pendingFrame = {
739
+ frameSeq: msg.frameSeq,
740
+ transport: FRAME_TRANSPORT_TRANSFER_V1,
741
+ buf: msg.drawlist,
742
+ byteLen: msg.byteLen,
743
+ };
744
+ }
745
+ idleDelayMs = tickIntervalMs;
746
+ scheduleTickNow();
747
+ return;
748
+ }
749
+ case "frameKick": {
750
+ if (engineId === null)
751
+ return;
752
+ if (frameTransport.kind === FRAME_TRANSPORT_SAB_V1) {
753
+ syncPendingSabFrameFromMailbox();
754
+ }
755
+ idleDelayMs = tickIntervalMs;
756
+ scheduleTickNow();
757
+ return;
758
+ }
759
+ case "setConfig": {
760
+ if (engineId === null)
761
+ return;
762
+ let rc = -1;
763
+ try {
764
+ rc = native.engineSetConfig(engineId, msg.config);
765
+ }
766
+ catch (err) {
767
+ fatal("engineSetConfig", -1, `engine_set_config threw: ${safeDetail(err)}`);
768
+ running = false;
769
+ return;
770
+ }
771
+ if (rc < 0) {
772
+ fatal("engineSetConfig", rc, "engine_set_config failed");
773
+ running = false;
774
+ }
775
+ return;
776
+ }
777
+ case "postUserEvent": {
778
+ if (engineId === null)
779
+ return;
780
+ let rc = -1;
781
+ try {
782
+ rc = native.enginePostUserEvent(engineId, msg.tag, new Uint8Array(msg.payload, 0, msg.byteLen));
783
+ }
784
+ catch (err) {
785
+ fatal("enginePostUserEvent", -1, `engine_post_user_event threw: ${safeDetail(err)}`);
786
+ running = false;
787
+ return;
788
+ }
789
+ if (rc < 0) {
790
+ fatal("enginePostUserEvent", rc, "engine_post_user_event failed");
791
+ running = false;
792
+ }
793
+ return;
794
+ }
795
+ case "eventsAck": {
796
+ if (discardBuffer === null)
797
+ return;
798
+ eventPool.push(msg.buffer);
799
+ return;
800
+ }
801
+ case "getCaps": {
802
+ if (engineId === null)
803
+ return;
804
+ try {
805
+ const caps = native.engineGetCaps(engineId);
806
+ postToMain({
807
+ type: "caps",
808
+ colorMode: caps.colorMode,
809
+ supportsMouse: caps.supportsMouse,
810
+ supportsBracketedPaste: caps.supportsBracketedPaste,
811
+ supportsFocusEvents: caps.supportsFocusEvents,
812
+ supportsOsc52: caps.supportsOsc52,
813
+ supportsSyncUpdate: caps.supportsSyncUpdate,
814
+ supportsScrollRegion: caps.supportsScrollRegion,
815
+ supportsCursorShape: caps.supportsCursorShape,
816
+ supportsOutputWaitWritable: caps.supportsOutputWaitWritable,
817
+ sgrAttrsSupported: caps.sgrAttrsSupported,
818
+ });
819
+ }
820
+ catch (err) {
821
+ fatal("engineGetCaps", -1, `engine_get_caps threw: ${safeDetail(err)}`);
822
+ }
823
+ return;
824
+ }
825
+ case "shutdown": {
826
+ shutdownNow();
827
+ return;
828
+ }
829
+ // Debug API handlers
830
+ case "debug:enable": {
831
+ if (engineId === null)
832
+ return;
833
+ let rc = -1;
834
+ try {
835
+ const nativeConfig = {
836
+ enabled: msg.config.enabled ?? true,
837
+ ringCapacity: msg.config.ringCapacity ?? 0,
838
+ minSeverity: msg.config.minSeverity ?? 0,
839
+ categoryMask: msg.config.categoryMask ?? 0xffffffff,
840
+ captureRawEvents: msg.config.captureRawEvents ?? false,
841
+ captureDrawlistBytes: msg.config.captureDrawlistBytes ?? false,
842
+ };
843
+ rc = native.engineDebugEnable(engineId, nativeConfig);
844
+ }
845
+ catch (err) {
846
+ fatal("engineDebugEnable", -1, `engine_debug_enable threw: ${safeDetail(err)}`);
847
+ return;
848
+ }
849
+ postToMain({ type: "debug:enableResult", result: rc });
850
+ return;
851
+ }
852
+ case "debug:disable": {
853
+ if (engineId === null)
854
+ return;
855
+ let rc = -1;
856
+ try {
857
+ rc = native.engineDebugDisable(engineId);
858
+ }
859
+ catch (err) {
860
+ fatal("engineDebugDisable", -1, `engine_debug_disable threw: ${safeDetail(err)}`);
861
+ return;
862
+ }
863
+ postToMain({ type: "debug:disableResult", result: rc });
864
+ return;
865
+ }
866
+ case "debug:query": {
867
+ if (engineId === null)
868
+ return;
869
+ try {
870
+ const nativeQuery = {
871
+ minRecordId: msg.query.minRecordId ? BigInt(msg.query.minRecordId) : undefined,
872
+ maxRecordId: msg.query.maxRecordId ? BigInt(msg.query.maxRecordId) : undefined,
873
+ minFrameId: msg.query.minFrameId ? BigInt(msg.query.minFrameId) : undefined,
874
+ maxFrameId: msg.query.maxFrameId ? BigInt(msg.query.maxFrameId) : undefined,
875
+ categoryMask: msg.query.categoryMask,
876
+ minSeverity: msg.query.minSeverity,
877
+ maxRecords: msg.query.maxRecords,
878
+ };
879
+ const requestedHeadersCap = parsePositiveInt(msg.headersCap) ?? 0;
880
+ const headersCap = Math.min(DEBUG_QUERY_MAX_HEADERS_CAP, Math.max(DEBUG_QUERY_MIN_HEADERS_CAP, requestedHeadersCap));
881
+ const headersBuf = new ArrayBuffer(headersCap);
882
+ const headersArr = new Uint8Array(headersBuf);
883
+ const result = native.engineDebugQuery(engineId, nativeQuery, headersArr);
884
+ const maxHeaders = Math.floor(headersCap / DEBUG_HEADER_BYTES);
885
+ const returnedHeaders = Number.isInteger(result.recordsReturned) && result.recordsReturned > 0
886
+ ? Math.min(result.recordsReturned, maxHeaders)
887
+ : 0;
888
+ const headersByteLen = returnedHeaders * DEBUG_HEADER_BYTES;
889
+ postToMain({
890
+ type: "debug:queryResult",
891
+ headers: headersBuf,
892
+ headersByteLen,
893
+ result: {
894
+ recordsReturned: returnedHeaders,
895
+ recordsAvailable: result.recordsAvailable,
896
+ oldestRecordId: String(result.oldestRecordId),
897
+ newestRecordId: String(result.newestRecordId),
898
+ recordsDropped: result.recordsDropped,
899
+ },
900
+ }, [headersBuf]);
901
+ }
902
+ catch (err) {
903
+ fatal("engineDebugQuery", -1, `engine_debug_query threw: ${safeDetail(err)}`);
904
+ }
905
+ return;
906
+ }
907
+ case "debug:getPayload": {
908
+ if (engineId === null)
909
+ return;
910
+ try {
911
+ const payloadBuf = new ArrayBuffer(msg.payloadCap);
912
+ const payloadArr = new Uint8Array(payloadBuf);
913
+ const recordId = BigInt(msg.recordId);
914
+ const bytesWritten = native.engineDebugGetPayload(engineId, recordId, payloadArr);
915
+ const payloadByteLen = bytesWritten > 0 ? Math.min(bytesWritten, msg.payloadCap) : 0;
916
+ postToMain({
917
+ type: "debug:getPayloadResult",
918
+ payload: payloadBuf,
919
+ payloadByteLen,
920
+ result: bytesWritten,
921
+ }, [payloadBuf]);
922
+ }
923
+ catch (err) {
924
+ fatal("engineDebugGetPayload", -1, `engine_debug_get_payload threw: ${safeDetail(err)}`);
925
+ }
926
+ return;
927
+ }
928
+ case "debug:getStats": {
929
+ if (engineId === null)
930
+ return;
931
+ try {
932
+ const stats = native.engineDebugGetStats(engineId);
933
+ postToMain({
934
+ type: "debug:getStatsResult",
935
+ stats: {
936
+ totalRecords: String(stats.totalRecords),
937
+ totalDropped: String(stats.totalDropped),
938
+ errorCount: stats.errorCount,
939
+ warnCount: stats.warnCount,
940
+ currentRingUsage: stats.currentRingUsage,
941
+ ringCapacity: stats.ringCapacity,
942
+ },
943
+ });
944
+ }
945
+ catch (err) {
946
+ fatal("engineDebugGetStats", -1, `engine_debug_get_stats threw: ${safeDetail(err)}`);
947
+ }
948
+ return;
949
+ }
950
+ case "debug:export": {
951
+ if (engineId === null)
952
+ return;
953
+ try {
954
+ const exportBuf = new ArrayBuffer(msg.bufferCap);
955
+ const exportArr = new Uint8Array(exportBuf);
956
+ const bytesWritten = native.engineDebugExport(engineId, exportArr);
957
+ const bufferByteLen = bytesWritten > 0 ? Math.min(bytesWritten, msg.bufferCap) : 0;
958
+ postToMain({
959
+ type: "debug:exportResult",
960
+ buffer: exportBuf,
961
+ bufferByteLen,
962
+ }, [exportBuf]);
963
+ }
964
+ catch (err) {
965
+ fatal("engineDebugExport", -1, `engine_debug_export threw: ${safeDetail(err)}`);
966
+ }
967
+ return;
968
+ }
969
+ case "debug:reset": {
970
+ if (engineId === null)
971
+ return;
972
+ let rc = -1;
973
+ try {
974
+ rc = native.engineDebugReset(engineId);
975
+ }
976
+ catch (err) {
977
+ fatal("engineDebugReset", -1, `engine_debug_reset threw: ${safeDetail(err)}`);
978
+ return;
979
+ }
980
+ postToMain({ type: "debug:resetResult", result: rc });
981
+ return;
982
+ }
983
+ case "perf:snapshot": {
984
+ const snapshot = perfSnapshot();
985
+ postToMain({ type: "perf:snapshotResult", snapshot });
986
+ return;
987
+ }
988
+ }
989
+ }
990
+ if (parentPort === null) {
991
+ throw new Error("engineWorker: parentPort is null (not running in worker_threads)");
992
+ }
993
+ parentPort.on("message", (m) => {
994
+ if (typeof m !== "object" || m === null)
995
+ return;
996
+ const type = m.type;
997
+ if (typeof type !== "string")
998
+ return;
999
+ onMessage(m);
1000
+ });
1001
+ //# sourceMappingURL=engineWorker.js.map