@czap/worker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/compositor-script.d.ts +19 -0
  4. package/dist/compositor-script.d.ts.map +1 -0
  5. package/dist/compositor-script.js +374 -0
  6. package/dist/compositor-script.js.map +1 -0
  7. package/dist/compositor-startup.d.ts +200 -0
  8. package/dist/compositor-startup.d.ts.map +1 -0
  9. package/dist/compositor-startup.js +490 -0
  10. package/dist/compositor-startup.js.map +1 -0
  11. package/dist/compositor-types.d.ts +135 -0
  12. package/dist/compositor-types.d.ts.map +1 -0
  13. package/dist/compositor-types.js +7 -0
  14. package/dist/compositor-types.js.map +1 -0
  15. package/dist/compositor-worker.d.ts +65 -0
  16. package/dist/compositor-worker.d.ts.map +1 -0
  17. package/dist/compositor-worker.js +454 -0
  18. package/dist/compositor-worker.js.map +1 -0
  19. package/dist/evaluate-inline.d.ts +22 -0
  20. package/dist/evaluate-inline.d.ts.map +1 -0
  21. package/dist/evaluate-inline.js +42 -0
  22. package/dist/evaluate-inline.js.map +1 -0
  23. package/dist/host.d.ts +97 -0
  24. package/dist/host.d.ts.map +1 -0
  25. package/dist/host.js +115 -0
  26. package/dist/host.js.map +1 -0
  27. package/dist/index.d.ts +40 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +40 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/messages.d.ts +254 -0
  32. package/dist/messages.d.ts.map +1 -0
  33. package/dist/messages.js +55 -0
  34. package/dist/messages.js.map +1 -0
  35. package/dist/render-worker.d.ts +77 -0
  36. package/dist/render-worker.d.ts.map +1 -0
  37. package/dist/render-worker.js +396 -0
  38. package/dist/render-worker.js.map +1 -0
  39. package/dist/spsc-ring.d.ts +171 -0
  40. package/dist/spsc-ring.d.ts.map +1 -0
  41. package/dist/spsc-ring.js +240 -0
  42. package/dist/spsc-ring.js.map +1 -0
  43. package/package.json +51 -0
  44. package/src/compositor-script.ts +374 -0
  45. package/src/compositor-startup.ts +666 -0
  46. package/src/compositor-types.ts +175 -0
  47. package/src/compositor-worker.ts +613 -0
  48. package/src/evaluate-inline.ts +42 -0
  49. package/src/host.ts +189 -0
  50. package/src/index.ts +49 -0
  51. package/src/messages.ts +343 -0
  52. package/src/render-worker.ts +454 -0
  53. package/src/spsc-ring.ts +309 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * RenderWorker -- off-main-thread video renderer using OffscreenCanvas.
3
+ *
4
+ * The render worker:
5
+ * 1. Receives an OffscreenCanvas via postMessage transfer
6
+ * 2. On `start-render`, iterates frames at the configured fps
7
+ * 3. For each frame, computes state (simplified inline compositor)
8
+ * and draws to the OffscreenCanvas 2d context
9
+ * 4. Posts `frame` messages back with VideoFrameOutput data
10
+ * 5. Posts `render-complete` when done
11
+ *
12
+ * The worker script is inlined as a Blob URL (no separate file needed).
13
+ *
14
+ * @module
15
+ */
16
+ import { type VideoConfig, type VideoFrameOutput } from '@czap/core';
17
+ /**
18
+ * Host-facing surface of a render worker. Owns the underlying `Worker`
19
+ * and `OffscreenCanvas` once transferred; created by
20
+ * {@link RenderWorker.create}.
21
+ */
22
+ export interface RenderWorkerShape {
23
+ /** The underlying Worker instance. */
24
+ readonly worker: Worker;
25
+ /**
26
+ * Transfer an OffscreenCanvas to the worker.
27
+ * The canvas must have been obtained via `canvas.transferControlToOffscreen()`.
28
+ */
29
+ transferCanvas(canvas: OffscreenCanvas): void;
30
+ /** Start rendering frames with the given video configuration. */
31
+ startRender(config: VideoConfig): void;
32
+ /** Stop an in-progress render. */
33
+ stopRender(): void;
34
+ /** Subscribe to per-frame output. Returns an unsubscribe function. */
35
+ onFrame(callback: (output: VideoFrameOutput) => void): () => void;
36
+ /** Subscribe to render completion. Returns an unsubscribe function. */
37
+ onComplete(callback: (totalFrames: number) => void): () => void;
38
+ /** Terminate the worker and clean up resources. */
39
+ dispose(): void;
40
+ }
41
+ declare function _createRenderWorker(): RenderWorkerShape;
42
+ /**
43
+ * Factory namespace for the render worker.
44
+ *
45
+ * Call {@link RenderWorker.create} on the main thread to mint a worker
46
+ * that owns an `OffscreenCanvas` and renders `VideoFrameOutput` frames
47
+ * off the main thread. Transfer control via
48
+ * {@link RenderWorkerShape.transferCanvas} before calling `startRender`.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { RenderWorker } from '@czap/worker';
53
+ *
54
+ * const renderer = RenderWorker.create();
55
+ * const offscreen = canvas.transferControlToOffscreen();
56
+ * renderer.transferCanvas(offscreen);
57
+ * renderer.onFrame((frame) => {
58
+ * // stream frame.image / frame.timestampMs somewhere
59
+ * });
60
+ * renderer.startRender({ durationMs: 4000, fps: 30, width: 640, height: 360 });
61
+ * ```
62
+ */
63
+ export declare const RenderWorker: {
64
+ /**
65
+ * Spin up a render worker. The worker starts idle; transfer an
66
+ * `OffscreenCanvas` via
67
+ * {@link RenderWorkerShape.transferCanvas} before calling
68
+ * `startRender`.
69
+ */
70
+ readonly create: typeof _createRenderWorker;
71
+ };
72
+ export declare namespace RenderWorker {
73
+ /** Public host-side surface returned by {@link RenderWorker.create}. */
74
+ type Shape = RenderWorkerShape;
75
+ }
76
+ export {};
77
+ //# sourceMappingURL=render-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-worker.d.ts","sourceRoot":"","sources":["../src/render-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAQlF;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAE9C,iEAAiE;IACjE,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAEvC,kCAAkC;IAClC,UAAU,IAAI,IAAI,CAAC;IAEnB,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAElE,uEAAuE;IACvE,UAAU,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAEhE,mDAAmD;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAqRD,iBAAS,mBAAmB,IAAI,iBAAiB,CAmFhD;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY;IACvB;;;;;OAKG;;CAEK,CAAC;AAEX,MAAM,CAAC,OAAO,WAAW,YAAY,CAAC;IACpC,wEAAwE;IACxE,KAAY,KAAK,GAAG,iBAAiB,CAAC;CACvC"}
@@ -0,0 +1,396 @@
1
+ /**
2
+ * RenderWorker -- off-main-thread video renderer using OffscreenCanvas.
3
+ *
4
+ * The render worker:
5
+ * 1. Receives an OffscreenCanvas via postMessage transfer
6
+ * 2. On `start-render`, iterates frames at the configured fps
7
+ * 3. For each frame, computes state (simplified inline compositor)
8
+ * and draws to the OffscreenCanvas 2d context
9
+ * 4. Posts `frame` messages back with VideoFrameOutput data
10
+ * 5. Posts `render-complete` when done
11
+ *
12
+ * The worker script is inlined as a Blob URL (no separate file needed).
13
+ *
14
+ * @module
15
+ */
16
+ import { Diagnostics } from '@czap/core';
17
+ import { EVALUATE_THRESHOLDS_SOURCE } from './evaluate-inline.js';
18
+ // ---------------------------------------------------------------------------
19
+ // Inline worker script
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Self-contained render worker script.
23
+ *
24
+ * Contains a minimal compositor and frame iterator that draws state
25
+ * visualization to an OffscreenCanvas.
26
+ */
27
+ const RENDER_WORKER_SCRIPT = /* js */ `
28
+ "use strict";
29
+
30
+ /** @type {OffscreenCanvas | null} */
31
+ let canvas = null;
32
+
33
+ /** @type {OffscreenCanvasRenderingContext2D | null} */
34
+ let ctx = null;
35
+
36
+ /** @type {boolean} */
37
+ let rendering = false;
38
+
39
+ /** @type {boolean} */
40
+ let stopRequested = false;
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Simplified inline compositor (mirrors compositor-worker / Boundary.evaluate)
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /** @type {Map<string, { id: string; states: string[]; thresholds: number[]; currentState: string }>} */
47
+ const quantizers = new Map();
48
+
49
+ /** @type {Map<string, Record<string, number>>} */
50
+ const blendOverrides = new Map();
51
+
52
+ ${EVALUATE_THRESHOLDS_SOURCE}
53
+
54
+ /**
55
+ * Compute a CompositeState from the current quantizer state.
56
+ */
57
+ function computeState() {
58
+ const discrete = {};
59
+ const blend = {};
60
+ const css = {};
61
+ const glsl = {};
62
+ const aria = {};
63
+
64
+ for (const [name, q] of quantizers) {
65
+ const stateStr = q.currentState;
66
+ discrete[name] = stateStr;
67
+
68
+ const override = blendOverrides.get(name);
69
+ if (override !== undefined) {
70
+ blend[name] = override;
71
+ } else {
72
+ const weights = {};
73
+ for (const s of q.states) {
74
+ weights[s] = s === stateStr ? 1 : 0;
75
+ }
76
+ blend[name] = weights;
77
+ }
78
+
79
+ css["--czap-" + name] = stateStr;
80
+
81
+ let stateIndex = 0;
82
+ for (let i = 0; i < q.states.length; i++) {
83
+ if (q.states[i] === stateStr) {
84
+ stateIndex = i;
85
+ break;
86
+ }
87
+ }
88
+ glsl["u_" + name] = stateIndex;
89
+ aria["data-czap-" + name] = stateStr;
90
+ }
91
+
92
+ return { discrete, blend, outputs: { css, glsl, aria } };
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Canvas rendering
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Draw the current CompositeState to the OffscreenCanvas.
101
+ * This is a diagnostic visualization; real applications would
102
+ * implement domain-specific rendering.
103
+ *
104
+ * @param {{ discrete: Record<string, string>; blend: Record<string, Record<string, number>>; outputs: { css: Record<string, number|string>; glsl: Record<string, number>; aria: Record<string, string> } }} state
105
+ * @param {number} frame
106
+ * @param {number} progress
107
+ */
108
+ function drawState(state, frame, progress) {
109
+ if (!ctx || !canvas) return;
110
+
111
+ const w = canvas.width;
112
+ const h = canvas.height;
113
+
114
+ // Clear
115
+ ctx.clearRect(0, 0, w, h);
116
+
117
+ // Background: gradient based on progress
118
+ const gray = Math.round(32 + progress * 32);
119
+ ctx.fillStyle = "rgb(" + gray + "," + gray + "," + gray + ")";
120
+ ctx.fillRect(0, 0, w, h);
121
+
122
+ // Draw discrete state labels
123
+ ctx.fillStyle = "#ffffff";
124
+ ctx.font = "14px monospace";
125
+ ctx.textBaseline = "top";
126
+
127
+ let y = 16;
128
+ const keys = Object.keys(state.discrete);
129
+ for (let i = 0; i < keys.length; i++) {
130
+ const name = keys[i];
131
+ const value = state.discrete[name];
132
+ ctx.fillText(name + ": " + value, 16, y);
133
+ y += 20;
134
+ }
135
+
136
+ // Draw progress bar
137
+ const barY = h - 24;
138
+ const barH = 8;
139
+ ctx.fillStyle = "#333333";
140
+ ctx.fillRect(16, barY, w - 32, barH);
141
+ ctx.fillStyle = "#4488ff";
142
+ ctx.fillRect(16, barY, (w - 32) * progress, barH);
143
+
144
+ // Frame counter
145
+ ctx.fillStyle = "#aaaaaa";
146
+ ctx.font = "12px monospace";
147
+ ctx.textBaseline = "bottom";
148
+ ctx.fillText("frame " + frame, 16, barY - 4);
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Render loop
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /**
156
+ * Run the fixed-step render loop.
157
+ * @param {{ fps: number; width: number; height: number; durationMs: number }} config
158
+ */
159
+ async function runRender(config) {
160
+ if (rendering) return;
161
+ rendering = true;
162
+ stopRequested = false;
163
+
164
+ const totalFrames = Math.ceil((config.durationMs / 1000) * config.fps);
165
+
166
+ try {
167
+ for (let i = 0; i < totalFrames; i++) {
168
+ if (stopRequested) break;
169
+
170
+ const timestamp = (i * 1000) / config.fps;
171
+ const progress = totalFrames > 1 ? i / (totalFrames - 1) : 1;
172
+ const state = computeState();
173
+
174
+ // Draw to canvas
175
+ drawState(state, i, progress);
176
+
177
+ /** @type {import('./messages.js').VideoFrameOutput} */
178
+ const output = { frame: i, timestamp, progress, state };
179
+
180
+ self.postMessage({ type: "frame", output: output });
181
+
182
+ // Yield to allow stop messages to be processed.
183
+ // In a real scenario, the frame rate would be controlled by
184
+ // the encoding pipeline; here we use a minimal yield.
185
+ if (i % 10 === 9) {
186
+ await new Promise(function (r) { setTimeout(r, 0); });
187
+ }
188
+ }
189
+
190
+ self.postMessage({ type: "render-complete", totalFrames: totalFrames });
191
+ } catch (err) {
192
+ self.postMessage({
193
+ type: "error",
194
+ message: err instanceof Error ? err.message : String(err),
195
+ });
196
+ } finally {
197
+ rendering = false;
198
+ }
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Message handler
203
+ // ---------------------------------------------------------------------------
204
+
205
+ self.addEventListener("message", function (e) {
206
+ const msg = e.data;
207
+ if (!msg || typeof msg.type !== "string") return;
208
+
209
+ switch (msg.type) {
210
+ case "init": {
211
+ quantizers.clear();
212
+ blendOverrides.clear();
213
+ self.postMessage({ type: "ready" });
214
+ break;
215
+ }
216
+
217
+ case "transfer-canvas": {
218
+ canvas = msg.canvas;
219
+ ctx = canvas.getContext("2d");
220
+ break;
221
+ }
222
+
223
+ case "add-quantizer": {
224
+ const initialState = msg.states[0] || "";
225
+ quantizers.set(msg.name, {
226
+ id: msg.boundaryId,
227
+ states: Array.from(msg.states),
228
+ thresholds: Array.from(msg.thresholds),
229
+ currentState: initialState,
230
+ });
231
+ break;
232
+ }
233
+
234
+ case "remove-quantizer": {
235
+ quantizers.delete(msg.name);
236
+ blendOverrides.delete(msg.name);
237
+ break;
238
+ }
239
+
240
+ case "evaluate": {
241
+ const q = quantizers.get(msg.name);
242
+ if (q) {
243
+ q.currentState = evaluateThresholds(q.thresholds, q.states, msg.value);
244
+ }
245
+ break;
246
+ }
247
+
248
+ case "set-blend": {
249
+ blendOverrides.set(msg.name, msg.weights);
250
+ break;
251
+ }
252
+
253
+ case "start-render": {
254
+ runRender(msg.config);
255
+ break;
256
+ }
257
+
258
+ case "stop-render": {
259
+ stopRequested = true;
260
+ break;
261
+ }
262
+
263
+ case "dispose": {
264
+ stopRequested = true;
265
+ quantizers.clear();
266
+ blendOverrides.clear();
267
+ canvas = null;
268
+ ctx = null;
269
+ self.close();
270
+ break;
271
+ }
272
+ }
273
+ });
274
+ `;
275
+ // ---------------------------------------------------------------------------
276
+ // Factory helpers
277
+ // ---------------------------------------------------------------------------
278
+ function _send(worker, msg, transfer) {
279
+ if (transfer && transfer.length > 0) {
280
+ worker.postMessage(msg, transfer);
281
+ }
282
+ else {
283
+ worker.postMessage(msg);
284
+ }
285
+ }
286
+ // ---------------------------------------------------------------------------
287
+ // Factory
288
+ // ---------------------------------------------------------------------------
289
+ function _createRenderWorker() {
290
+ const blob = new Blob([RENDER_WORKER_SCRIPT], { type: 'application/javascript' });
291
+ const url = URL.createObjectURL(blob);
292
+ const worker = new Worker(url, { type: 'classic', name: 'czap-renderer' });
293
+ URL.revokeObjectURL(url);
294
+ const frameListeners = new Set();
295
+ const completeListeners = new Set();
296
+ worker.addEventListener('message', (e) => {
297
+ const msg = e.data;
298
+ if (!msg || typeof msg.type !== 'string')
299
+ return;
300
+ switch (msg.type) {
301
+ case 'frame':
302
+ for (const cb of frameListeners)
303
+ cb(msg.output);
304
+ break;
305
+ case 'render-complete':
306
+ for (const cb of completeListeners)
307
+ cb(msg.totalFrames);
308
+ break;
309
+ case 'error':
310
+ Diagnostics.error({
311
+ source: 'czap/worker.render-worker',
312
+ code: 'worker-message-error',
313
+ message: 'Render worker reported an error.',
314
+ detail: msg.message,
315
+ });
316
+ break;
317
+ }
318
+ });
319
+ worker.addEventListener('error', (e) => {
320
+ Diagnostics.error({
321
+ source: 'czap/worker.render-worker',
322
+ code: 'worker-unhandled-error',
323
+ message: 'Render worker raised an unhandled error.',
324
+ detail: e.message,
325
+ });
326
+ });
327
+ // Initialize
328
+ _send(worker, { type: 'init' });
329
+ return {
330
+ get worker() {
331
+ return worker;
332
+ },
333
+ transferCanvas(canvas) {
334
+ // The canvas is Transferable -- it must be in the transfer list
335
+ _send(worker, { type: 'transfer-canvas', canvas }, [canvas]);
336
+ },
337
+ startRender(config) {
338
+ _send(worker, { type: 'start-render', config });
339
+ },
340
+ stopRender() {
341
+ _send(worker, { type: 'stop-render' });
342
+ },
343
+ onFrame(callback) {
344
+ frameListeners.add(callback);
345
+ return () => {
346
+ frameListeners.delete(callback);
347
+ };
348
+ },
349
+ onComplete(callback) {
350
+ completeListeners.add(callback);
351
+ return () => {
352
+ completeListeners.delete(callback);
353
+ };
354
+ },
355
+ dispose() {
356
+ _send(worker, { type: 'dispose' });
357
+ frameListeners.clear();
358
+ completeListeners.clear();
359
+ worker.terminate();
360
+ },
361
+ };
362
+ }
363
+ // ---------------------------------------------------------------------------
364
+ // Export
365
+ // ---------------------------------------------------------------------------
366
+ /**
367
+ * Factory namespace for the render worker.
368
+ *
369
+ * Call {@link RenderWorker.create} on the main thread to mint a worker
370
+ * that owns an `OffscreenCanvas` and renders `VideoFrameOutput` frames
371
+ * off the main thread. Transfer control via
372
+ * {@link RenderWorkerShape.transferCanvas} before calling `startRender`.
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * import { RenderWorker } from '@czap/worker';
377
+ *
378
+ * const renderer = RenderWorker.create();
379
+ * const offscreen = canvas.transferControlToOffscreen();
380
+ * renderer.transferCanvas(offscreen);
381
+ * renderer.onFrame((frame) => {
382
+ * // stream frame.image / frame.timestampMs somewhere
383
+ * });
384
+ * renderer.startRender({ durationMs: 4000, fps: 30, width: 640, height: 360 });
385
+ * ```
386
+ */
387
+ export const RenderWorker = {
388
+ /**
389
+ * Spin up a render worker. The worker starts idle; transfer an
390
+ * `OffscreenCanvas` via
391
+ * {@link RenderWorkerShape.transferCanvas} before calling
392
+ * `startRender`.
393
+ */
394
+ create: _createRenderWorker,
395
+ };
396
+ //# sourceMappingURL=render-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-worker.js","sourceRoot":"","sources":["../src/render-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ /**
2
+ * SPSCRing -- lock-free single-producer single-consumer ring buffer
3
+ * backed by SharedArrayBuffer.
4
+ *
5
+ * Designed for real-time compositor state streaming between a Worker
6
+ * (producer) and the main thread (consumer) without blocking either side.
7
+ *
8
+ * ## SharedArrayBuffer requirements
9
+ *
10
+ * SharedArrayBuffer requires the page to be served with the following
11
+ * HTTP headers (COOP/COEP):
12
+ *
13
+ * Cross-Origin-Opener-Policy: same-origin
14
+ * Cross-Origin-Embedder-Policy: require-corp
15
+ *
16
+ * Without these headers, `new SharedArrayBuffer(...)` will throw.
17
+ *
18
+ * ## Memory layout
19
+ *
20
+ * ```
21
+ * Int32Array view (control region):
22
+ * [0]: write cursor (atomically incremented by producer)
23
+ * [1]: read cursor (atomically incremented by consumer)
24
+ *
25
+ * Float64Array view (data region):
26
+ * Offset = 8 bytes (aligned after two Int32 control slots)
27
+ * [0 .. slotCount * slotSize - 1]: ring buffer data slots
28
+ * ```
29
+ *
30
+ * The producer writes at `writeCursor % slotCount`, the consumer reads
31
+ * at `readCursor % slotCount`. The buffer is full when
32
+ * `write - read === slotCount`, empty when `write === read`.
33
+ *
34
+ * Only `Atomics.load` and `Atomics.store` are used -- no `Atomics.wait`
35
+ * or `Atomics.notify` -- keeping this fully lock-free and non-blocking.
36
+ *
37
+ * @module
38
+ */
39
+ /**
40
+ * Producer- or consumer-side handle to a single-producer/single-consumer
41
+ * ring buffer backed by `SharedArrayBuffer`. Created by
42
+ * {@link SPSCRing.attachProducer} or {@link SPSCRing.attachConsumer}.
43
+ */
44
+ export interface SPSCRingBufferShape {
45
+ /**
46
+ * Push a data slot into the ring buffer.
47
+ * Returns `false` if the buffer is full (non-blocking).
48
+ */
49
+ push(data: Float64Array): boolean;
50
+ /**
51
+ * Pop a data slot from the ring buffer into the provided output array.
52
+ * Returns `false` if the buffer is empty (non-blocking).
53
+ */
54
+ pop(out: Float64Array): boolean;
55
+ /** Number of slots in the ring buffer. */
56
+ readonly capacity: number;
57
+ /** Current number of occupied slots. */
58
+ readonly count: number;
59
+ }
60
+ /**
61
+ * Create a matched producer/consumer pair sharing the same SharedArrayBuffer.
62
+ *
63
+ * Typically called on the main thread; the `buffer` (SharedArrayBuffer) is
64
+ * then transferred to the Worker via `postMessage`, and the Worker calls
65
+ * `SPSCRing.attachProducer` to get its side of the ring.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { SPSCRing } from '@czap/worker';
70
+ *
71
+ * const { buffer, producer, consumer } = SPSCRing.createPair(64, 4);
72
+ * // producer.push(new Float64Array([1, 2, 3, 4])); // true
73
+ * // consumer.pop(new Float64Array(4)); // true
74
+ * // Transfer buffer to a Worker via postMessage
75
+ * worker.postMessage({ buffer, slotCount: 64, slotSize: 4 });
76
+ * ```
77
+ *
78
+ * @param slotCount - Number of slots in the ring (power of 2 recommended)
79
+ * @param slotSize - Number of Float64 values per slot
80
+ * @returns An object with the shared buffer and producer/consumer ring handles
81
+ */
82
+ declare function _createPair(slotCount: number, slotSize: number): {
83
+ buffer: SharedArrayBuffer;
84
+ producer: SPSCRingBufferShape;
85
+ consumer: SPSCRingBufferShape;
86
+ };
87
+ /**
88
+ * Attach as producer to an existing SharedArrayBuffer.
89
+ * Call this inside the Worker that produces data.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { SPSCRing } from '@czap/worker';
94
+ *
95
+ * // Inside a Worker's message handler:
96
+ * self.onmessage = (e) => {
97
+ * const { buffer, slotCount, slotSize } = e.data;
98
+ * const producer = SPSCRing.attachProducer(buffer, slotCount, slotSize);
99
+ * const data = new Float64Array([1.0, 2.0, 3.0, 4.0]);
100
+ * producer.push(data); // true if buffer not full
101
+ * };
102
+ * ```
103
+ *
104
+ * @param sab - The SharedArrayBuffer from the main thread
105
+ * @param slotCount - Number of slots (must match createPair)
106
+ * @param slotSize - Float64 values per slot (must match createPair)
107
+ * @returns A producer-side {@link SPSCRingBufferShape}
108
+ */
109
+ declare function _attachProducer(sab: SharedArrayBuffer, slotCount: number, slotSize: number): SPSCRingBufferShape;
110
+ /**
111
+ * Attach as consumer to an existing SharedArrayBuffer.
112
+ * Call this on the main thread that consumes data.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { SPSCRing } from '@czap/worker';
117
+ *
118
+ * // On the main thread after receiving buffer from Worker:
119
+ * const consumer = SPSCRing.attachConsumer(sharedBuffer, 64, 4);
120
+ * const out = new Float64Array(4);
121
+ * if (consumer.pop(out)) {
122
+ * console.log('Received:', out); // Float64Array [1.0, 2.0, 3.0, 4.0]
123
+ * }
124
+ * ```
125
+ *
126
+ * @param sab - The SharedArrayBuffer shared with the producer
127
+ * @param slotCount - Number of slots (must match createPair)
128
+ * @param slotSize - Float64 values per slot (must match createPair)
129
+ * @returns A consumer-side {@link SPSCRingBufferShape}
130
+ */
131
+ declare function _attachConsumer(sab: SharedArrayBuffer, slotCount: number, slotSize: number): SPSCRingBufferShape;
132
+ /**
133
+ * SPSC ring buffer namespace.
134
+ *
135
+ * Lock-free single-producer single-consumer ring buffer backed by
136
+ * `SharedArrayBuffer`. Designed for real-time compositor state streaming
137
+ * between a Worker (producer) and the main thread (consumer) without
138
+ * blocking either side. Uses only `Atomics.load`/`Atomics.store` --
139
+ * fully non-blocking.
140
+ *
141
+ * @example
142
+ * ```ts
143
+ * import { SPSCRing } from '@czap/worker';
144
+ *
145
+ * // Main thread: create pair and send buffer to Worker
146
+ * const { buffer, producer, consumer } = SPSCRing.createPair(128, 8);
147
+ * worker.postMessage({ buffer, slotCount: 128, slotSize: 8 });
148
+ *
149
+ * // In Worker: attach as producer
150
+ * // const producer = SPSCRing.attachProducer(buffer, 128, 8);
151
+ * // producer.push(new Float64Array(8));
152
+ *
153
+ * // Main thread: consume in animation loop
154
+ * const out = new Float64Array(8);
155
+ * function frame() {
156
+ * while (consumer.pop(out)) { /* process out *\/ }
157
+ * requestAnimationFrame(frame);
158
+ * }
159
+ * ```
160
+ */
161
+ export declare const SPSCRing: {
162
+ readonly createPair: typeof _createPair;
163
+ readonly attachProducer: typeof _attachProducer;
164
+ readonly attachConsumer: typeof _attachConsumer;
165
+ };
166
+ export declare namespace SPSCRing {
167
+ /** Producer- or consumer-facing view of a SPSC ring buffer. */
168
+ type Shape = SPSCRingBufferShape;
169
+ }
170
+ export {};
171
+ //# sourceMappingURL=spsc-ring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spsc-ring.d.ts","sourceRoot":"","sources":["../src/spsc-ring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAoBH;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC;IAElC;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC;IAEhC,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAiGD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,iBAAS,WAAW,CAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf;IACD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,mBAAmB,CAAC;CAC/B,CAOA;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,iBAAS,eAAe,CAAC,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAEzG;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,iBAAS,eAAe,CAAC,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAEzG;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,QAAQ;;;;CAIX,CAAC;AAEX,MAAM,CAAC,OAAO,WAAW,QAAQ,CAAC;IAChC,+DAA+D;IAC/D,KAAY,KAAK,GAAG,mBAAmB,CAAC;CACzC"}