@rezi-ui/node 0.1.0-alpha.6 → 0.1.0-alpha.60

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 (93) hide show
  1. package/README.md +26 -6
  2. package/dist/backend/backendSharedConfig.d.ts +11 -0
  3. package/dist/backend/backendSharedConfig.d.ts.map +1 -0
  4. package/dist/backend/backendSharedConfig.js +99 -0
  5. package/dist/backend/backendSharedConfig.js.map +1 -0
  6. package/dist/backend/backendSharedDebug.d.ts +4 -0
  7. package/dist/backend/backendSharedDebug.d.ts.map +1 -0
  8. package/dist/backend/backendSharedDebug.js +29 -0
  9. package/dist/backend/backendSharedDebug.js.map +1 -0
  10. package/dist/backend/backendSharedMarkers.d.ts +10 -0
  11. package/dist/backend/backendSharedMarkers.d.ts.map +1 -0
  12. package/dist/backend/backendSharedMarkers.js +50 -0
  13. package/dist/backend/backendSharedMarkers.js.map +1 -0
  14. package/dist/backend/emojiWidthPolicy.d.ts +15 -0
  15. package/dist/backend/emojiWidthPolicy.d.ts.map +1 -0
  16. package/dist/backend/emojiWidthPolicy.js +229 -0
  17. package/dist/backend/emojiWidthPolicy.js.map +1 -0
  18. package/dist/backend/nodeBackend.d.ts +30 -2
  19. package/dist/backend/nodeBackend.d.ts.map +1 -1
  20. package/dist/backend/nodeBackend.js +418 -66
  21. package/dist/backend/nodeBackend.js.map +1 -1
  22. package/dist/backend/nodeBackendInline.d.ts.map +1 -1
  23. package/dist/backend/nodeBackendInline.js +131 -55
  24. package/dist/backend/nodeBackendInline.js.map +1 -1
  25. package/dist/backend/terminalProfile.d.ts +5 -0
  26. package/dist/backend/terminalProfile.d.ts.map +1 -0
  27. package/dist/backend/terminalProfile.js +117 -0
  28. package/dist/backend/terminalProfile.js.map +1 -0
  29. package/dist/dev/hotStateReload.d.ts +65 -0
  30. package/dist/dev/hotStateReload.d.ts.map +1 -0
  31. package/dist/dev/hotStateReload.js +438 -0
  32. package/dist/dev/hotStateReload.js.map +1 -0
  33. package/dist/dev/nodeAppHotReload.d.ts +11 -0
  34. package/dist/dev/nodeAppHotReload.d.ts.map +1 -0
  35. package/dist/dev/nodeAppHotReload.js +78 -0
  36. package/dist/dev/nodeAppHotReload.js.map +1 -0
  37. package/dist/frameAudit.d.ts +51 -0
  38. package/dist/frameAudit.d.ts.map +1 -0
  39. package/dist/frameAudit.js +257 -0
  40. package/dist/frameAudit.js.map +1 -0
  41. package/dist/image.d.ts +4 -0
  42. package/dist/image.d.ts.map +1 -0
  43. package/dist/image.js +43 -0
  44. package/dist/image.js.map +1 -0
  45. package/dist/index.d.ts +44 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +198 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/repro/index.d.ts +3 -0
  50. package/dist/repro/index.d.ts.map +1 -0
  51. package/dist/repro/index.js +2 -0
  52. package/dist/repro/index.js.map +1 -0
  53. package/dist/repro/recorder.d.ts +30 -0
  54. package/dist/repro/recorder.d.ts.map +1 -0
  55. package/dist/repro/recorder.js +321 -0
  56. package/dist/repro/recorder.js.map +1 -0
  57. package/dist/streams/tail.d.ts +6 -0
  58. package/dist/streams/tail.d.ts.map +1 -0
  59. package/dist/streams/tail.js +113 -0
  60. package/dist/streams/tail.js.map +1 -0
  61. package/dist/worker/engineWorker.js +487 -14
  62. package/dist/worker/engineWorker.js.map +1 -1
  63. package/dist/worker/protocol.d.ts +3 -0
  64. package/dist/worker/protocol.d.ts.map +1 -1
  65. package/dist/worker/testShims/invalidPollBytesNative.d.ts +22 -0
  66. package/dist/worker/testShims/invalidPollBytesNative.d.ts.map +1 -0
  67. package/dist/worker/testShims/invalidPollBytesNative.js +65 -0
  68. package/dist/worker/testShims/invalidPollBytesNative.js.map +1 -0
  69. package/dist/worker/testShims/limitsExpectNative.d.ts +22 -0
  70. package/dist/worker/testShims/limitsExpectNative.d.ts.map +1 -0
  71. package/dist/worker/testShims/limitsExpectNative.js +85 -0
  72. package/dist/worker/testShims/limitsExpectNative.js.map +1 -0
  73. package/dist/worker/testShims/limitsNative.d.ts +22 -0
  74. package/dist/worker/testShims/limitsNative.d.ts.map +1 -0
  75. package/dist/worker/testShims/limitsNative.js +90 -0
  76. package/dist/worker/testShims/limitsNative.js.map +1 -0
  77. package/dist/worker/tickTiming.d.ts +7 -0
  78. package/dist/worker/tickTiming.d.ts.map +1 -0
  79. package/dist/worker/tickTiming.js +26 -0
  80. package/dist/worker/tickTiming.js.map +1 -0
  81. package/package.json +12 -8
  82. package/dist/__e2e__/fixtures/terminal-app.d.ts +0 -2
  83. package/dist/__e2e__/fixtures/terminal-app.d.ts.map +0 -1
  84. package/dist/__e2e__/fixtures/terminal-app.js +0 -42
  85. package/dist/__e2e__/fixtures/terminal-app.js.map +0 -1
  86. package/dist/__e2e__/terminal-render.e2e.test.d.ts +0 -2
  87. package/dist/__e2e__/terminal-render.e2e.test.d.ts.map +0 -1
  88. package/dist/__e2e__/terminal-render.e2e.test.js +0 -125
  89. package/dist/__e2e__/terminal-render.e2e.test.js.map +0 -1
  90. package/dist/__tests__/worker_integration.test.d.ts +0 -2
  91. package/dist/__tests__/worker_integration.test.d.ts.map +0 -1
  92. package/dist/__tests__/worker_integration.test.js +0 -569
  93. package/dist/__tests__/worker_integration.test.js.map +0 -1
@@ -6,11 +6,19 @@
6
6
  * @see docs/dev/style-guide.md
7
7
  * @see docs/backend/native.md
8
8
  */
9
+ import { existsSync } from "node:fs";
9
10
  import { Worker } from "node:worker_threads";
10
11
  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 { ZR_DRAWLIST_VERSION_V1, ZR_ENGINE_ABI_MAJOR, ZR_ENGINE_ABI_MINOR, ZR_ENGINE_ABI_PATCH, ZR_EVENT_BATCH_VERSION_V1, ZrUiError, setTextMeasureEmojiPolicy, severityToNum, } from "@rezi-ui/core";
13
+ import { createFrameAuditLogger, drawlistFingerprint, maybeDumpDrawlistBytes, } from "../frameAudit.js";
12
14
  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";
15
+ import { DEFAULT_FPS_CAP, DEFAULT_MAX_EVENT_BYTES, MAX_SAFE_EVENT_BYTES, MAX_SAFE_FPS_CAP, normalizeBackendNativeConfig, parseBoundedPositiveIntOrThrow, parsePositiveInt, parsePositiveIntOr, resolveTargetFps, } from "./backendSharedConfig.js";
16
+ import { DEBUG_QUERY_DEFAULT_RECORDS, DEBUG_QUERY_MAX_RECORDS } from "./backendSharedDebug.js";
17
+ import { attachBackendMarkers } from "./backendSharedMarkers.js";
18
+ import { applyEmojiWidthPolicy, resolveBackendEmojiWidthPolicy } from "./emojiWidthPolicy.js";
13
19
  import { createNodeBackendInlineInternal } from "./nodeBackendInline.js";
20
+ import { terminalProfileFromNodeEnv } from "./terminalProfile.js";
21
+ const WIDTH_POLICY_KEY = "widthPolicy";
14
22
  function deferred() {
15
23
  let resolve;
16
24
  let reject;
@@ -20,37 +28,48 @@ function deferred() {
20
28
  });
21
29
  return { promise, resolve, reject };
22
30
  }
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;
31
+ function safeErr(err) {
32
+ return err instanceof Error ? err : new Error(String(err));
33
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;
34
+ function resolveWorkerEntry(workerData) {
35
+ const options = { workerData };
36
+ const workerEntryJs = new URL("../worker/engineWorker.js", import.meta.url);
37
+ if (existsSync(workerEntryJs)) {
38
+ return { entry: workerEntryJs, options };
39
+ }
40
+ // Source-mode worktrees do not emit sibling .js worker files under src.
41
+ // Use a JS bootstrap that registers tsx and then imports engineWorker.ts.
42
+ const workerEntryBootstrapJs = new URL("../worker/engineWorker.bootstrap.js", import.meta.url);
43
+ if (existsSync(workerEntryBootstrapJs)) {
44
+ return { entry: workerEntryBootstrapJs, options };
45
+ }
46
+ throw new ZrUiError("ZRUI_BACKEND_ERROR", "Unable to locate worker entry (expected engineWorker.js or engineWorker.bootstrap.js)");
44
47
  }
45
- function readNativeTargetFps(cfg) {
46
- const targetFpsCfg = cfg;
47
- return parsePositiveInt(targetFpsCfg.targetFps) ?? parsePositiveInt(targetFpsCfg.target_fps);
48
+ function hasInteractiveTty() {
49
+ return (process.stdin.isTTY === true || process.stdout.isTTY === true || process.stderr.isTTY === true);
48
50
  }
49
- function safeErr(err) {
50
- return err instanceof Error ? err : new Error(String(err));
51
+ export function selectNodeBackendExecutionMode(input) {
52
+ const { requestedExecutionMode, fpsCap } = input;
53
+ const resolvedExecutionMode = requestedExecutionMode === "inline"
54
+ ? "inline"
55
+ : requestedExecutionMode === "worker"
56
+ ? "worker"
57
+ : fpsCap <= 30
58
+ ? "inline"
59
+ : "worker";
60
+ return {
61
+ resolvedExecutionMode,
62
+ selectedExecutionMode: resolvedExecutionMode,
63
+ fallbackReason: null,
64
+ };
65
+ }
66
+ function assertWorkerEnvironmentSupported(nativeShimModule) {
67
+ if (nativeShimModule !== undefined)
68
+ return;
69
+ if (hasInteractiveTty())
70
+ return;
71
+ throw new ZrUiError("ZRUI_BACKEND_ERROR", 'Worker backend requires a TTY when using @rezi-ui/native. Use `executionMode: "inline"` for headless runs or pass `nativeShimModule` in test harnesses.');
51
72
  }
52
- const DEBUG_QUERY_DEFAULT_RECORDS = 4096;
53
- const DEBUG_QUERY_MAX_RECORDS = 16384;
54
73
  const FRAME_SAB_SLOT_COUNT_DEFAULT = 8;
55
74
  const FRAME_SAB_SLOT_BYTES_DEFAULT = 1 << 20;
56
75
  function copyInto(buf, bytes) {
@@ -124,6 +143,38 @@ function acquireSabSlot(t) {
124
143
  }
125
144
  return -1;
126
145
  }
146
+ function acquireSabSlotTracked(t) {
147
+ const start = t.nextSlot.value % t.slotCount;
148
+ for (let i = 0; i < t.slotCount; i++) {
149
+ const slot = (start + i) % t.slotCount;
150
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_FREE, FRAME_SAB_SLOT_STATE_WRITING);
151
+ if (prev === FRAME_SAB_SLOT_STATE_FREE) {
152
+ t.nextSlot.value = (slot + 1) % t.slotCount;
153
+ return { slotIndex: slot, reclaimedReady: false };
154
+ }
155
+ }
156
+ for (let i = 0; i < t.slotCount; i++) {
157
+ const slot = (start + i) % t.slotCount;
158
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_READY, FRAME_SAB_SLOT_STATE_WRITING);
159
+ if (prev === FRAME_SAB_SLOT_STATE_READY) {
160
+ t.nextSlot.value = (slot + 1) % t.slotCount;
161
+ return { slotIndex: slot, reclaimedReady: true };
162
+ }
163
+ }
164
+ return { slotIndex: -1, reclaimedReady: false };
165
+ }
166
+ function acquireSabFreeSlot(t) {
167
+ const start = t.nextSlot.value % t.slotCount;
168
+ for (let i = 0; i < t.slotCount; i++) {
169
+ const slot = (start + i) % t.slotCount;
170
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_FREE, FRAME_SAB_SLOT_STATE_WRITING);
171
+ if (prev === FRAME_SAB_SLOT_STATE_FREE) {
172
+ t.nextSlot.value = (slot + 1) % t.slotCount;
173
+ return slot;
174
+ }
175
+ }
176
+ return -1;
177
+ }
127
178
  function publishSabFrame(t, frameSeq, slotIndex, slotToken, byteLen) {
128
179
  Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, slotIndex);
129
180
  Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, byteLen);
@@ -131,21 +182,30 @@ function publishSabFrame(t, frameSeq, slotIndex, slotToken, byteLen) {
131
182
  Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, frameSeq);
132
183
  }
133
184
  export function createNodeBackendInternal(opts = {}) {
185
+ const frameAudit = createFrameAuditLogger("backend");
134
186
  const cfg = opts.config ?? {};
135
- const fpsCap = parsePositiveIntOr(cfg.fpsCap, 60);
187
+ const fpsCap = parseBoundedPositiveIntOrThrow("fpsCap", cfg.fpsCap, DEFAULT_FPS_CAP, MAX_SAFE_FPS_CAP);
136
188
  const requestedExecutionMode = cfg.executionMode ?? "auto";
137
- const executionMode = requestedExecutionMode === "inline"
138
- ? "inline"
139
- : requestedExecutionMode === "worker"
140
- ? "worker"
141
- : fpsCap <= 30
142
- ? "inline"
143
- : "worker";
189
+ const executionModeSelection = selectNodeBackendExecutionMode({
190
+ requestedExecutionMode,
191
+ fpsCap,
192
+ hasAnyTty: hasInteractiveTty(),
193
+ ...(opts.nativeShimModule === undefined ? {} : { nativeShimModule: opts.nativeShimModule }),
194
+ });
195
+ const executionMode = executionModeSelection.selectedExecutionMode;
196
+ if (executionModeSelection.fallbackReason !== null && frameAudit.enabled) {
197
+ frameAudit.emit("backend.executionModeFallback", {
198
+ requestedExecutionMode,
199
+ resolvedExecutionMode: executionModeSelection.resolvedExecutionMode,
200
+ selectedExecutionMode: executionMode,
201
+ reason: executionModeSelection.fallbackReason,
202
+ });
203
+ }
144
204
  if (executionMode === "inline") {
145
205
  return createNodeBackendInlineInternal(opts);
146
206
  }
147
- const maxEventBytes = parsePositiveIntOr(cfg.maxEventBytes, 1 << 20);
148
- const useDrawlistV2 = cfg.useDrawlistV2 === true;
207
+ const requestedDrawlistVersion = ZR_DRAWLIST_VERSION_V1;
208
+ const maxEventBytes = parseBoundedPositiveIntOrThrow("maxEventBytes", cfg.maxEventBytes, DEFAULT_MAX_EVENT_BYTES, MAX_SAFE_EVENT_BYTES);
149
209
  const frameTransportMode = cfg.frameTransport === "transfer" || cfg.frameTransport === "sab" ? cfg.frameTransport : "auto";
150
210
  const frameSabSlotCount = parsePositiveIntOr(cfg.frameSabSlotCount, FRAME_SAB_SLOT_COUNT_DEFAULT);
151
211
  const frameSabSlotBytes = parsePositiveIntOr(cfg.frameSabSlotBytes, FRAME_SAB_SLOT_BYTES_DEFAULT);
@@ -165,28 +225,24 @@ export function createNodeBackendInternal(opts = {}) {
165
225
  kind: FRAME_TRANSPORT_TRANSFER_V1,
166
226
  version: FRAME_TRANSPORT_VERSION,
167
227
  };
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 = {
228
+ const nativeConfig = normalizeBackendNativeConfig(cfg.nativeConfig);
229
+ const nativeTargetFps = resolveTargetFps(fpsCap, nativeConfig);
230
+ const initConfigBase = {
175
231
  ...nativeConfig,
176
- // Keep native tick generation aligned with app/backend fpsCap unless
177
- // explicitly overridden in nativeConfig.
232
+ // fpsCap is the single frame-scheduling knob; native target fps must align.
178
233
  targetFps: nativeTargetFps,
179
234
  // Negotiation pins (docs/16 + docs/01)
180
235
  requestedEngineAbiMajor: ZR_ENGINE_ABI_MAJOR,
181
236
  requestedEngineAbiMinor: ZR_ENGINE_ABI_MINOR,
182
237
  requestedEngineAbiPatch: ZR_ENGINE_ABI_PATCH,
183
- requestedDrawlistVersion: useDrawlistV2 ? ZR_DRAWLIST_VERSION_V2 : ZR_DRAWLIST_VERSION_V1,
238
+ requestedDrawlistVersion: requestedDrawlistVersion,
184
239
  requestedEventBatchVersion: ZR_EVENT_BATCH_VERSION_V1,
185
240
  // Node worker runtime caps
186
241
  fpsCap,
187
242
  maxEventBytes,
188
243
  frameTransport: frameTransportWire,
189
244
  };
245
+ let initConfigResolved = null;
190
246
  let worker = null;
191
247
  let disposed = false;
192
248
  let started = false;
@@ -199,6 +255,7 @@ export function createNodeBackendInternal(opts = {}) {
199
255
  let nextFrameSeq = 1;
200
256
  const frameAcceptedWaiters = new Map();
201
257
  const frameCompletionWaiters = new Map();
258
+ const frameAuditBySeq = new Map();
202
259
  const eventQueue = [];
203
260
  const eventWaiters = [];
204
261
  const capsWaiters = [];
@@ -248,10 +305,85 @@ export function createNodeBackendInternal(opts = {}) {
248
305
  waiter.reject(err);
249
306
  }
250
307
  frameCompletionWaiters.clear();
308
+ if (frameAudit.enabled) {
309
+ for (const [seq, meta] of frameAuditBySeq.entries()) {
310
+ frameAudit.emit("frame.aborted", {
311
+ reason: err.message,
312
+ ageMs: Math.max(0, Date.now() - meta.submitAtMs),
313
+ ...meta,
314
+ });
315
+ }
316
+ frameAuditBySeq.clear();
317
+ }
318
+ }
319
+ function registerFrameAudit(frameSeq, submitPath, transport, bytes, slotIndex, slotToken) {
320
+ if (!frameAudit.enabled)
321
+ return;
322
+ const fp = drawlistFingerprint(bytes);
323
+ const meta = {
324
+ frameSeq,
325
+ submitAtMs: Date.now(),
326
+ submitPath,
327
+ transport,
328
+ byteLen: fp.byteLen,
329
+ hash32: fp.hash32,
330
+ prefixHash32: fp.prefixHash32,
331
+ cmdCount: fp.cmdCount,
332
+ totalSize: fp.totalSize,
333
+ head16: fp.head16,
334
+ tail16: fp.tail16,
335
+ ...(slotIndex === undefined ? {} : { slotIndex }),
336
+ ...(slotToken === undefined ? {} : { slotToken }),
337
+ };
338
+ frameAuditBySeq.set(frameSeq, meta);
339
+ maybeDumpDrawlistBytes("backend", submitPath, frameSeq, bytes);
340
+ frameAudit.emit("frame.submitted", meta);
341
+ }
342
+ function markAcceptedFramesUpTo(acceptedSeq) {
343
+ if (!frameAudit.enabled)
344
+ return;
345
+ for (const [seq, meta] of frameAuditBySeq.entries()) {
346
+ if (seq > acceptedSeq)
347
+ continue;
348
+ if (meta.acceptedLogged === true)
349
+ continue;
350
+ frameAudit.emit("frame.accepted", {
351
+ acceptedSeq,
352
+ ageMs: Math.max(0, Date.now() - meta.submitAtMs),
353
+ ...meta,
354
+ });
355
+ meta.acceptedLogged = true;
356
+ }
357
+ }
358
+ function markCoalescedFramesBefore(acceptedSeq) {
359
+ if (!frameAudit.enabled)
360
+ return;
361
+ for (const [seq, meta] of frameAuditBySeq.entries()) {
362
+ if (seq >= acceptedSeq)
363
+ continue;
364
+ frameAudit.emit("frame.coalesced", {
365
+ acceptedSeq,
366
+ ageMs: Math.max(0, Date.now() - meta.submitAtMs),
367
+ ...meta,
368
+ });
369
+ frameAuditBySeq.delete(seq);
370
+ }
371
+ }
372
+ function markCompletedFrame(frameSeq, completedResult) {
373
+ if (!frameAudit.enabled)
374
+ return;
375
+ const meta = frameAuditBySeq.get(frameSeq);
376
+ frameAudit.emit("frame.completed", {
377
+ completedResult,
378
+ ageMs: meta ? Math.max(0, Date.now() - meta.submitAtMs) : null,
379
+ ...(meta ?? {}),
380
+ });
381
+ frameAuditBySeq.delete(frameSeq);
251
382
  }
252
383
  function resolveAcceptedFramesUpTo(acceptedSeq) {
253
384
  if (!Number.isInteger(acceptedSeq) || acceptedSeq <= 0)
254
385
  return;
386
+ markAcceptedFramesUpTo(acceptedSeq);
255
387
  for (const [seq, waiter] of frameAcceptedWaiters.entries()) {
256
388
  if (seq > acceptedSeq)
257
389
  continue;
@@ -262,6 +394,7 @@ export function createNodeBackendInternal(opts = {}) {
262
394
  function resolveCoalescedCompletionFramesUpTo(acceptedSeq) {
263
395
  if (!Number.isInteger(acceptedSeq) || acceptedSeq <= 0)
264
396
  return;
397
+ markCoalescedFramesBefore(acceptedSeq);
265
398
  for (const [seq, waiter] of frameCompletionWaiters.entries()) {
266
399
  if (seq >= acceptedSeq)
267
400
  continue;
@@ -270,6 +403,7 @@ export function createNodeBackendInternal(opts = {}) {
270
403
  }
271
404
  }
272
405
  function settleCompletedFrame(frameSeq, completedResult) {
406
+ markCompletedFrame(frameSeq, completedResult);
273
407
  const waiter = frameCompletionWaiters.get(frameSeq);
274
408
  if (waiter === undefined)
275
409
  return;
@@ -280,6 +414,26 @@ export function createNodeBackendInternal(opts = {}) {
280
414
  }
281
415
  waiter.resolve(undefined);
282
416
  }
417
+ function reserveFramePromise(frameSeq) {
418
+ const frameAcceptedDef = deferred();
419
+ frameAcceptedWaiters.set(frameSeq, frameAcceptedDef);
420
+ const frameCompletionDef = deferred();
421
+ frameCompletionWaiters.set(frameSeq, frameCompletionDef);
422
+ const framePromise = frameCompletionDef.promise;
423
+ Object.defineProperty(framePromise, FRAME_ACCEPTED_ACK_MARKER, {
424
+ value: frameAcceptedDef.promise,
425
+ configurable: false,
426
+ enumerable: false,
427
+ writable: false,
428
+ });
429
+ return framePromise;
430
+ }
431
+ function releaseFrameReservation(frameSeq) {
432
+ frameAcceptedWaiters.delete(frameSeq);
433
+ frameCompletionWaiters.delete(frameSeq);
434
+ if (frameAudit.enabled)
435
+ frameAuditBySeq.delete(frameSeq);
436
+ }
283
437
  function failAll(err) {
284
438
  while (eventWaiters.length > 0)
285
439
  eventWaiters.shift()?.reject(err);
@@ -339,6 +493,13 @@ export function createNodeBackendInternal(opts = {}) {
339
493
  return;
340
494
  }
341
495
  case "frameStatus": {
496
+ if (frameAudit.enabled) {
497
+ frameAudit.emit("worker.frameStatus", {
498
+ acceptedSeq: msg.acceptedSeq,
499
+ completedSeq: msg.completedSeq ?? null,
500
+ completedResult: msg.completedResult ?? null,
501
+ });
502
+ }
342
503
  if (!Number.isInteger(msg.acceptedSeq) || msg.acceptedSeq <= 0) {
343
504
  fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `invalid frameStatus.acceptedSeq: ${String(msg.acceptedSeq)}`);
344
505
  failAll(fatal);
@@ -369,6 +530,26 @@ export function createNodeBackendInternal(opts = {}) {
369
530
  return;
370
531
  }
371
532
  case "events": {
533
+ if (!Number.isInteger(msg.byteLen) || msg.byteLen < 0) {
534
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `events: invalid byteLen=${String(msg.byteLen)}`);
535
+ failAll(fatal);
536
+ return;
537
+ }
538
+ if (msg.byteLen > msg.batch.byteLength) {
539
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `events: byteLen=${String(msg.byteLen)} exceeds batch.byteLength=${String(msg.batch.byteLength)}`);
540
+ failAll(fatal);
541
+ return;
542
+ }
543
+ if (msg.byteLen > maxEventBytes) {
544
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `events: byteLen=${String(msg.byteLen)} exceeds maxEventBytes=${String(maxEventBytes)}`);
545
+ failAll(fatal);
546
+ return;
547
+ }
548
+ if (!Number.isInteger(msg.droppedSinceLast) || msg.droppedSinceLast < 0) {
549
+ fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `events: invalid droppedSinceLast=${String(msg.droppedSinceLast)}`);
550
+ failAll(fatal);
551
+ return;
552
+ }
372
553
  const waiter = eventWaiters.shift();
373
554
  if (waiter !== undefined) {
374
555
  const buf = msg.batch;
@@ -526,6 +707,9 @@ export function createNodeBackendInternal(opts = {}) {
526
707
  supportsScrollRegion: msg.supportsScrollRegion,
527
708
  supportsCursorShape: msg.supportsCursorShape,
528
709
  supportsOutputWaitWritable: msg.supportsOutputWaitWritable,
710
+ supportsUnderlineStyles: msg.supportsUnderlineStyles,
711
+ supportsColoredUnderlines: msg.supportsColoredUnderlines,
712
+ supportsHyperlinks: msg.supportsHyperlinks,
529
713
  sgrAttrsSupported: msg.sgrAttrsSupported,
530
714
  };
531
715
  cachedCaps = caps;
@@ -568,7 +752,23 @@ export function createNodeBackendInternal(opts = {}) {
568
752
  throw fatal;
569
753
  if (started)
570
754
  return;
755
+ assertWorkerEnvironmentSupported(opts.nativeShimModule);
571
756
  if (worker === null) {
757
+ if (initConfigResolved === null) {
758
+ const resolvedEmojiWidthPolicy = await resolveBackendEmojiWidthPolicy(cfg.emojiWidthPolicy, nativeConfig);
759
+ const nativeWidthPolicy = applyEmojiWidthPolicy(resolvedEmojiWidthPolicy);
760
+ initConfigResolved = {
761
+ ...initConfigBase,
762
+ widthPolicy: nativeWidthPolicy,
763
+ };
764
+ }
765
+ else {
766
+ // Keep core measurement policy deterministic across stop/start cycles.
767
+ const widthPolicy = initConfigResolved[WIDTH_POLICY_KEY];
768
+ if (typeof widthPolicy === "number") {
769
+ setTextMeasureEmojiPolicy(widthPolicy === 0 ? "narrow" : "wide");
770
+ }
771
+ }
572
772
  startDef = deferred();
573
773
  startSettled = false;
574
774
  stopDef = null;
@@ -576,11 +776,19 @@ export function createNodeBackendInternal(opts = {}) {
576
776
  stopRequested = false;
577
777
  if (sabFrameTransport !== null)
578
778
  resetSabFrameTransport(sabFrameTransport);
579
- const entry = new URL("../worker/engineWorker.js", import.meta.url);
580
779
  const workerData = opts.nativeShimModule === undefined
581
780
  ? undefined
582
781
  : { nativeShimModule: opts.nativeShimModule };
583
- worker = new Worker(entry, { workerData });
782
+ const workerEntry = resolveWorkerEntry(workerData);
783
+ worker = new Worker(workerEntry.entry, workerEntry.options);
784
+ if (frameAudit.enabled) {
785
+ frameAudit.emit("worker.spawn", {
786
+ frameTransport: frameTransportWire.kind,
787
+ frameSabSlotCount: frameSabSlotCount,
788
+ frameSabSlotBytes: frameSabSlotBytes,
789
+ workerEntry: workerEntry.entry.href,
790
+ });
791
+ }
584
792
  exitDef = deferred();
585
793
  worker.on("message", handleWorkerMessage);
586
794
  worker.on("error", (err) => {
@@ -592,11 +800,27 @@ export function createNodeBackendInternal(opts = {}) {
592
800
  worker.on("exit", (code) => {
593
801
  handleWorkerExit(code);
594
802
  });
595
- send({ type: "init", config: initConfig });
803
+ send({ type: "init", config: initConfigResolved });
596
804
  }
597
805
  if (startDef === null)
598
806
  throw new Error("NodeBackend: invariant violated (startDef is null)");
599
- await startDef.promise;
807
+ try {
808
+ await startDef.promise;
809
+ }
810
+ catch (err) {
811
+ // Startup fatals can race with worker teardown. Waiting for exit keeps
812
+ // caller shutdown paths deterministic and avoids process-level teardown
813
+ // races when user code exits immediately after a start() rejection.
814
+ if (exitDef !== null) {
815
+ try {
816
+ await exitDef.promise;
817
+ }
818
+ catch {
819
+ // ignore teardown wait failures
820
+ }
821
+ }
822
+ throw err;
823
+ }
600
824
  },
601
825
  async stop() {
602
826
  if (disposed)
@@ -655,37 +879,44 @@ export function createNodeBackendInternal(opts = {}) {
655
879
  if (worker === null)
656
880
  return Promise.reject(new Error("NodeBackend: worker not available"));
657
881
  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
- });
882
+ const framePromise = reserveFramePromise(frameSeq);
669
883
  if (sabFrameTransport !== null && drawlist.byteLength <= sabFrameTransport.slotBytes) {
670
884
  const slotIndex = acquireSabSlot(sabFrameTransport);
671
885
  if (slotIndex >= 0) {
672
886
  const slotToken = frameSeqToSlotToken(frameSeq);
887
+ registerFrameAudit(frameSeq, "requestFrame", FRAME_TRANSPORT_SAB_V1, drawlist, slotIndex, slotToken);
673
888
  const slotOffset = slotIndex * sabFrameTransport.slotBytes;
674
889
  sabFrameTransport.dataBytes.set(drawlist, slotOffset);
675
890
  Atomics.store(sabFrameTransport.tokens, slotIndex, slotToken);
676
891
  Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_READY);
677
892
  publishSabFrame(sabFrameTransport, frameSeq, slotIndex, slotToken, drawlist.byteLength);
893
+ if (frameAudit.enabled) {
894
+ frameAudit.emit("frame.sab.publish", {
895
+ frameSeq,
896
+ slotIndex,
897
+ slotToken,
898
+ byteLen: drawlist.byteLength,
899
+ });
900
+ }
678
901
  // SAB consumers wake on futex notify instead of per-frame
679
902
  // MessagePort frameKick round-trips.
680
903
  Atomics.notify(sabFrameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 1);
681
904
  return framePromise;
682
905
  }
906
+ if (frameAudit.enabled) {
907
+ frameAudit.emit("frame.sab.fallback_transfer", {
908
+ frameSeq,
909
+ byteLen: drawlist.byteLength,
910
+ reason: "no-slot-available",
911
+ });
912
+ }
683
913
  }
684
914
  // Transfer fallback participates in the same ACK model:
685
915
  // - accepted ACK (hidden marker) can unblock app scheduling early
686
916
  // - completion promise settles on worker completion/coalescing status
687
917
  const buf = new ArrayBuffer(drawlist.byteLength);
688
918
  copyInto(buf, drawlist);
919
+ registerFrameAudit(frameSeq, "requestFrame", FRAME_TRANSPORT_TRANSFER_V1, drawlist);
689
920
  try {
690
921
  send({
691
922
  type: "frame",
@@ -696,10 +927,21 @@ export function createNodeBackendInternal(opts = {}) {
696
927
  }, [buf]);
697
928
  }
698
929
  catch (err) {
699
- frameAcceptedWaiters.delete(frameSeq);
700
- frameCompletionWaiters.delete(frameSeq);
930
+ releaseFrameReservation(frameSeq);
931
+ if (frameAudit.enabled) {
932
+ frameAudit.emit("frame.transfer.publish_error", {
933
+ frameSeq,
934
+ detail: safeErr(err).message,
935
+ });
936
+ }
701
937
  return Promise.reject(safeErr(err));
702
938
  }
939
+ if (frameAudit.enabled) {
940
+ frameAudit.emit("frame.transfer.publish", {
941
+ frameSeq,
942
+ byteLen: drawlist.byteLength,
943
+ });
944
+ }
703
945
  return framePromise;
704
946
  },
705
947
  pollEvents() {
@@ -763,6 +1005,10 @@ export function createNodeBackendInternal(opts = {}) {
763
1005
  send({ type: "getCaps" });
764
1006
  return d.promise;
765
1007
  },
1008
+ async getTerminalProfile() {
1009
+ const caps = await backend.getCaps();
1010
+ return terminalProfileFromNodeEnv(caps);
1011
+ },
766
1012
  };
767
1013
  const debug = {
768
1014
  debugEnable: (config) => enqueueDebug(async () => {
@@ -937,6 +1183,112 @@ export function createNodeBackendInternal(opts = {}) {
937
1183
  return snapshot;
938
1184
  }),
939
1185
  };
940
- return Object.assign(backend, { debug, perf });
1186
+ const beginFrameMetrics = {
1187
+ success: 0,
1188
+ fallbackToRequestFrame: 0,
1189
+ readyReclaims: 0,
1190
+ };
1191
+ const beginFrame = sabFrameTransport === null
1192
+ ? null
1193
+ : (minCapacity) => {
1194
+ if (disposed)
1195
+ return null;
1196
+ if (fatal !== null)
1197
+ return null;
1198
+ if (stopRequested || !started || worker === null)
1199
+ return null;
1200
+ const required = typeof minCapacity === "number" && Number.isInteger(minCapacity) && minCapacity > 0
1201
+ ? minCapacity
1202
+ : 0;
1203
+ if (required > sabFrameTransport.slotBytes)
1204
+ return null;
1205
+ const result = acquireSabSlotTracked(sabFrameTransport);
1206
+ if (result.slotIndex < 0) {
1207
+ beginFrameMetrics.fallbackToRequestFrame++;
1208
+ if (frameAudit.enabled) {
1209
+ frameAudit.emit("frame.beginFrame.fallback", {
1210
+ reason: "no-slot-available",
1211
+ metrics: { ...beginFrameMetrics },
1212
+ });
1213
+ }
1214
+ return null;
1215
+ }
1216
+ if (result.reclaimedReady) {
1217
+ beginFrameMetrics.readyReclaims++;
1218
+ }
1219
+ beginFrameMetrics.success++;
1220
+ const slotIndex = result.slotIndex;
1221
+ const slotOffset = slotIndex * sabFrameTransport.slotBytes;
1222
+ const buf = sabFrameTransport.dataBytes.subarray(slotOffset, slotOffset + sabFrameTransport.slotBytes);
1223
+ let finalized = false;
1224
+ return {
1225
+ buf,
1226
+ commit: (byteLen) => {
1227
+ if (finalized) {
1228
+ return Promise.reject(new Error("NodeBackend: beginFrame writer already finalized"));
1229
+ }
1230
+ finalized = true;
1231
+ if (disposed) {
1232
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1233
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1234
+ return Promise.reject(new Error("NodeBackend: disposed"));
1235
+ }
1236
+ if (fatal !== null) {
1237
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1238
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1239
+ return Promise.reject(fatal);
1240
+ }
1241
+ if (stopRequested || !started || worker === null) {
1242
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1243
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1244
+ return Promise.reject(new Error("NodeBackend: stopped"));
1245
+ }
1246
+ if (!Number.isInteger(byteLen) ||
1247
+ byteLen < 0 ||
1248
+ byteLen > sabFrameTransport.slotBytes) {
1249
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1250
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1251
+ return Promise.reject(new Error("NodeBackend: beginFrame commit byteLen out of range"));
1252
+ }
1253
+ const frameSeq = nextFrameSeq++;
1254
+ const framePromise = reserveFramePromise(frameSeq);
1255
+ const slotToken = frameSeqToSlotToken(frameSeq);
1256
+ registerFrameAudit(frameSeq, "beginFrame", FRAME_TRANSPORT_SAB_V1, buf.subarray(0, byteLen), slotIndex, slotToken);
1257
+ Atomics.store(sabFrameTransport.tokens, slotIndex, slotToken);
1258
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_READY);
1259
+ publishSabFrame(sabFrameTransport, frameSeq, slotIndex, slotToken, byteLen);
1260
+ if (frameAudit.enabled) {
1261
+ frameAudit.emit("frame.beginFrame.publish", {
1262
+ frameSeq,
1263
+ slotIndex,
1264
+ slotToken,
1265
+ byteLen,
1266
+ reclaimedReady: result.reclaimedReady,
1267
+ metrics: { ...beginFrameMetrics },
1268
+ });
1269
+ }
1270
+ Atomics.notify(sabFrameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 1);
1271
+ return framePromise;
1272
+ },
1273
+ abort: () => {
1274
+ if (finalized)
1275
+ return;
1276
+ finalized = true;
1277
+ if (frameAudit.enabled) {
1278
+ frameAudit.emit("frame.beginFrame.abort", {
1279
+ slotIndex,
1280
+ });
1281
+ }
1282
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1283
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1284
+ },
1285
+ };
1286
+ };
1287
+ return attachBackendMarkers(Object.assign(backend, { debug, perf }), {
1288
+ requestedDrawlistVersion,
1289
+ maxEventBytes,
1290
+ fpsCap,
1291
+ beginFrame,
1292
+ });
941
1293
  }
942
1294
  //# sourceMappingURL=nodeBackend.js.map