sabcom 0.1.140 → 0.1.141

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.
package/src/index.ts CHANGED
@@ -1,348 +1,657 @@
1
- export const SEMAPHORE = 0;
2
-
3
- export enum Semaphore {
4
- READY,
5
- HANDSHAKE,
6
- PAYLOAD,
1
+ export interface Options {
2
+ timeout?: number;
7
3
  }
8
4
 
9
- export enum Handshake {
10
- TOTAL_SIZE = 1,
11
- TOTAL_CHUNKS,
5
+ export interface Channel {
6
+ write(data: Uint8Array, options?: Options): Promise<void>;
7
+ read(options?: Options): Promise<Uint8Array>;
8
+ writeSync(data: Uint8Array, options?: Options): void;
9
+ readSync(options?: Options): Uint8Array;
10
+ close(): void;
12
11
  }
13
12
 
14
- export enum Header {
15
- CHUNK_INDEX = 1,
16
- CHUNK_OFFSET,
17
- CHUNK_SIZE,
18
- }
13
+ const MAGIC = 0x53424332;
14
+ const INIT_SENTINEL = -1;
15
+ const MIN_BUFFER_BYTES = 4096;
16
+ const VERSION = 2;
17
+ const HEADER_WORDS = 5;
18
+ const RING_CONTROL_WORDS = 4;
19
+ const DESCRIPTOR_WORDS = 3;
20
+ const CONTINUATION = -1;
21
+ const SLOT_OPTIONS = [256, 128, 64, 32, 16, 8];
22
+ const MIN_DATA_BYTES = 256;
23
+ const FLAG_CLOSED_A = 1;
24
+ const FLAG_CLOSED_B = 2;
19
25
 
20
- export const HEADER_VALUES = 4;
26
+ const IDX_MAGIC = 0;
27
+ const IDX_VERSION = 1;
28
+ const IDX_OWNER_A = 2;
29
+ const IDX_OWNER_B = 3;
30
+ const IDX_FLAGS = 4;
21
31
 
22
- /**
23
- * Size in bytes reserved for protocol header in the SharedArrayBuffer.
24
- * The usable payload size is `buffer.byteLength - HEADER_SIZE`.
25
- */
26
- export const HEADER_SIZE = Uint32Array.BYTES_PER_ELEMENT * HEADER_VALUES;
32
+ const IDX_WRITE_MSG = 0;
33
+ const IDX_READ_MSG = 1;
34
+ const IDX_READ_BYTE = 3;
27
35
 
28
- export interface Options {
29
- /** Max wait time in milliseconds before timeout error. Default: 5000 */
30
- timeout?: number;
36
+ const IDX_DESC_OFFSET = 0;
37
+ const IDX_DESC_SIZE = 1;
38
+ const IDX_DESC_TOTAL = 2;
39
+
40
+ interface Layout {
41
+ controlBytes: number;
42
+ descriptorBytes: number;
43
+ dataBytes: number;
44
+ perDirectionBytes: number;
45
+ ringSlots: number;
31
46
  }
32
47
 
33
- /**
34
- * Request yielded by generator functions for Atomics.wait/waitAsync.
35
- * Pass to Atomics.wait() for sync or Atomics.waitAsync() for async waiting.
36
- */
37
- export interface WaitRequest {
38
- /** Int32Array view of the SharedArrayBuffer header */
39
- target: Int32Array;
40
- /** Index in the array to wait on (always SEMAPHORE = 0) */
41
- index: number;
42
- /** Value to compare against before waiting */
43
- value: number;
44
- /** Timeout in milliseconds */
45
- timeout?: number;
48
+ interface RingViews {
49
+ control: Int32Array;
50
+ descriptors: Int32Array;
51
+ bytes: Uint8Array;
52
+ dataBytes: number;
53
+ ringSlots: number;
54
+ ringMask: number;
46
55
  }
47
56
 
48
- export type WaitResponse = ReturnType<typeof Atomics.wait>;
49
-
50
- /**
51
- * Low-level generator for writing data with custom flow control.
52
- * Use for progress tracking, cancellation, or custom scheduling.
53
- * @param data - Bytes to send (can be larger than buffer, will be chunked automatically)
54
- * @param buffer - SharedArrayBuffer shared with the reader thread (byteLength must be multiple of 4 and larger than HEADER_SIZE)
55
- * @param options - Optional configuration
56
- * @yields {WaitRequest} Request to wait for reader acknowledgment
57
- * @throws {Error} "SharedArrayBuffer byteLength must be a multiple of 4"
58
- * @throws {Error} "SharedArrayBuffer too small for header"
59
- * @throws {Error} "Reader handshake timeout" - reader didn't respond in time
60
- * @throws {Error} "Reader timeout on chunk N/M" - reader stopped responding mid-transfer
61
- * @example
62
- * ```typescript
63
- * import { writeGenerator } from 'sabcom';
64
- *
65
- * const gen = writeGenerator(data, buffer);
66
- * let chunks = 0;
67
- * for (const request of gen) {
68
- * const result = Atomics.wait(request.target, request.index, request.value, request.timeout);
69
- * if (result === 'timed-out') throw new Error('Timeout');
70
- * console.log(`Chunk ${++chunks} sent`);
71
- * }
72
- * ```
73
- */
74
- export function* writeGenerator(data: Uint8Array, buffer: SharedArrayBuffer, { timeout = 5000 }: Options = {}): Generator<WaitRequest, void, WaitResponse> {
75
- if (buffer.byteLength % Int32Array.BYTES_PER_ELEMENT !== 0) {
76
- throw new Error('SharedArrayBuffer byteLength must be a multiple of 4');
77
- }
78
- const chunkSize = buffer.byteLength - HEADER_SIZE;
79
- if (chunkSize <= 0) {
80
- throw new Error('SharedArrayBuffer too small for header');
57
+ interface State {
58
+ header: Int32Array;
59
+ inbound: RingViews;
60
+ outbound: RingViews;
61
+ ownerIndex: number;
62
+ localClosedMask: number;
63
+ peerClosedMask: number;
64
+ localClosed: boolean;
65
+ writeLocked: boolean;
66
+ readLocked: boolean;
67
+ readTail: Promise<void>;
68
+ writeTail: Promise<void>;
69
+ wMsg: number;
70
+ wByte: number;
71
+ rMsg: number;
72
+ rByte: number;
73
+ pendingAckMsg: number;
74
+ pendingAckByte: number;
75
+ pendingAckSize: number;
76
+ }
77
+
78
+ const layoutCache = new Map<number, Layout | null>();
79
+ const stateCache = new WeakMap<SharedArrayBuffer, State>();
80
+ const handleCache = new WeakMap<SharedArrayBuffer, Channel>();
81
+ const resolvedTail = Promise.resolve();
82
+
83
+ const roundDownToWord = (value: number): number => value & ~3;
84
+
85
+ const computeLayout = (byteLength: number): Layout | null => {
86
+ const cached = layoutCache.get(byteLength);
87
+ if (cached !== undefined) return cached;
88
+
89
+ if (byteLength % Int32Array.BYTES_PER_ELEMENT !== 0 || byteLength < MIN_BUFFER_BYTES) {
90
+ layoutCache.set(byteLength, null);
91
+ return null;
81
92
  }
82
- const totalSize = data.length;
83
- const totalChunks = Math.ceil(totalSize / chunkSize);
84
- const header = new Int32Array(buffer);
85
93
 
86
- header[Handshake.TOTAL_SIZE] = totalSize;
87
- header[Handshake.TOTAL_CHUNKS] = totalChunks;
88
- Atomics.store(header, SEMAPHORE, Semaphore.HANDSHAKE);
89
- Atomics.notify(header, SEMAPHORE);
94
+ const headerBytes = HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT;
95
+ const perDirectionBytes = roundDownToWord((byteLength - headerBytes) >> 1);
96
+ const controlBytes = RING_CONTROL_WORDS * Int32Array.BYTES_PER_ELEMENT;
90
97
 
91
- try {
92
- const handshakeResult: WaitResponse = yield {
93
- target: header,
94
- index: SEMAPHORE,
95
- value: Semaphore.HANDSHAKE,
96
- timeout,
97
- };
98
- if (handshakeResult === 'timed-out') {
99
- throw new Error('Reader handshake timeout');
98
+ for (const ringSlots of SLOT_OPTIONS) {
99
+ const descriptorBytes = ringSlots * DESCRIPTOR_WORDS * Int32Array.BYTES_PER_ELEMENT;
100
+ const dataBytes = perDirectionBytes - controlBytes - descriptorBytes;
101
+ if (dataBytes >= MIN_DATA_BYTES) {
102
+ const layout = { controlBytes, descriptorBytes, dataBytes, perDirectionBytes, ringSlots };
103
+ layoutCache.set(byteLength, layout);
104
+ return layout;
100
105
  }
106
+ }
101
107
 
102
- const payload = new Uint8Array(buffer, HEADER_SIZE);
103
- for (let i = 0; i < totalChunks; i++) {
104
- const start = i * chunkSize;
105
- const end = Math.min(start + chunkSize, totalSize);
106
- const size = end - start;
107
- payload.set(data.subarray(start, end), 0);
108
- header[Header.CHUNK_INDEX] = i;
109
- header[Header.CHUNK_OFFSET] = start;
110
- header[Header.CHUNK_SIZE] = size;
111
- Atomics.store(header, SEMAPHORE, Semaphore.PAYLOAD);
112
- Atomics.notify(header, SEMAPHORE);
113
-
114
- const chunkResult: WaitResponse = yield {
115
- target: header,
116
- index: SEMAPHORE,
117
- value: Semaphore.PAYLOAD,
118
- timeout,
119
- };
120
- if (chunkResult === 'timed-out') {
121
- throw new Error(`Reader timeout on chunk ${i}/${totalChunks - 1}`);
122
- }
123
- }
124
- } finally {
125
- Atomics.store(header, SEMAPHORE, Semaphore.READY);
126
- Atomics.notify(header, SEMAPHORE);
108
+ layoutCache.set(byteLength, null);
109
+ return null;
110
+ };
111
+
112
+ const createRingViews = (buffer: SharedArrayBuffer, byteOffset: number, layout: Layout): RingViews => {
113
+ const descriptorOffset = byteOffset + layout.controlBytes;
114
+ const dataOffset = descriptorOffset + layout.descriptorBytes;
115
+ return {
116
+ control: new Int32Array(buffer, byteOffset, RING_CONTROL_WORDS),
117
+ descriptors: new Int32Array(buffer, descriptorOffset, layout.ringSlots * DESCRIPTOR_WORDS),
118
+ bytes: new Uint8Array(buffer, dataOffset, layout.dataBytes),
119
+ dataBytes: layout.dataBytes,
120
+ ringSlots: layout.ringSlots,
121
+ ringMask: layout.ringSlots - 1,
122
+ };
123
+ };
124
+
125
+ const createEndpointToken = (): number => {
126
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
127
+ const buf = new Int32Array(1);
128
+ do {
129
+ crypto.getRandomValues(buf);
130
+ } while (buf[0] === 0 || buf[0] === INIT_SENTINEL || buf[0] === MAGIC);
131
+ return buf[0];
127
132
  }
128
- }
133
+ let token = 0;
134
+ while (token === 0 || token === INIT_SENTINEL || token === MAGIC) {
135
+ token = (Math.random() * 0x7fffffff) | 0;
136
+ }
137
+ return token;
138
+ };
129
139
 
130
- /**
131
- * Low-level generator for reading data with custom flow control.
132
- * Use for progress tracking, cancellation, or custom scheduling.
133
- * @param buffer - SharedArrayBuffer shared with the writer thread (byteLength must be multiple of 4 and larger than HEADER_SIZE)
134
- * @param options - Optional configuration
135
- * @yields {WaitRequest} Request to wait for writer data
136
- * @returns Complete data as Uint8Array
137
- * @throws {Error} "SharedArrayBuffer byteLength must be a multiple of 4"
138
- * @throws {Error} "SharedArrayBuffer too small for header"
139
- * @throws {Error} "Handshake timeout" - writer didn't send data in time
140
- * @throws {Error} "Invalid handshake state" - protocol error
141
- * @throws {Error} "Writer timeout waiting for chunk N" - writer stopped responding mid-transfer
142
- * @example
143
- * ```typescript
144
- * import { readGenerator } from 'sabcom';
145
- *
146
- * const gen = readGenerator(buffer);
147
- * let result = gen.next();
148
- * while (!result.done) {
149
- * const waitResult = Atomics.wait(result.value.target, result.value.index, result.value.value, result.value.timeout);
150
- * result = gen.next(waitResult);
151
- * }
152
- * const data = result.value; // Uint8Array
153
- * ```
154
- */
155
- export function* readGenerator(buffer: SharedArrayBuffer, { timeout = 5000 }: Options = {}): Generator<WaitRequest, Uint8Array, WaitResponse> {
156
- if (buffer.byteLength % Int32Array.BYTES_PER_ELEMENT !== 0) {
157
- throw new Error('SharedArrayBuffer byteLength must be a multiple of 4');
140
+ const ENDPOINT_TOKEN = createEndpointToken();
141
+
142
+ const finalizeHeader = (buffer: SharedArrayBuffer, header: Int32Array): void => {
143
+ const bodyOffset = HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT;
144
+ if (bodyOffset < buffer.byteLength) {
145
+ new Int32Array(buffer, bodyOffset, (buffer.byteLength - bodyOffset) / Int32Array.BYTES_PER_ELEMENT).fill(0);
158
146
  }
159
- const chunkSize = buffer.byteLength - HEADER_SIZE;
160
- if (chunkSize <= 0) {
161
- throw new Error('SharedArrayBuffer too small for header');
147
+ Atomics.store(header, IDX_VERSION, VERSION);
148
+ Atomics.store(header, IDX_OWNER_A, 0);
149
+ Atomics.store(header, IDX_OWNER_B, 0);
150
+ Atomics.store(header, IDX_FLAGS, 0);
151
+ Atomics.store(header, IDX_MAGIC, MAGIC);
152
+ Atomics.notify(header, IDX_MAGIC);
153
+ };
154
+
155
+ const initializeHeaderSync = (buffer: SharedArrayBuffer, header: Int32Array): void => {
156
+ for (;;) {
157
+ const magic = Atomics.load(header, IDX_MAGIC);
158
+ if (magic === MAGIC) {
159
+ const version = Atomics.load(header, IDX_VERSION);
160
+ if (version !== VERSION) throw new Error(`Unsupported channel version ${version}`);
161
+ return;
162
+ }
163
+ if (magic === INIT_SENTINEL) {
164
+ Atomics.wait(header, IDX_MAGIC, INIT_SENTINEL);
165
+ continue;
166
+ }
167
+ if (magic !== 0) throw new Error('SharedArrayBuffer does not contain a supported channel');
168
+ if (Atomics.compareExchange(header, IDX_MAGIC, 0, INIT_SENTINEL) !== 0) continue;
169
+ finalizeHeader(buffer, header);
170
+ return;
162
171
  }
163
- const header = new Int32Array(buffer);
172
+ };
164
173
 
165
- const handshakeResult: WaitResponse = yield {
166
- target: header,
167
- index: SEMAPHORE,
168
- value: Semaphore.READY,
169
- timeout,
170
- };
171
- if (handshakeResult === 'timed-out') {
172
- throw new Error('Handshake timeout');
174
+ const resetClosedHeaderSync = (buffer: SharedArrayBuffer, header: Int32Array): void => {
175
+ for (;;) {
176
+ const magic = Atomics.load(header, IDX_MAGIC);
177
+ if (magic === INIT_SENTINEL) {
178
+ Atomics.wait(header, IDX_MAGIC, INIT_SENTINEL);
179
+ continue;
180
+ }
181
+ if (magic !== MAGIC) return;
182
+ const version = Atomics.load(header, IDX_VERSION);
183
+ if (version !== VERSION) throw new Error(`Unsupported channel version ${version}`);
184
+ if (Atomics.load(header, IDX_OWNER_A) !== 0 || Atomics.load(header, IDX_OWNER_B) !== 0 || Atomics.load(header, IDX_FLAGS) === 0) return;
185
+ if (Atomics.compareExchange(header, IDX_MAGIC, MAGIC, INIT_SENTINEL) !== MAGIC) continue;
186
+ finalizeHeader(buffer, header);
187
+ return;
173
188
  }
174
- if (header[SEMAPHORE] !== Semaphore.HANDSHAKE) {
175
- throw new Error('Invalid handshake state');
189
+ };
190
+
191
+ const resetHeaderSync = (buffer: SharedArrayBuffer, header: Int32Array): void => {
192
+ initializeHeaderSync(buffer, header);
193
+ for (;;) {
194
+ if (Atomics.load(header, IDX_OWNER_A) !== 0 || Atomics.load(header, IDX_OWNER_B) !== 0) {
195
+ throw new Error('Channel is still open');
196
+ }
197
+ if (Atomics.compareExchange(header, IDX_MAGIC, MAGIC, INIT_SENTINEL) !== MAGIC) {
198
+ initializeHeaderSync(buffer, header);
199
+ continue;
200
+ }
201
+ finalizeHeader(buffer, header);
202
+ return;
176
203
  }
204
+ };
177
205
 
178
- const totalSize = header[Handshake.TOTAL_SIZE];
179
- const totalChunks = header[Handshake.TOTAL_CHUNKS];
180
- if (totalSize < 0 || totalChunks < 0) {
181
- throw new Error('Invalid handshake values');
206
+ const claimEndpoint = (header: Int32Array): 0 | 1 => {
207
+ const ownerA = Atomics.load(header, IDX_OWNER_A);
208
+ if (ownerA === ENDPOINT_TOKEN) {
209
+ Atomics.and(header, IDX_FLAGS, ~FLAG_CLOSED_A);
210
+ return 0;
182
211
  }
183
- if (totalSize === 0 && totalChunks !== 0) {
184
- throw new Error('Invalid handshake values');
212
+ if (ownerA === 0 && Atomics.compareExchange(header, IDX_OWNER_A, 0, ENDPOINT_TOKEN) === 0) {
213
+ Atomics.and(header, IDX_FLAGS, ~FLAG_CLOSED_A);
214
+ return 0;
185
215
  }
186
- if (totalSize > totalChunks * chunkSize) {
187
- throw new Error('Invalid handshake values');
216
+ const ownerB = Atomics.load(header, IDX_OWNER_B);
217
+ if (ownerB === ENDPOINT_TOKEN) {
218
+ Atomics.and(header, IDX_FLAGS, ~FLAG_CLOSED_B);
219
+ return 1;
188
220
  }
189
- const data = new Uint8Array(totalSize);
190
-
191
- Atomics.store(header, SEMAPHORE, Semaphore.READY);
192
- Atomics.notify(header, SEMAPHORE);
193
-
194
- const payload = new Uint8Array(buffer, HEADER_SIZE);
195
- for (let i = 0; i < totalChunks; i++) {
196
- const chunkResult: WaitResponse = yield {
197
- target: header,
198
- index: SEMAPHORE,
199
- value: Semaphore.READY,
200
- timeout,
201
- };
202
- if (chunkResult === 'timed-out') {
203
- throw new Error(`Writer timeout waiting for chunk ${i}`);
204
- }
205
- // @ts-expect-error does not infer number
206
- if (header[SEMAPHORE] !== Semaphore.PAYLOAD) {
207
- throw new Error(`Expected payload header, received ${Semaphore[header[SEMAPHORE]]}`);
221
+ if (ownerB === 0 && Atomics.compareExchange(header, IDX_OWNER_B, 0, ENDPOINT_TOKEN) === 0) {
222
+ Atomics.and(header, IDX_FLAGS, ~FLAG_CLOSED_B);
223
+ return 1;
224
+ }
225
+ throw new Error('SharedArrayBuffer channel already claimed by two endpoints');
226
+ };
227
+
228
+ const createState = (buffer: SharedArrayBuffer, layout: Layout): State => {
229
+ const cached = stateCache.get(buffer);
230
+ if (cached !== undefined) return cached;
231
+
232
+ const byteOffset = HEADER_WORDS * Int32Array.BYTES_PER_ELEMENT;
233
+ const directionA = createRingViews(buffer, byteOffset, layout);
234
+ const directionB = createRingViews(buffer, byteOffset + layout.perDirectionBytes, layout);
235
+ const header = new Int32Array(buffer, 0, HEADER_WORDS);
236
+ const endpoint = claimEndpoint(header);
237
+
238
+ const outbound = endpoint === 0 ? directionA : directionB;
239
+ const inbound = endpoint === 0 ? directionB : directionA;
240
+
241
+ const state: State = {
242
+ header,
243
+ inbound,
244
+ outbound,
245
+ ownerIndex: endpoint === 0 ? IDX_OWNER_A : IDX_OWNER_B,
246
+ localClosedMask: endpoint === 0 ? FLAG_CLOSED_A : FLAG_CLOSED_B,
247
+ peerClosedMask: endpoint === 0 ? FLAG_CLOSED_B : FLAG_CLOSED_A,
248
+ localClosed: false,
249
+ writeLocked: false,
250
+ readLocked: false,
251
+ readTail: resolvedTail,
252
+ writeTail: resolvedTail,
253
+ wMsg: Atomics.load(outbound.control, IDX_WRITE_MSG),
254
+ wByte: 0,
255
+ rMsg: Atomics.load(inbound.control, IDX_READ_MSG),
256
+ rByte: 0,
257
+ pendingAckMsg: 0,
258
+ pendingAckByte: 0,
259
+ pendingAckSize: 0,
260
+ };
261
+
262
+ stateCache.set(buffer, state);
263
+ return state;
264
+ };
265
+
266
+ const getStateSync = (buffer: SharedArrayBuffer): State => {
267
+ const cached = stateCache.get(buffer);
268
+ if (cached !== undefined) return cached;
269
+
270
+ const layout = computeLayout(buffer.byteLength);
271
+ if (layout === null) throw new Error(`SharedArrayBuffer too small for channel (minimum ${MIN_BUFFER_BYTES} bytes)`);
272
+
273
+ const header = new Int32Array(buffer, 0, HEADER_WORDS);
274
+ initializeHeaderSync(buffer, header);
275
+ resetClosedHeaderSync(buffer, header);
276
+ return createState(buffer, layout);
277
+ };
278
+
279
+ const copyToRing = (ring: Uint8Array, ringOffset: number, source: Uint8Array, sourceOffset: number, size: number): void => {
280
+ const first = Math.min(size, ring.byteLength - ringOffset);
281
+ ring.set(source.subarray(sourceOffset, sourceOffset + first), ringOffset);
282
+ if (first < size) ring.set(source.subarray(sourceOffset + first, sourceOffset + size), 0);
283
+ };
284
+
285
+ const copyFromRing = (target: Uint8Array, targetOffset: number, ring: Uint8Array, ringOffset: number, size: number): void => {
286
+ const first = Math.min(size, ring.byteLength - ringOffset);
287
+ target.set(ring.subarray(ringOffset, ringOffset + first), targetOffset);
288
+ if (first < size) target.set(ring.subarray(0, size - first), targetOffset + first);
289
+ };
290
+
291
+ const freeWriteSpace = (ring: RingViews, writeMsg: number, writeByte: number): number => {
292
+ const readMsg = Atomics.load(ring.control, IDX_READ_MSG);
293
+ const pending = (writeMsg - readMsg) | 0;
294
+ if (pending >= ring.ringSlots) return 0;
295
+ if (pending === 0) return ring.dataBytes;
296
+ const readByte = Atomics.load(ring.control, IDX_READ_BYTE);
297
+ const free = ring.dataBytes - ((writeByte - readByte) | 0);
298
+ return free > 0 ? free : 0;
299
+ };
300
+
301
+ const publishSegment = (ring: RingViews, offset: number, size: number, total: number, writeMsg: number): void => {
302
+ const di = (writeMsg & ring.ringMask) * DESCRIPTOR_WORDS;
303
+ ring.descriptors[di + IDX_DESC_OFFSET] = offset;
304
+ ring.descriptors[di + IDX_DESC_SIZE] = size;
305
+ ring.descriptors[di + IDX_DESC_TOTAL] = total;
306
+ Atomics.store(ring.control, IDX_WRITE_MSG, (writeMsg + 1) | 0);
307
+ Atomics.notify(ring.control, IDX_WRITE_MSG, 1);
308
+ };
309
+
310
+ const ackSegment = (ring: RingViews, readMsg: number, readByte: number, size: number): void => {
311
+ Atomics.store(ring.control, IDX_READ_BYTE, (readByte + size) | 0);
312
+ Atomics.store(ring.control, IDX_READ_MSG, (readMsg + 1) | 0);
313
+ Atomics.notify(ring.control, IDX_READ_MSG, 1);
314
+ };
315
+
316
+ const flushPendingAck = (state: State): void => {
317
+ if (state.pendingAckSize > 0) {
318
+ ackSegment(state.inbound, state.pendingAckMsg, state.pendingAckByte, state.pendingAckSize);
319
+ state.pendingAckSize = 0;
320
+ }
321
+ };
322
+
323
+ const isPeerClosed = (state: State): boolean => (Atomics.load(state.header, IDX_FLAGS) & state.peerClosedMask) !== 0;
324
+
325
+ const assertOpen = (state: State): void => {
326
+ if (state.localClosed) throw new Error('Channel is closed');
327
+ };
328
+
329
+ const assertWritable = (state: State): void => {
330
+ assertOpen(state);
331
+ if (isPeerClosed(state)) throw new Error('Peer channel is closed');
332
+ };
333
+
334
+ const assertReadable = (state: State, receiving: boolean): void => {
335
+ assertOpen(state);
336
+ if (isPeerClosed(state)) throw new Error(receiving ? 'Peer channel closed while receiving message' : 'Peer channel is closed');
337
+ };
338
+
339
+ const notifyWaiters = (state: State): void => {
340
+ Atomics.notify(state.inbound.control, IDX_WRITE_MSG);
341
+ Atomics.notify(state.inbound.control, IDX_READ_MSG);
342
+ Atomics.notify(state.outbound.control, IDX_WRITE_MSG);
343
+ Atomics.notify(state.outbound.control, IDX_READ_MSG);
344
+ };
345
+
346
+ const writeChannelSync = (data: Uint8Array, state: State, timeout: number): void => {
347
+ if (state.writeLocked) throw new Error('Cannot writeSync while an async write is in progress');
348
+ flushPendingAck(state);
349
+ const ring = state.outbound;
350
+ assertWritable(state);
351
+ let writeMsg = state.wMsg;
352
+ let writeByte = state.wByte;
353
+
354
+ if (data.length === 0) {
355
+ while (freeWriteSpace(ring, writeMsg, writeByte) === 0) {
356
+ assertWritable(state);
357
+ const readMsg = Atomics.load(ring.control, IDX_READ_MSG);
358
+ if (Atomics.wait(ring.control, IDX_READ_MSG, readMsg, timeout) === 'timed-out') throw new Error('Write timeout waiting for channel space');
208
359
  }
209
- const chunkIndex = header[Header.CHUNK_INDEX];
210
- if (i !== chunkIndex) {
211
- throw new Error(`Reader integrity failure for chunk ${chunkIndex} expected ${i}`);
360
+ publishSegment(ring, (writeByte >>> 0) % ring.dataBytes, 0, 0, writeMsg);
361
+ state.wMsg = (writeMsg + 1) | 0;
362
+ return;
363
+ }
364
+
365
+ let sourceOffset = 0;
366
+ let firstSegment = true;
367
+ while (sourceOffset < data.length) {
368
+ let free = freeWriteSpace(ring, writeMsg, writeByte);
369
+ while (free === 0) {
370
+ assertWritable(state);
371
+ const readMsg = Atomics.load(ring.control, IDX_READ_MSG);
372
+ if (Atomics.wait(ring.control, IDX_READ_MSG, readMsg, timeout) === 'timed-out') throw new Error('Write timeout waiting for channel space');
373
+ free = freeWriteSpace(ring, writeMsg, writeByte);
212
374
  }
213
- const offset = header[Header.CHUNK_OFFSET];
214
- const size = header[Header.CHUNK_SIZE];
215
- if (offset < 0 || size <= 0 || size > chunkSize || offset + size > totalSize) {
216
- throw new Error(`Invalid chunk metadata for chunk ${chunkIndex}`);
375
+ const size = Math.min(data.length - sourceOffset, free);
376
+ const ringOffset = (writeByte >>> 0) % ring.dataBytes;
377
+ copyToRing(ring.bytes, ringOffset, data, sourceOffset, size);
378
+ publishSegment(ring, ringOffset, size, firstSegment ? data.length : CONTINUATION, writeMsg);
379
+ writeMsg = (writeMsg + 1) | 0;
380
+ writeByte = (writeByte + size) | 0;
381
+ state.wMsg = writeMsg;
382
+ state.wByte = writeByte;
383
+ sourceOffset += size;
384
+ firstSegment = false;
385
+ }
386
+ };
387
+
388
+ const writeChannel = async (data: Uint8Array, state: State, timeout: number): Promise<void> => {
389
+ flushPendingAck(state);
390
+ const ring = state.outbound;
391
+ assertWritable(state);
392
+ let writeMsg = state.wMsg;
393
+ let writeByte = state.wByte;
394
+
395
+ if (data.length === 0) {
396
+ while (freeWriteSpace(ring, writeMsg, writeByte) === 0) {
397
+ assertWritable(state);
398
+ const readMsg = Atomics.load(ring.control, IDX_READ_MSG);
399
+ if ((await Atomics.waitAsync(ring.control, IDX_READ_MSG, readMsg, timeout).value) === 'timed-out')
400
+ throw new Error('Write timeout waiting for channel space');
217
401
  }
218
- data.set(payload.subarray(0, size), offset);
219
- Atomics.store(header, SEMAPHORE, Semaphore.READY);
220
- Atomics.notify(header, SEMAPHORE);
402
+ publishSegment(ring, (writeByte >>> 0) % ring.dataBytes, 0, 0, writeMsg);
403
+ state.wMsg = (writeMsg + 1) | 0;
404
+ return;
221
405
  }
222
- return data;
223
- }
224
406
 
225
- /**
226
- * Synchronously writes data to a SharedArrayBuffer for cross-thread transfer.
227
- * Blocks until the reader receives all data. Use in workers where blocking is acceptable.
228
- * @param data - Bytes to send (can be larger than buffer, will be chunked automatically)
229
- * @param buffer - SharedArrayBuffer shared with the reader thread (byteLength must be multiple of 4 and larger than HEADER_SIZE)
230
- * @param options - Optional configuration
231
- * @throws {Error} "SharedArrayBuffer byteLength must be a multiple of 4"
232
- * @throws {Error} "SharedArrayBuffer too small for header"
233
- * @throws {Error} "Reader handshake timeout" - reader didn't respond in time
234
- * @throws {Error} "Reader timeout on chunk N/M" - reader stopped responding mid-transfer
235
- * @example
236
- * ```typescript
237
- * import { workerData } from 'worker_threads';
238
- * import { writeSync } from 'sabcom';
239
- *
240
- * const buffer = workerData as SharedArrayBuffer;
241
- * const data = new TextEncoder().encode('Hello from worker');
242
- * writeSync(data, buffer);
243
- * ```
244
- */
245
- export const writeSync = (data: Uint8Array, buffer: SharedArrayBuffer, options?: Options): void => {
246
- const gen = writeGenerator(data, buffer, options);
247
- let result = gen.next();
248
- while (!result.done) {
249
- const waitResult = Atomics.wait(result.value.target, result.value.index, result.value.value, result.value.timeout);
250
- result = gen.next(waitResult);
407
+ let sourceOffset = 0;
408
+ let firstSegment = true;
409
+ while (sourceOffset < data.length) {
410
+ let free = freeWriteSpace(ring, writeMsg, writeByte);
411
+ while (free === 0) {
412
+ assertWritable(state);
413
+ const readMsg = Atomics.load(ring.control, IDX_READ_MSG);
414
+ if ((await Atomics.waitAsync(ring.control, IDX_READ_MSG, readMsg, timeout).value) === 'timed-out')
415
+ throw new Error('Write timeout waiting for channel space');
416
+ free = freeWriteSpace(ring, writeMsg, writeByte);
417
+ }
418
+ const size = Math.min(data.length - sourceOffset, free);
419
+ const ringOffset = (writeByte >>> 0) % ring.dataBytes;
420
+ copyToRing(ring.bytes, ringOffset, data, sourceOffset, size);
421
+ publishSegment(ring, ringOffset, size, firstSegment ? data.length : CONTINUATION, writeMsg);
422
+ writeMsg = (writeMsg + 1) | 0;
423
+ writeByte = (writeByte + size) | 0;
424
+ state.wMsg = writeMsg;
425
+ state.wByte = writeByte;
426
+ sourceOffset += size;
427
+ firstSegment = false;
251
428
  }
252
429
  };
253
430
 
254
- /**
255
- * Asynchronously writes data to a SharedArrayBuffer for cross-thread transfer.
256
- * Non-blocking, suitable for main thread. Resolves when the reader receives all data.
257
- * @param data - Bytes to send (can be larger than buffer, will be chunked automatically)
258
- * @param buffer - SharedArrayBuffer shared with the reader thread (byteLength must be multiple of 4 and larger than HEADER_SIZE)
259
- * @param options - Optional configuration
260
- * @throws {Error} "SharedArrayBuffer byteLength must be a multiple of 4"
261
- * @throws {Error} "SharedArrayBuffer too small for header"
262
- * @throws {Error} "Reader handshake timeout" - reader didn't respond in time
263
- * @throws {Error} "Reader timeout on chunk N/M" - reader stopped responding mid-transfer
264
- * @example
265
- * ```typescript
266
- * import { Worker } from 'worker_threads';
267
- * import { write } from 'sabcom';
268
- *
269
- * const buffer = new SharedArrayBuffer(4096);
270
- * const worker = new Worker('./worker.js', { workerData: buffer });
271
- * const data = new TextEncoder().encode('Hello from main');
272
- * await write(data, buffer);
273
- * ```
274
- */
275
- export const write = async (data: Uint8Array, buffer: SharedArrayBuffer, options?: Options): Promise<void> => {
276
- const gen = writeGenerator(data, buffer, options);
277
- let result = gen.next();
278
- while (!result.done) {
279
- const request = result.value;
280
- const waitResult = await Atomics.waitAsync(request.target, request.index, request.value, request.timeout).value;
281
- result = gen.next(waitResult);
431
+ const readChannelSync = (state: State, timeout: number): Uint8Array => {
432
+ if (state.readLocked) throw new Error('Cannot readSync while an async read is in progress');
433
+ const ring = state.inbound;
434
+
435
+ flushPendingAck(state);
436
+
437
+ let readMsg = state.rMsg;
438
+ let readByte = state.rByte;
439
+
440
+ while (readMsg === Atomics.load(ring.control, IDX_WRITE_MSG)) {
441
+ assertReadable(state, false);
442
+ if (Atomics.wait(ring.control, IDX_WRITE_MSG, readMsg, timeout) === 'timed-out') throw new Error('Read timeout');
282
443
  }
444
+
445
+ const di = (readMsg & ring.ringMask) * DESCRIPTOR_WORDS;
446
+ const offset = ring.descriptors[di + IDX_DESC_OFFSET];
447
+ const size = ring.descriptors[di + IDX_DESC_SIZE];
448
+ const total = ring.descriptors[di + IDX_DESC_TOTAL];
449
+
450
+ if (offset < 0 || offset >= ring.dataBytes || size < 0 || size > ring.dataBytes) throw new Error('Invalid descriptor');
451
+ if (total < 0) throw new Error('Invalid descriptor');
452
+
453
+ // Single-segment, contiguous in ring: zero-copy with deferred ack
454
+ if (size === total && offset + size <= ring.dataBytes) {
455
+ state.pendingAckMsg = readMsg;
456
+ state.pendingAckByte = readByte;
457
+ state.pendingAckSize = size;
458
+ state.rMsg = (readMsg + 1) | 0;
459
+ state.rByte = (readByte + size) | 0;
460
+ if (size === 0) return new Uint8Array(0);
461
+ return new Uint8Array(ring.bytes.buffer, ring.bytes.byteOffset + offset, size);
462
+ }
463
+
464
+ // Multi-segment or wrapping: allocate + copy + immediate ack
465
+ const data = new Uint8Array(total);
466
+ const targetSize = total;
467
+ let targetOffset = 0;
468
+
469
+ if (targetOffset + size > targetSize) throw new Error('Invalid descriptor');
470
+ if (size > 0) copyFromRing(data, targetOffset, ring.bytes, offset, size);
471
+ targetOffset += size;
472
+ ackSegment(ring, readMsg, readByte, size);
473
+ readMsg = (readMsg + 1) | 0;
474
+ readByte = (readByte + size) | 0;
475
+ state.rMsg = readMsg;
476
+ state.rByte = readByte;
477
+
478
+ while (targetOffset < targetSize) {
479
+ while (readMsg === Atomics.load(ring.control, IDX_WRITE_MSG)) {
480
+ assertReadable(state, true);
481
+ if (Atomics.wait(ring.control, IDX_WRITE_MSG, readMsg, timeout) === 'timed-out') throw new Error('Read timeout waiting for segment');
482
+ }
483
+
484
+ const di2 = (readMsg & ring.ringMask) * DESCRIPTOR_WORDS;
485
+ const off2 = ring.descriptors[di2 + IDX_DESC_OFFSET];
486
+ const sz2 = ring.descriptors[di2 + IDX_DESC_SIZE];
487
+ const tot2 = ring.descriptors[di2 + IDX_DESC_TOTAL];
488
+
489
+ if (off2 < 0 || off2 >= ring.dataBytes || sz2 < 0 || sz2 > ring.dataBytes) throw new Error('Invalid descriptor');
490
+ if (tot2 !== CONTINUATION) throw new Error('Invalid descriptor');
491
+ if (targetOffset + sz2 > targetSize) throw new Error('Invalid descriptor');
492
+
493
+ if (sz2 > 0) copyFromRing(data, targetOffset, ring.bytes, off2, sz2);
494
+ targetOffset += sz2;
495
+ ackSegment(ring, readMsg, readByte, sz2);
496
+ readMsg = (readMsg + 1) | 0;
497
+ readByte = (readByte + sz2) | 0;
498
+ state.rMsg = readMsg;
499
+ state.rByte = readByte;
500
+ }
501
+
502
+ return data;
283
503
  };
284
504
 
285
- /**
286
- * Synchronously reads data from a SharedArrayBuffer written by another thread.
287
- * Blocks until all data is received. Use in workers where blocking is acceptable.
288
- * @param buffer - SharedArrayBuffer shared with the writer thread (byteLength must be multiple of 4 and larger than HEADER_SIZE)
289
- * @param options - Optional configuration
290
- * @returns Complete data as Uint8Array
291
- * @throws {Error} "SharedArrayBuffer byteLength must be a multiple of 4"
292
- * @throws {Error} "SharedArrayBuffer too small for header"
293
- * @throws {Error} "Handshake timeout" - writer didn't send data in time
294
- * @throws {Error} "Invalid handshake state" - protocol error
295
- * @throws {Error} "Writer timeout waiting for chunk N" - writer stopped responding mid-transfer
296
- * @example
297
- * ```typescript
298
- * import { workerData } from 'worker_threads';
299
- * import { readSync } from 'sabcom';
300
- *
301
- * const buffer = workerData as SharedArrayBuffer;
302
- * const data = readSync(buffer);
303
- * const message = new TextDecoder().decode(data);
304
- * ```
305
- */
306
- export const readSync = (buffer: SharedArrayBuffer, options?: Options): Uint8Array => {
307
- const gen = readGenerator(buffer, options);
308
- let result = gen.next();
309
- while (!result.done) {
310
- const waitResult = Atomics.wait(result.value.target, result.value.index, result.value.value, result.value.timeout);
311
- result = gen.next(waitResult);
505
+ const readChannel = async (state: State, timeout: number): Promise<Uint8Array> => {
506
+ const ring = state.inbound;
507
+
508
+ flushPendingAck(state);
509
+
510
+ let readMsg = state.rMsg;
511
+ let readByte = state.rByte;
512
+
513
+ while (readMsg === Atomics.load(ring.control, IDX_WRITE_MSG)) {
514
+ assertReadable(state, false);
515
+ if ((await Atomics.waitAsync(ring.control, IDX_WRITE_MSG, readMsg, timeout).value) === 'timed-out') throw new Error('Read timeout');
312
516
  }
313
- return result.value;
517
+
518
+ const di = (readMsg & ring.ringMask) * DESCRIPTOR_WORDS;
519
+ const offset = ring.descriptors[di + IDX_DESC_OFFSET];
520
+ const size = ring.descriptors[di + IDX_DESC_SIZE];
521
+ const total = ring.descriptors[di + IDX_DESC_TOTAL];
522
+
523
+ if (offset < 0 || offset >= ring.dataBytes || size < 0 || size > ring.dataBytes) throw new Error('Invalid descriptor');
524
+ if (total < 0) throw new Error('Invalid descriptor');
525
+
526
+ if (size === total && offset + size <= ring.dataBytes) {
527
+ state.pendingAckMsg = readMsg;
528
+ state.pendingAckByte = readByte;
529
+ state.pendingAckSize = size;
530
+ state.rMsg = (readMsg + 1) | 0;
531
+ state.rByte = (readByte + size) | 0;
532
+ if (size === 0) return new Uint8Array(0);
533
+ return new Uint8Array(ring.bytes.buffer, ring.bytes.byteOffset + offset, size);
534
+ }
535
+
536
+ const data = new Uint8Array(total);
537
+ const targetSize = total;
538
+ let targetOffset = 0;
539
+
540
+ if (targetOffset + size > targetSize) throw new Error('Invalid descriptor');
541
+ if (size > 0) copyFromRing(data, targetOffset, ring.bytes, offset, size);
542
+ targetOffset += size;
543
+ ackSegment(ring, readMsg, readByte, size);
544
+ readMsg = (readMsg + 1) | 0;
545
+ readByte = (readByte + size) | 0;
546
+ state.rMsg = readMsg;
547
+ state.rByte = readByte;
548
+
549
+ while (targetOffset < targetSize) {
550
+ while (readMsg === Atomics.load(ring.control, IDX_WRITE_MSG)) {
551
+ assertReadable(state, true);
552
+ if ((await Atomics.waitAsync(ring.control, IDX_WRITE_MSG, readMsg, timeout).value) === 'timed-out') throw new Error('Read timeout waiting for segment');
553
+ }
554
+
555
+ const di2 = (readMsg & ring.ringMask) * DESCRIPTOR_WORDS;
556
+ const off2 = ring.descriptors[di2 + IDX_DESC_OFFSET];
557
+ const sz2 = ring.descriptors[di2 + IDX_DESC_SIZE];
558
+ const tot2 = ring.descriptors[di2 + IDX_DESC_TOTAL];
559
+
560
+ if (off2 < 0 || off2 >= ring.dataBytes || sz2 < 0 || sz2 > ring.dataBytes) throw new Error('Invalid descriptor');
561
+ if (tot2 !== CONTINUATION) throw new Error('Invalid descriptor');
562
+ if (targetOffset + sz2 > targetSize) throw new Error('Invalid descriptor');
563
+
564
+ if (sz2 > 0) copyFromRing(data, targetOffset, ring.bytes, off2, sz2);
565
+ targetOffset += sz2;
566
+ ackSegment(ring, readMsg, readByte, sz2);
567
+ readMsg = (readMsg + 1) | 0;
568
+ readByte = (readByte + sz2) | 0;
569
+ state.rMsg = readMsg;
570
+ state.rByte = readByte;
571
+ }
572
+
573
+ return data;
314
574
  };
315
575
 
316
- /**
317
- * Asynchronously reads data from a SharedArrayBuffer written by another thread.
318
- * Non-blocking, suitable for main thread. Resolves when all data is received.
319
- * @param buffer - SharedArrayBuffer shared with the writer thread (byteLength must be multiple of 4 and larger than HEADER_SIZE)
320
- * @param options - Optional configuration
321
- * @returns Complete data as Uint8Array
322
- * @throws {Error} "SharedArrayBuffer byteLength must be a multiple of 4"
323
- * @throws {Error} "SharedArrayBuffer too small for header"
324
- * @throws {Error} "Handshake timeout" - writer didn't send data in time
325
- * @throws {Error} "Invalid handshake state" - protocol error
326
- * @throws {Error} "Writer timeout waiting for chunk N" - writer stopped responding mid-transfer
327
- * @example
328
- * ```typescript
329
- * import { Worker } from 'worker_threads';
330
- * import { read } from 'sabcom';
331
- *
332
- * const buffer = new SharedArrayBuffer(4096);
333
- * const worker = new Worker('./worker.js', { workerData: buffer });
334
- * // Worker writes data...
335
- * const data = await read(buffer);
336
- * const message = new TextDecoder().decode(data);
337
- * ```
338
- */
339
- export const read = async (buffer: SharedArrayBuffer, options?: Options): Promise<Uint8Array> => {
340
- const gen = readGenerator(buffer, options);
341
- let result = gen.next();
342
- while (!result.done) {
343
- const request = result.value;
344
- const waitResult = await Atomics.waitAsync(request.target, request.index, request.value, request.timeout).value;
345
- result = gen.next(waitResult);
576
+ const runExclusive = async <T>(state: State, key: 'readTail' | 'writeTail', task: () => Promise<T>): Promise<T> => {
577
+ const previous = state[key];
578
+ let release!: () => void;
579
+ const next = new Promise<void>((resolve) => {
580
+ release = resolve;
581
+ });
582
+ state[key] = previous.then(
583
+ () => next,
584
+ () => next,
585
+ );
586
+ await previous;
587
+ try {
588
+ return await task();
589
+ } finally {
590
+ release();
346
591
  }
347
- return result.value;
592
+ };
593
+
594
+ const closeState = (buffer: SharedArrayBuffer, state: State): void => {
595
+ if (state.localClosed) return;
596
+ flushPendingAck(state);
597
+ state.localClosed = true;
598
+ Atomics.or(state.header, IDX_FLAGS, state.localClosedMask);
599
+ Atomics.compareExchange(state.header, state.ownerIndex, ENDPOINT_TOKEN, 0);
600
+ notifyWaiters(state);
601
+ handleCache.delete(buffer);
602
+ stateCache.delete(buffer);
603
+ };
604
+
605
+ export const createBuffer = (byteLength: number): SharedArrayBuffer => {
606
+ if (byteLength % Int32Array.BYTES_PER_ELEMENT !== 0) throw new Error('SharedArrayBuffer byteLength must be a multiple of 4');
607
+ const buffer = new SharedArrayBuffer(byteLength);
608
+ if (computeLayout(buffer.byteLength) === null) throw new Error(`SharedArrayBuffer too small for channel (minimum ${MIN_BUFFER_BYTES} bytes)`);
609
+ initializeHeaderSync(buffer, new Int32Array(buffer, 0, HEADER_WORDS));
610
+ return buffer;
611
+ };
612
+
613
+ export const open = (buffer: SharedArrayBuffer): Channel => {
614
+ const cached = handleCache.get(buffer);
615
+ if (cached !== undefined) return cached;
616
+
617
+ if (buffer.byteLength % Int32Array.BYTES_PER_ELEMENT !== 0) throw new Error('SharedArrayBuffer byteLength must be a multiple of 4');
618
+
619
+ const state = getStateSync(buffer);
620
+
621
+ const channel: Channel = {
622
+ write: (data, options) =>
623
+ runExclusive(state, 'writeTail', async () => {
624
+ state.writeLocked = true;
625
+ try {
626
+ await writeChannel(data, state, options?.timeout ?? 5000);
627
+ } finally {
628
+ state.writeLocked = false;
629
+ }
630
+ }),
631
+ read: (options) =>
632
+ runExclusive(state, 'readTail', async () => {
633
+ state.readLocked = true;
634
+ try {
635
+ return await readChannel(state, options?.timeout ?? 5000);
636
+ } finally {
637
+ state.readLocked = false;
638
+ }
639
+ }),
640
+ writeSync: (data, options) => writeChannelSync(data, state, options?.timeout ?? 5000),
641
+ readSync: (options) => readChannelSync(state, options?.timeout ?? 5000),
642
+ close: () => closeState(buffer, state),
643
+ };
644
+
645
+ handleCache.set(buffer, channel);
646
+ return channel;
647
+ };
648
+
649
+ export const reset = (buffer: SharedArrayBuffer): void => {
650
+ if (buffer.byteLength % Int32Array.BYTES_PER_ELEMENT !== 0) throw new Error('SharedArrayBuffer byteLength must be a multiple of 4');
651
+ if (computeLayout(buffer.byteLength) === null) throw new Error(`SharedArrayBuffer too small for channel (minimum ${MIN_BUFFER_BYTES} bytes)`);
652
+ const cached = stateCache.get(buffer);
653
+ if (cached !== undefined && !cached.localClosed) throw new Error('Channel is still open in this runtime');
654
+ resetHeaderSync(buffer, new Int32Array(buffer, 0, HEADER_WORDS));
655
+ handleCache.delete(buffer);
656
+ stateCache.delete(buffer);
348
657
  };