@rezi-ui/node 0.1.0-alpha.44 → 0.1.0-alpha.47

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.
@@ -25,13 +25,6 @@ export type NodeBackendConfig = Readonly<{
25
25
  * remain aligned by construction.
26
26
  */
27
27
  maxEventBytes?: number;
28
- /**
29
- * Explicit drawlist version request.
30
- *
31
- * Defaults to `5` (enables v3 style extensions + v4 canvas + v5 image commands).
32
- * Supported versions are `2`-`5`.
33
- */
34
- drawlistVersion?: 2 | 3 | 4 | 5;
35
28
  /**
36
29
  * Frame transport mode:
37
30
  * - "auto": prefer SAB mailbox transport when available, fallback to transfer.
@@ -1 +1 @@
1
- {"version":3,"file":"nodeBackend.d.ts","sourceRoot":"","sources":["../../src/backend/nodeBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAGV,YAAY,EAKZ,cAAc,EAGf,MAAM,eAAe,CAAC;AA2CvB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,KAAK,CAAC;IAC7C,2CAA2C;IAC3C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;CAC/C,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,QAAQ,CACd,MAAM,CACJ,MAAM,EACN;QACE,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;KAC5B,CACF,CACF,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,YAAY,EAAE,MAAM,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACtD,CAAC,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,CAAC;AAiRpG,wBAAgB,yBAAyB,CAAC,IAAI,GAAE,uBAA4B,GAAG,WAAW,CAk+BzF"}
1
+ {"version":3,"file":"nodeBackend.d.ts","sourceRoot":"","sources":["../../src/backend/nodeBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAGV,YAAY,EAKZ,cAAc,EAGf,MAAM,eAAe,CAAC;AAkDvB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,KAAK,CAAC;IAC7C,2CAA2C;IAC3C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;CAC/C,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,MAAM,EAAE,QAAQ,CACd,MAAM,CACJ,MAAM,EACN;QACE,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;KAC5B,CACF,CACF,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,YAAY,EAAE,MAAM,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACtD,CAAC,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,CAAC;AA+UpG,wBAAgB,yBAAyB,CAAC,IAAI,GAAE,uBAA4B,GAAG,WAAW,CAwvCzF"}
@@ -7,8 +7,9 @@
7
7
  * @see docs/backend/native.md
8
8
  */
9
9
  import { Worker } from "node:worker_threads";
10
- import { BACKEND_DRAWLIST_VERSION_MARKER, BACKEND_FPS_CAP_MARKER, BACKEND_MAX_EVENT_BYTES_MARKER, BACKEND_RAW_WRITE_MARKER, DEFAULT_TERMINAL_CAPS, FRAME_ACCEPTED_ACK_MARKER, } from "@rezi-ui/core";
11
- import { ZR_DRAWLIST_VERSION_V5, ZR_ENGINE_ABI_MAJOR, ZR_ENGINE_ABI_MINOR, ZR_ENGINE_ABI_PATCH, ZR_EVENT_BATCH_VERSION_V1, ZrUiError, setTextMeasureEmojiPolicy, severityToNum, } from "@rezi-ui/core";
10
+ import { BACKEND_BEGIN_FRAME_MARKER, BACKEND_DRAWLIST_VERSION_MARKER, BACKEND_FPS_CAP_MARKER, BACKEND_MAX_EVENT_BYTES_MARKER, BACKEND_RAW_WRITE_MARKER, DEFAULT_TERMINAL_CAPS, FRAME_ACCEPTED_ACK_MARKER, } from "@rezi-ui/core";
11
+ 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";
12
+ import { createFrameAuditLogger, drawlistFingerprint, maybeDumpDrawlistBytes, } from "../frameAudit.js";
12
13
  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
14
  import { applyEmojiWidthPolicy, resolveBackendEmojiWidthPolicy } from "./emojiWidthPolicy.js";
14
15
  import { createNodeBackendInlineInternal } from "./nodeBackendInline.js";
@@ -80,19 +81,6 @@ function parsePositiveInt(n) {
80
81
  return null;
81
82
  return n;
82
83
  }
83
- function parseDrawlistVersion(v) {
84
- if (v === undefined)
85
- return null;
86
- if (v === 2 || v === 3 || v === 4 || v === 5)
87
- return v;
88
- throw new ZrUiError("ZRUI_INVALID_PROPS", `createNodeBackend config mismatch: drawlistVersion must be one of 2, 3, 4, 5 (got ${String(v)}).`);
89
- }
90
- function resolveRequestedDrawlistVersion(config) {
91
- const explicitDrawlistVersion = parseDrawlistVersion(config.drawlistVersion);
92
- if (explicitDrawlistVersion !== null)
93
- return explicitDrawlistVersion;
94
- return ZR_DRAWLIST_VERSION_V5;
95
- }
96
84
  function parseBoundedPositiveIntOrThrow(name, value, fallback, max) {
97
85
  if (value === undefined)
98
86
  return fallback;
@@ -205,6 +193,38 @@ function acquireSabSlot(t) {
205
193
  }
206
194
  return -1;
207
195
  }
196
+ function acquireSabSlotTracked(t) {
197
+ const start = t.nextSlot.value % t.slotCount;
198
+ for (let i = 0; i < t.slotCount; i++) {
199
+ const slot = (start + i) % t.slotCount;
200
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_FREE, FRAME_SAB_SLOT_STATE_WRITING);
201
+ if (prev === FRAME_SAB_SLOT_STATE_FREE) {
202
+ t.nextSlot.value = (slot + 1) % t.slotCount;
203
+ return { slotIndex: slot, reclaimedReady: false };
204
+ }
205
+ }
206
+ for (let i = 0; i < t.slotCount; i++) {
207
+ const slot = (start + i) % t.slotCount;
208
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_READY, FRAME_SAB_SLOT_STATE_WRITING);
209
+ if (prev === FRAME_SAB_SLOT_STATE_READY) {
210
+ t.nextSlot.value = (slot + 1) % t.slotCount;
211
+ return { slotIndex: slot, reclaimedReady: true };
212
+ }
213
+ }
214
+ return { slotIndex: -1, reclaimedReady: false };
215
+ }
216
+ function acquireSabFreeSlot(t) {
217
+ const start = t.nextSlot.value % t.slotCount;
218
+ for (let i = 0; i < t.slotCount; i++) {
219
+ const slot = (start + i) % t.slotCount;
220
+ const prev = Atomics.compareExchange(t.states, slot, FRAME_SAB_SLOT_STATE_FREE, FRAME_SAB_SLOT_STATE_WRITING);
221
+ if (prev === FRAME_SAB_SLOT_STATE_FREE) {
222
+ t.nextSlot.value = (slot + 1) % t.slotCount;
223
+ return slot;
224
+ }
225
+ }
226
+ return -1;
227
+ }
208
228
  function publishSabFrame(t, frameSeq, slotIndex, slotToken, byteLen) {
209
229
  Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SLOT_WORD, slotIndex);
210
230
  Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_BYTES_WORD, byteLen);
@@ -212,6 +232,7 @@ function publishSabFrame(t, frameSeq, slotIndex, slotToken, byteLen) {
212
232
  Atomics.store(t.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, frameSeq);
213
233
  }
214
234
  export function createNodeBackendInternal(opts = {}) {
235
+ const frameAudit = createFrameAuditLogger("backend");
215
236
  const cfg = opts.config ?? {};
216
237
  const fpsCap = parseBoundedPositiveIntOrThrow("fpsCap", cfg.fpsCap, DEFAULT_FPS_CAP, MAX_SAFE_FPS_CAP);
217
238
  const requestedExecutionMode = cfg.executionMode ?? "auto";
@@ -225,7 +246,7 @@ export function createNodeBackendInternal(opts = {}) {
225
246
  if (executionMode === "inline") {
226
247
  return createNodeBackendInlineInternal(opts);
227
248
  }
228
- const requestedDrawlistVersion = resolveRequestedDrawlistVersion(cfg);
249
+ const requestedDrawlistVersion = ZR_DRAWLIST_VERSION_V1;
229
250
  const maxEventBytes = parseBoundedPositiveIntOrThrow("maxEventBytes", cfg.maxEventBytes, DEFAULT_MAX_EVENT_BYTES, MAX_SAFE_EVENT_BYTES);
230
251
  const frameTransportMode = cfg.frameTransport === "transfer" || cfg.frameTransport === "sab" ? cfg.frameTransport : "auto";
231
252
  const frameSabSlotCount = parsePositiveIntOr(cfg.frameSabSlotCount, FRAME_SAB_SLOT_COUNT_DEFAULT);
@@ -280,6 +301,7 @@ export function createNodeBackendInternal(opts = {}) {
280
301
  let nextFrameSeq = 1;
281
302
  const frameAcceptedWaiters = new Map();
282
303
  const frameCompletionWaiters = new Map();
304
+ const frameAuditBySeq = new Map();
283
305
  const eventQueue = [];
284
306
  const eventWaiters = [];
285
307
  const capsWaiters = [];
@@ -329,10 +351,85 @@ export function createNodeBackendInternal(opts = {}) {
329
351
  waiter.reject(err);
330
352
  }
331
353
  frameCompletionWaiters.clear();
354
+ if (frameAudit.enabled) {
355
+ for (const [seq, meta] of frameAuditBySeq.entries()) {
356
+ frameAudit.emit("frame.aborted", {
357
+ reason: err.message,
358
+ ageMs: Math.max(0, Date.now() - meta.submitAtMs),
359
+ ...meta,
360
+ });
361
+ }
362
+ frameAuditBySeq.clear();
363
+ }
364
+ }
365
+ function registerFrameAudit(frameSeq, submitPath, transport, bytes, slotIndex, slotToken) {
366
+ if (!frameAudit.enabled)
367
+ return;
368
+ const fp = drawlistFingerprint(bytes);
369
+ const meta = {
370
+ frameSeq,
371
+ submitAtMs: Date.now(),
372
+ submitPath,
373
+ transport,
374
+ byteLen: fp.byteLen,
375
+ hash32: fp.hash32,
376
+ prefixHash32: fp.prefixHash32,
377
+ cmdCount: fp.cmdCount,
378
+ totalSize: fp.totalSize,
379
+ head16: fp.head16,
380
+ tail16: fp.tail16,
381
+ ...(slotIndex === undefined ? {} : { slotIndex }),
382
+ ...(slotToken === undefined ? {} : { slotToken }),
383
+ };
384
+ frameAuditBySeq.set(frameSeq, meta);
385
+ maybeDumpDrawlistBytes("backend", submitPath, frameSeq, bytes);
386
+ frameAudit.emit("frame.submitted", meta);
387
+ }
388
+ function markAcceptedFramesUpTo(acceptedSeq) {
389
+ if (!frameAudit.enabled)
390
+ return;
391
+ for (const [seq, meta] of frameAuditBySeq.entries()) {
392
+ if (seq > acceptedSeq)
393
+ continue;
394
+ if (meta.acceptedLogged === true)
395
+ continue;
396
+ frameAudit.emit("frame.accepted", {
397
+ acceptedSeq,
398
+ ageMs: Math.max(0, Date.now() - meta.submitAtMs),
399
+ ...meta,
400
+ });
401
+ meta.acceptedLogged = true;
402
+ }
403
+ }
404
+ function markCoalescedFramesBefore(acceptedSeq) {
405
+ if (!frameAudit.enabled)
406
+ return;
407
+ for (const [seq, meta] of frameAuditBySeq.entries()) {
408
+ if (seq >= acceptedSeq)
409
+ continue;
410
+ frameAudit.emit("frame.coalesced", {
411
+ acceptedSeq,
412
+ ageMs: Math.max(0, Date.now() - meta.submitAtMs),
413
+ ...meta,
414
+ });
415
+ frameAuditBySeq.delete(seq);
416
+ }
417
+ }
418
+ function markCompletedFrame(frameSeq, completedResult) {
419
+ if (!frameAudit.enabled)
420
+ return;
421
+ const meta = frameAuditBySeq.get(frameSeq);
422
+ frameAudit.emit("frame.completed", {
423
+ completedResult,
424
+ ageMs: meta ? Math.max(0, Date.now() - meta.submitAtMs) : null,
425
+ ...(meta ?? {}),
426
+ });
427
+ frameAuditBySeq.delete(frameSeq);
332
428
  }
333
429
  function resolveAcceptedFramesUpTo(acceptedSeq) {
334
430
  if (!Number.isInteger(acceptedSeq) || acceptedSeq <= 0)
335
431
  return;
432
+ markAcceptedFramesUpTo(acceptedSeq);
336
433
  for (const [seq, waiter] of frameAcceptedWaiters.entries()) {
337
434
  if (seq > acceptedSeq)
338
435
  continue;
@@ -343,6 +440,7 @@ export function createNodeBackendInternal(opts = {}) {
343
440
  function resolveCoalescedCompletionFramesUpTo(acceptedSeq) {
344
441
  if (!Number.isInteger(acceptedSeq) || acceptedSeq <= 0)
345
442
  return;
443
+ markCoalescedFramesBefore(acceptedSeq);
346
444
  for (const [seq, waiter] of frameCompletionWaiters.entries()) {
347
445
  if (seq >= acceptedSeq)
348
446
  continue;
@@ -351,6 +449,7 @@ export function createNodeBackendInternal(opts = {}) {
351
449
  }
352
450
  }
353
451
  function settleCompletedFrame(frameSeq, completedResult) {
452
+ markCompletedFrame(frameSeq, completedResult);
354
453
  const waiter = frameCompletionWaiters.get(frameSeq);
355
454
  if (waiter === undefined)
356
455
  return;
@@ -361,6 +460,26 @@ export function createNodeBackendInternal(opts = {}) {
361
460
  }
362
461
  waiter.resolve(undefined);
363
462
  }
463
+ function reserveFramePromise(frameSeq) {
464
+ const frameAcceptedDef = deferred();
465
+ frameAcceptedWaiters.set(frameSeq, frameAcceptedDef);
466
+ const frameCompletionDef = deferred();
467
+ frameCompletionWaiters.set(frameSeq, frameCompletionDef);
468
+ const framePromise = frameCompletionDef.promise;
469
+ Object.defineProperty(framePromise, FRAME_ACCEPTED_ACK_MARKER, {
470
+ value: frameAcceptedDef.promise,
471
+ configurable: false,
472
+ enumerable: false,
473
+ writable: false,
474
+ });
475
+ return framePromise;
476
+ }
477
+ function releaseFrameReservation(frameSeq) {
478
+ frameAcceptedWaiters.delete(frameSeq);
479
+ frameCompletionWaiters.delete(frameSeq);
480
+ if (frameAudit.enabled)
481
+ frameAuditBySeq.delete(frameSeq);
482
+ }
364
483
  function failAll(err) {
365
484
  while (eventWaiters.length > 0)
366
485
  eventWaiters.shift()?.reject(err);
@@ -420,6 +539,13 @@ export function createNodeBackendInternal(opts = {}) {
420
539
  return;
421
540
  }
422
541
  case "frameStatus": {
542
+ if (frameAudit.enabled) {
543
+ frameAudit.emit("worker.frameStatus", {
544
+ acceptedSeq: msg.acceptedSeq,
545
+ completedSeq: msg.completedSeq ?? null,
546
+ completedResult: msg.completedResult ?? null,
547
+ });
548
+ }
423
549
  if (!Number.isInteger(msg.acceptedSeq) || msg.acceptedSeq <= 0) {
424
550
  fatal = new ZrUiError("ZRUI_BACKEND_ERROR", `invalid frameStatus.acceptedSeq: ${String(msg.acceptedSeq)}`);
425
551
  failAll(fatal);
@@ -700,6 +826,13 @@ export function createNodeBackendInternal(opts = {}) {
700
826
  ? undefined
701
827
  : { nativeShimModule: opts.nativeShimModule };
702
828
  worker = new Worker(entry, { workerData });
829
+ if (frameAudit.enabled) {
830
+ frameAudit.emit("worker.spawn", {
831
+ frameTransport: frameTransportWire.kind,
832
+ frameSabSlotCount: frameSabSlotCount,
833
+ frameSabSlotBytes: frameSabSlotBytes,
834
+ });
835
+ }
703
836
  exitDef = deferred();
704
837
  worker.on("message", handleWorkerMessage);
705
838
  worker.on("error", (err) => {
@@ -774,37 +907,44 @@ export function createNodeBackendInternal(opts = {}) {
774
907
  if (worker === null)
775
908
  return Promise.reject(new Error("NodeBackend: worker not available"));
776
909
  const frameSeq = nextFrameSeq++;
777
- const frameAcceptedDef = deferred();
778
- frameAcceptedWaiters.set(frameSeq, frameAcceptedDef);
779
- const frameCompletionDef = deferred();
780
- frameCompletionWaiters.set(frameSeq, frameCompletionDef);
781
- const framePromise = frameCompletionDef.promise;
782
- Object.defineProperty(framePromise, FRAME_ACCEPTED_ACK_MARKER, {
783
- value: frameAcceptedDef.promise,
784
- configurable: false,
785
- enumerable: false,
786
- writable: false,
787
- });
910
+ const framePromise = reserveFramePromise(frameSeq);
788
911
  if (sabFrameTransport !== null && drawlist.byteLength <= sabFrameTransport.slotBytes) {
789
912
  const slotIndex = acquireSabSlot(sabFrameTransport);
790
913
  if (slotIndex >= 0) {
791
914
  const slotToken = frameSeqToSlotToken(frameSeq);
915
+ registerFrameAudit(frameSeq, "requestFrame", FRAME_TRANSPORT_SAB_V1, drawlist, slotIndex, slotToken);
792
916
  const slotOffset = slotIndex * sabFrameTransport.slotBytes;
793
917
  sabFrameTransport.dataBytes.set(drawlist, slotOffset);
794
918
  Atomics.store(sabFrameTransport.tokens, slotIndex, slotToken);
795
919
  Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_READY);
796
920
  publishSabFrame(sabFrameTransport, frameSeq, slotIndex, slotToken, drawlist.byteLength);
921
+ if (frameAudit.enabled) {
922
+ frameAudit.emit("frame.sab.publish", {
923
+ frameSeq,
924
+ slotIndex,
925
+ slotToken,
926
+ byteLen: drawlist.byteLength,
927
+ });
928
+ }
797
929
  // SAB consumers wake on futex notify instead of per-frame
798
930
  // MessagePort frameKick round-trips.
799
931
  Atomics.notify(sabFrameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 1);
800
932
  return framePromise;
801
933
  }
934
+ if (frameAudit.enabled) {
935
+ frameAudit.emit("frame.sab.fallback_transfer", {
936
+ frameSeq,
937
+ byteLen: drawlist.byteLength,
938
+ reason: "no-slot-available",
939
+ });
940
+ }
802
941
  }
803
942
  // Transfer fallback participates in the same ACK model:
804
943
  // - accepted ACK (hidden marker) can unblock app scheduling early
805
944
  // - completion promise settles on worker completion/coalescing status
806
945
  const buf = new ArrayBuffer(drawlist.byteLength);
807
946
  copyInto(buf, drawlist);
947
+ registerFrameAudit(frameSeq, "requestFrame", FRAME_TRANSPORT_TRANSFER_V1, drawlist);
808
948
  try {
809
949
  send({
810
950
  type: "frame",
@@ -815,10 +955,21 @@ export function createNodeBackendInternal(opts = {}) {
815
955
  }, [buf]);
816
956
  }
817
957
  catch (err) {
818
- frameAcceptedWaiters.delete(frameSeq);
819
- frameCompletionWaiters.delete(frameSeq);
958
+ releaseFrameReservation(frameSeq);
959
+ if (frameAudit.enabled) {
960
+ frameAudit.emit("frame.transfer.publish_error", {
961
+ frameSeq,
962
+ detail: safeErr(err).message,
963
+ });
964
+ }
820
965
  return Promise.reject(safeErr(err));
821
966
  }
967
+ if (frameAudit.enabled) {
968
+ frameAudit.emit("frame.transfer.publish", {
969
+ frameSeq,
970
+ byteLen: drawlist.byteLength,
971
+ });
972
+ }
822
973
  return framePromise;
823
974
  },
824
975
  pollEvents() {
@@ -1060,6 +1211,107 @@ export function createNodeBackendInternal(opts = {}) {
1060
1211
  return snapshot;
1061
1212
  }),
1062
1213
  };
1214
+ const beginFrameMetrics = {
1215
+ success: 0,
1216
+ fallbackToRequestFrame: 0,
1217
+ readyReclaims: 0,
1218
+ };
1219
+ const beginFrame = sabFrameTransport === null
1220
+ ? null
1221
+ : (minCapacity) => {
1222
+ if (disposed)
1223
+ return null;
1224
+ if (fatal !== null)
1225
+ return null;
1226
+ if (stopRequested || !started || worker === null)
1227
+ return null;
1228
+ const required = typeof minCapacity === "number" && Number.isInteger(minCapacity) && minCapacity > 0
1229
+ ? minCapacity
1230
+ : 0;
1231
+ if (required > sabFrameTransport.slotBytes)
1232
+ return null;
1233
+ const result = acquireSabSlotTracked(sabFrameTransport);
1234
+ if (result.slotIndex < 0) {
1235
+ beginFrameMetrics.fallbackToRequestFrame++;
1236
+ if (frameAudit.enabled) {
1237
+ frameAudit.emit("frame.beginFrame.fallback", {
1238
+ reason: "no-slot-available",
1239
+ metrics: { ...beginFrameMetrics },
1240
+ });
1241
+ }
1242
+ return null;
1243
+ }
1244
+ if (result.reclaimedReady) {
1245
+ beginFrameMetrics.readyReclaims++;
1246
+ }
1247
+ beginFrameMetrics.success++;
1248
+ const slotIndex = result.slotIndex;
1249
+ const slotOffset = slotIndex * sabFrameTransport.slotBytes;
1250
+ const buf = sabFrameTransport.dataBytes.subarray(slotOffset, slotOffset + sabFrameTransport.slotBytes);
1251
+ let finalized = false;
1252
+ return {
1253
+ buf,
1254
+ commit: (byteLen) => {
1255
+ if (finalized) {
1256
+ return Promise.reject(new Error("NodeBackend: beginFrame writer already finalized"));
1257
+ }
1258
+ finalized = true;
1259
+ if (disposed) {
1260
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1261
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1262
+ return Promise.reject(new Error("NodeBackend: disposed"));
1263
+ }
1264
+ if (fatal !== null) {
1265
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1266
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1267
+ return Promise.reject(fatal);
1268
+ }
1269
+ if (stopRequested || !started || worker === null) {
1270
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1271
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1272
+ return Promise.reject(new Error("NodeBackend: stopped"));
1273
+ }
1274
+ if (!Number.isInteger(byteLen) ||
1275
+ byteLen < 0 ||
1276
+ byteLen > sabFrameTransport.slotBytes) {
1277
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1278
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1279
+ return Promise.reject(new Error("NodeBackend: beginFrame commit byteLen out of range"));
1280
+ }
1281
+ const frameSeq = nextFrameSeq++;
1282
+ const framePromise = reserveFramePromise(frameSeq);
1283
+ const slotToken = frameSeqToSlotToken(frameSeq);
1284
+ registerFrameAudit(frameSeq, "beginFrame", FRAME_TRANSPORT_SAB_V1, buf.subarray(0, byteLen), slotIndex, slotToken);
1285
+ Atomics.store(sabFrameTransport.tokens, slotIndex, slotToken);
1286
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_READY);
1287
+ publishSabFrame(sabFrameTransport, frameSeq, slotIndex, slotToken, byteLen);
1288
+ if (frameAudit.enabled) {
1289
+ frameAudit.emit("frame.beginFrame.publish", {
1290
+ frameSeq,
1291
+ slotIndex,
1292
+ slotToken,
1293
+ byteLen,
1294
+ reclaimedReady: result.reclaimedReady,
1295
+ metrics: { ...beginFrameMetrics },
1296
+ });
1297
+ }
1298
+ Atomics.notify(sabFrameTransport.controlHeader, FRAME_SAB_CONTROL_PUBLISHED_SEQ_WORD, 1);
1299
+ return framePromise;
1300
+ },
1301
+ abort: () => {
1302
+ if (finalized)
1303
+ return;
1304
+ finalized = true;
1305
+ if (frameAudit.enabled) {
1306
+ frameAudit.emit("frame.beginFrame.abort", {
1307
+ slotIndex,
1308
+ });
1309
+ }
1310
+ Atomics.store(sabFrameTransport.tokens, slotIndex, 0);
1311
+ Atomics.store(sabFrameTransport.states, slotIndex, FRAME_SAB_SLOT_STATE_FREE);
1312
+ },
1313
+ };
1314
+ };
1063
1315
  const out = Object.assign(backend, { debug, perf });
1064
1316
  Object.defineProperties(out, {
1065
1317
  [BACKEND_DRAWLIST_VERSION_MARKER]: {
@@ -1096,6 +1348,14 @@ export function createNodeBackendInternal(opts = {}) {
1096
1348
  configurable: false,
1097
1349
  },
1098
1350
  });
1351
+ if (beginFrame !== null) {
1352
+ Object.defineProperty(out, BACKEND_BEGIN_FRAME_MARKER, {
1353
+ value: beginFrame,
1354
+ writable: false,
1355
+ enumerable: false,
1356
+ configurable: false,
1357
+ });
1358
+ }
1099
1359
  return out;
1100
1360
  }
1101
1361
  //# sourceMappingURL=nodeBackend.js.map