@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,309 @@
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
+ // ---------------------------------------------------------------------------
41
+ // Constants
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /** Byte offset where the Int32 control slots live. */
45
+ const WRITE_CURSOR_INDEX = 0;
46
+ const READ_CURSOR_INDEX = 1;
47
+
48
+ /**
49
+ * Byte size of the control region: two Int32 values (8 bytes),
50
+ * padded to 8-byte alignment for the Float64 data region.
51
+ */
52
+ const CONTROL_BYTES = 8;
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Types
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Producer- or consumer-side handle to a single-producer/single-consumer
60
+ * ring buffer backed by `SharedArrayBuffer`. Created by
61
+ * {@link SPSCRing.attachProducer} or {@link SPSCRing.attachConsumer}.
62
+ */
63
+ export interface SPSCRingBufferShape {
64
+ /**
65
+ * Push a data slot into the ring buffer.
66
+ * Returns `false` if the buffer is full (non-blocking).
67
+ */
68
+ push(data: Float64Array): boolean;
69
+
70
+ /**
71
+ * Pop a data slot from the ring buffer into the provided output array.
72
+ * Returns `false` if the buffer is empty (non-blocking).
73
+ */
74
+ pop(out: Float64Array): boolean;
75
+
76
+ /** Number of slots in the ring buffer. */
77
+ readonly capacity: number;
78
+
79
+ /** Current number of occupied slots. */
80
+ readonly count: number;
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Implementation
85
+ // ---------------------------------------------------------------------------
86
+
87
+ function _createBuffer(slotCount: number, slotSize: number): SharedArrayBuffer {
88
+ const dataBytes = slotCount * slotSize * Float64Array.BYTES_PER_ELEMENT;
89
+ return new SharedArrayBuffer(CONTROL_BYTES + dataBytes);
90
+ }
91
+
92
+ function _makeRing(
93
+ sab: SharedArrayBuffer,
94
+ slotCount: number,
95
+ slotSize: number,
96
+ role: 'producer' | 'consumer',
97
+ ): SPSCRingBufferShape {
98
+ if (slotCount <= 0 || !Number.isInteger(slotCount)) {
99
+ throw new RangeError(`SPSCRingBuffer: slotCount must be a positive integer, got ${slotCount}`);
100
+ }
101
+ if (slotSize <= 0 || !Number.isInteger(slotSize)) {
102
+ throw new RangeError(`SPSCRingBuffer: slotSize must be a positive integer, got ${slotSize}`);
103
+ }
104
+ const control = new Int32Array(sab, 0, 2);
105
+ const data = new Float64Array(sab, CONTROL_BYTES);
106
+
107
+ return {
108
+ push(input: Float64Array): boolean {
109
+ if (role !== 'producer') {
110
+ throw new Error('SPSCRingBuffer: only the producer may push');
111
+ }
112
+ if (input.length !== slotSize) {
113
+ throw new RangeError(`SPSCRingBuffer: expected slot size ${slotSize}, got ${input.length}`);
114
+ }
115
+
116
+ const write = Atomics.load(control, WRITE_CURSOR_INDEX);
117
+ const read = Atomics.load(control, READ_CURSOR_INDEX);
118
+
119
+ // Full when write - read === slotCount
120
+ if (write - read >= slotCount) {
121
+ return false;
122
+ }
123
+
124
+ const slotIndex = (write % slotCount) * slotSize;
125
+ for (let i = 0; i < slotSize; i++) {
126
+ data[slotIndex + i] = input[i]!;
127
+ }
128
+
129
+ // Store with release semantics: the data write must be visible
130
+ // before the cursor advances. Atomics.store on Int32Array provides
131
+ // a sequentially consistent store which is stronger than needed
132
+ // but correct.
133
+ Atomics.store(control, WRITE_CURSOR_INDEX, write + 1);
134
+ return true;
135
+ },
136
+
137
+ pop(out: Float64Array): boolean {
138
+ if (role !== 'consumer') {
139
+ throw new Error('SPSCRingBuffer: only the consumer may pop');
140
+ }
141
+ if (out.length !== slotSize) {
142
+ throw new RangeError(`SPSCRingBuffer: expected slot size ${slotSize}, got ${out.length}`);
143
+ }
144
+
145
+ const write = Atomics.load(control, WRITE_CURSOR_INDEX);
146
+ const read = Atomics.load(control, READ_CURSOR_INDEX);
147
+
148
+ // Empty when write === read
149
+ if (write === read) {
150
+ return false;
151
+ }
152
+
153
+ const slotIndex = (read % slotCount) * slotSize;
154
+ for (let i = 0; i < slotSize; i++) {
155
+ out[i] = data[slotIndex + i]!;
156
+ }
157
+
158
+ Atomics.store(control, READ_CURSOR_INDEX, read + 1);
159
+ return true;
160
+ },
161
+
162
+ get capacity(): number {
163
+ return slotCount;
164
+ },
165
+
166
+ get count(): number {
167
+ const write = Atomics.load(control, WRITE_CURSOR_INDEX);
168
+ const read = Atomics.load(control, READ_CURSOR_INDEX);
169
+ return write - read;
170
+ },
171
+ };
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Public API
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /**
179
+ * Create a matched producer/consumer pair sharing the same SharedArrayBuffer.
180
+ *
181
+ * Typically called on the main thread; the `buffer` (SharedArrayBuffer) is
182
+ * then transferred to the Worker via `postMessage`, and the Worker calls
183
+ * `SPSCRing.attachProducer` to get its side of the ring.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * import { SPSCRing } from '@czap/worker';
188
+ *
189
+ * const { buffer, producer, consumer } = SPSCRing.createPair(64, 4);
190
+ * // producer.push(new Float64Array([1, 2, 3, 4])); // true
191
+ * // consumer.pop(new Float64Array(4)); // true
192
+ * // Transfer buffer to a Worker via postMessage
193
+ * worker.postMessage({ buffer, slotCount: 64, slotSize: 4 });
194
+ * ```
195
+ *
196
+ * @param slotCount - Number of slots in the ring (power of 2 recommended)
197
+ * @param slotSize - Number of Float64 values per slot
198
+ * @returns An object with the shared buffer and producer/consumer ring handles
199
+ */
200
+ function _createPair(
201
+ slotCount: number,
202
+ slotSize: number,
203
+ ): {
204
+ buffer: SharedArrayBuffer;
205
+ producer: SPSCRingBufferShape;
206
+ consumer: SPSCRingBufferShape;
207
+ } {
208
+ const buffer = _createBuffer(slotCount, slotSize);
209
+ return {
210
+ buffer,
211
+ producer: _makeRing(buffer, slotCount, slotSize, 'producer'),
212
+ consumer: _makeRing(buffer, slotCount, slotSize, 'consumer'),
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Attach as producer to an existing SharedArrayBuffer.
218
+ * Call this inside the Worker that produces data.
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * import { SPSCRing } from '@czap/worker';
223
+ *
224
+ * // Inside a Worker's message handler:
225
+ * self.onmessage = (e) => {
226
+ * const { buffer, slotCount, slotSize } = e.data;
227
+ * const producer = SPSCRing.attachProducer(buffer, slotCount, slotSize);
228
+ * const data = new Float64Array([1.0, 2.0, 3.0, 4.0]);
229
+ * producer.push(data); // true if buffer not full
230
+ * };
231
+ * ```
232
+ *
233
+ * @param sab - The SharedArrayBuffer from the main thread
234
+ * @param slotCount - Number of slots (must match createPair)
235
+ * @param slotSize - Float64 values per slot (must match createPair)
236
+ * @returns A producer-side {@link SPSCRingBufferShape}
237
+ */
238
+ function _attachProducer(sab: SharedArrayBuffer, slotCount: number, slotSize: number): SPSCRingBufferShape {
239
+ return _makeRing(sab, slotCount, slotSize, 'producer');
240
+ }
241
+
242
+ /**
243
+ * Attach as consumer to an existing SharedArrayBuffer.
244
+ * Call this on the main thread that consumes data.
245
+ *
246
+ * @example
247
+ * ```ts
248
+ * import { SPSCRing } from '@czap/worker';
249
+ *
250
+ * // On the main thread after receiving buffer from Worker:
251
+ * const consumer = SPSCRing.attachConsumer(sharedBuffer, 64, 4);
252
+ * const out = new Float64Array(4);
253
+ * if (consumer.pop(out)) {
254
+ * console.log('Received:', out); // Float64Array [1.0, 2.0, 3.0, 4.0]
255
+ * }
256
+ * ```
257
+ *
258
+ * @param sab - The SharedArrayBuffer shared with the producer
259
+ * @param slotCount - Number of slots (must match createPair)
260
+ * @param slotSize - Float64 values per slot (must match createPair)
261
+ * @returns A consumer-side {@link SPSCRingBufferShape}
262
+ */
263
+ function _attachConsumer(sab: SharedArrayBuffer, slotCount: number, slotSize: number): SPSCRingBufferShape {
264
+ return _makeRing(sab, slotCount, slotSize, 'consumer');
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Export
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /**
272
+ * SPSC ring buffer namespace.
273
+ *
274
+ * Lock-free single-producer single-consumer ring buffer backed by
275
+ * `SharedArrayBuffer`. Designed for real-time compositor state streaming
276
+ * between a Worker (producer) and the main thread (consumer) without
277
+ * blocking either side. Uses only `Atomics.load`/`Atomics.store` --
278
+ * fully non-blocking.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * import { SPSCRing } from '@czap/worker';
283
+ *
284
+ * // Main thread: create pair and send buffer to Worker
285
+ * const { buffer, producer, consumer } = SPSCRing.createPair(128, 8);
286
+ * worker.postMessage({ buffer, slotCount: 128, slotSize: 8 });
287
+ *
288
+ * // In Worker: attach as producer
289
+ * // const producer = SPSCRing.attachProducer(buffer, 128, 8);
290
+ * // producer.push(new Float64Array(8));
291
+ *
292
+ * // Main thread: consume in animation loop
293
+ * const out = new Float64Array(8);
294
+ * function frame() {
295
+ * while (consumer.pop(out)) { /* process out *\/ }
296
+ * requestAnimationFrame(frame);
297
+ * }
298
+ * ```
299
+ */
300
+ export const SPSCRing = {
301
+ createPair: _createPair,
302
+ attachProducer: _attachProducer,
303
+ attachConsumer: _attachConsumer,
304
+ } as const;
305
+
306
+ export declare namespace SPSCRing {
307
+ /** Producer- or consumer-facing view of a SPSC ring buffer. */
308
+ export type Shape = SPSCRingBufferShape;
309
+ }