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/README.md +108 -264
- package/build/index.cjs +526 -193
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +9 -176
- package/build/index.js +521 -161
- package/build/index.js.map +1 -1
- package/package.json +7 -3
- package/src/index.ts +614 -305
package/src/index.ts
CHANGED
|
@@ -1,348 +1,657 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
export enum Semaphore {
|
|
4
|
-
READY,
|
|
5
|
-
HANDSHAKE,
|
|
6
|
-
PAYLOAD,
|
|
1
|
+
export interface Options {
|
|
2
|
+
timeout?: number;
|
|
7
3
|
}
|
|
8
4
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
172
|
+
};
|
|
164
173
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
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 (
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
};
|