space-data-module-sdk 0.8.21 → 0.8.23
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/docs/isomorphic-pthreads.html +88 -0
- package/docs/isomorphic-pthreads.md +290 -0
- package/package.json +7 -2
- package/src/browser.js +6 -0
- package/src/compiler/compileModule.js +11 -2
- package/src/flow/flatsqlLinkShim.js +295 -0
- package/src/flow/index.d.ts +31 -0
- package/src/flow/index.js +8 -0
- package/src/host/browserCapabilityProbe.js +251 -0
- package/src/host/flatsqlIo.js +14 -0
- package/src/host/flatsqlIoConformance.js +349 -0
- package/src/host/flatsqlIoContract.js +240 -0
- package/src/host/flatsqlIoImports.js +335 -0
- package/src/host/flatsqlIoMemoryBackend.js +178 -0
- package/src/host/flatsqlIoServer.js +960 -0
- package/src/host/flatsqlIoWorkers.js +323 -0
- package/src/host/hostWorkerBundleSources.js +11 -0
- package/src/host/hostWorkerBundles.js +79 -0
- package/src/host/index.js +9 -0
- package/src/host/nodeSyncFsIo.js +633 -0
- package/src/host/opfsIoBackend.js +280 -0
- package/src/host/opfsIoWorker.mjs +223 -0
- package/src/host/sabIoChannel.js +848 -0
- package/src/host/sabIoMirror.js +306 -0
- package/src/host/wasiThreadBrowserWorker.mjs +18 -11
- package/src/host/wasiThreadHost.js +258 -41
- package/src/host/wasiThreadWorker.mjs +24 -8
- package/src/host/wasiThreadWorkerRuntime.js +54 -0
- package/src/index.d.ts +347 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SharedArrayBuffer I/O request ring: the multi-slot extension of
|
|
3
|
+
* sabHostcallChannel.js for FlatSQL's seven `flatsql_io_*` imports
|
|
4
|
+
* (docs/architecture/flatsql-partition-store.md §5.5, A7, A36, A38).
|
|
5
|
+
*
|
|
6
|
+
* sabHostcallChannel.js serves ONE blocking guest against an async host, one
|
|
7
|
+
* request at a time, over postMessage. This channel serves MANY guest threads
|
|
8
|
+
* at once against one I/O worker, which holds every OPFS handle and never
|
|
9
|
+
* blocks:
|
|
10
|
+
*
|
|
11
|
+
* guest thread (pool worker) I/O worker (flatsqlIoServer.js)
|
|
12
|
+
* ────────────────────────── ──────────────────────────────
|
|
13
|
+
* claim a slot once (CAS FREE -> IDLE)
|
|
14
|
+
* write {op, handle, ptr, len, offset, flags}
|
|
15
|
+
* state <- PENDING; doorbell += 1; notify ───▶ waitAsync(doorbell) or message
|
|
16
|
+
* scan slots: PENDING -> SERVICING
|
|
17
|
+
* sync ops run at once, async opens
|
|
18
|
+
* are awaited without blocking others
|
|
19
|
+
* Atomics.wait(state) in bounded slices ◀─── status; state <- DONE; notify(state)
|
|
20
|
+
* state <- IDLE
|
|
21
|
+
*
|
|
22
|
+
* Properties the design requires:
|
|
23
|
+
* - Each request owns its slot until its result is read, so a guest thread
|
|
24
|
+
* never queues behind another guest's request; a slow open blocks only its
|
|
25
|
+
* caller. Size the ring to the guest threads plus async callers.
|
|
26
|
+
* - No timeout and no throw (A36). A client waits in bounded slices forever;
|
|
27
|
+
* a dead I/O worker is handled by its supervisor, which completes pending
|
|
28
|
+
* slots with an error status (failPendingSabIoRequests).
|
|
29
|
+
* - Doorbell: `Atomics.waitAsync` where the server has it; otherwise the
|
|
30
|
+
* client also posts a message on a BroadcastChannel (22.3a-5). The mode is
|
|
31
|
+
* chosen by the server and published in the header.
|
|
32
|
+
* - Data moves directly between the guest's shared memory and the file when
|
|
33
|
+
* the instance's memory is attached to the I/O worker; otherwise through
|
|
34
|
+
* the slot's own data area ("scratch"). Both paths give the same bytes.
|
|
35
|
+
* - Revocation (A36): the supervisor marks an instance REVOKING in the
|
|
36
|
+
* header; the server fails that instance's requests with ACCESS, closes its
|
|
37
|
+
* handles, and acknowledges with REVOKED. revokeSabIoInstance() resolves
|
|
38
|
+
* only after the acknowledgement, so no byte is written after it returns.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
FLATSQL_IO_ERR_ACCESS,
|
|
43
|
+
FLATSQL_IO_ERR_GENERIC,
|
|
44
|
+
FLATSQL_IO_ERR_IO,
|
|
45
|
+
FLATSQL_IO_MAX_PATH_BYTES,
|
|
46
|
+
} from "./flatsqlIoContract.js";
|
|
47
|
+
|
|
48
|
+
export const SAB_IO_MAGIC = 0x4f494453; // "SDIO" little-endian
|
|
49
|
+
export const SAB_IO_VERSION = 1;
|
|
50
|
+
|
|
51
|
+
// ---- header words (Int32 index) ------------------------------------------
|
|
52
|
+
export const SAB_IO_H_MAGIC = 0;
|
|
53
|
+
export const SAB_IO_H_VERSION = 1;
|
|
54
|
+
export const SAB_IO_H_SLOT_COUNT = 2;
|
|
55
|
+
export const SAB_IO_H_SLOT_BYTES = 3;
|
|
56
|
+
export const SAB_IO_H_MAX_INSTANCES = 4;
|
|
57
|
+
export const SAB_IO_H_DOORBELL = 5;
|
|
58
|
+
export const SAB_IO_H_SERVER_STATE = 6;
|
|
59
|
+
export const SAB_IO_H_DOORBELL_MODE = 7;
|
|
60
|
+
export const SAB_IO_H_SERVER_EPOCH = 8;
|
|
61
|
+
export const SAB_IO_H_CLAIM_HINT = 9;
|
|
62
|
+
export const SAB_IO_H_SLOT_RELEASES = 10;
|
|
63
|
+
export const SAB_IO_H_DATA_BYTES = 11;
|
|
64
|
+
export const SAB_IO_H_SERVED = 12;
|
|
65
|
+
export const SAB_IO_H_CHANNEL_ID = 13;
|
|
66
|
+
const HEADER_WORDS = 64;
|
|
67
|
+
const HEADER_BYTES = HEADER_WORDS * 4;
|
|
68
|
+
|
|
69
|
+
export const SAB_IO_SERVER_NOT_STARTED = 0;
|
|
70
|
+
export const SAB_IO_SERVER_RUNNING = 1;
|
|
71
|
+
export const SAB_IO_SERVER_STOPPED = 2;
|
|
72
|
+
export const SAB_IO_SERVER_DEAD = 3;
|
|
73
|
+
|
|
74
|
+
export const SAB_IO_DOORBELL_ATOMICS = 0;
|
|
75
|
+
export const SAB_IO_DOORBELL_MESSAGE = 1;
|
|
76
|
+
|
|
77
|
+
// ---- instance table (one Int32 per instance id) ----------------------------
|
|
78
|
+
export const SAB_IO_INSTANCE_NONE = 0;
|
|
79
|
+
export const SAB_IO_INSTANCE_ATTACHED = 1;
|
|
80
|
+
export const SAB_IO_INSTANCE_REVOKING = 2;
|
|
81
|
+
export const SAB_IO_INSTANCE_REVOKED = 3;
|
|
82
|
+
/**
|
|
83
|
+
* Instance id of callers that are not a wasm instance (tests, the engine
|
|
84
|
+
* worker's own bookkeeping). It has no attached memory and is never revoked;
|
|
85
|
+
* its data always moves through the slot data area.
|
|
86
|
+
*/
|
|
87
|
+
export const SAB_IO_SCRATCH_INSTANCE = 0x7fff;
|
|
88
|
+
|
|
89
|
+
// ---- slot words (Int32 index relative to the slot) ------------------------
|
|
90
|
+
export const SAB_IO_S_STATE = 0;
|
|
91
|
+
export const SAB_IO_S_OWNER = 1;
|
|
92
|
+
export const SAB_IO_S_SEQ = 2;
|
|
93
|
+
export const SAB_IO_S_DONE_SEQ = 3;
|
|
94
|
+
export const SAB_IO_S_OP = 4;
|
|
95
|
+
export const SAB_IO_S_INSTANCE = 5;
|
|
96
|
+
export const SAB_IO_S_HANDLE = 6;
|
|
97
|
+
export const SAB_IO_S_FLAGS = 7;
|
|
98
|
+
export const SAB_IO_S_PTR = 8;
|
|
99
|
+
export const SAB_IO_S_LEN = 9;
|
|
100
|
+
export const SAB_IO_S_PATH_LEN = 10;
|
|
101
|
+
export const SAB_IO_S_STATUS = 11;
|
|
102
|
+
export const SAB_IO_S_NOTIFY = 12;
|
|
103
|
+
export const SAB_IO_S_DATA_MODE = 13;
|
|
104
|
+
// Float64 index relative to the slot.
|
|
105
|
+
export const SAB_IO_S_F64_OFFSET = 8;
|
|
106
|
+
export const SAB_IO_S_F64_RESULT = 9;
|
|
107
|
+
const SLOT_HEADER_BYTES = 128;
|
|
108
|
+
|
|
109
|
+
export const SAB_IO_SLOT_FREE = 0;
|
|
110
|
+
export const SAB_IO_SLOT_IDLE = 1;
|
|
111
|
+
export const SAB_IO_SLOT_PENDING = 2;
|
|
112
|
+
export const SAB_IO_SLOT_SERVICING = 3;
|
|
113
|
+
export const SAB_IO_SLOT_DONE = 4;
|
|
114
|
+
|
|
115
|
+
export const SAB_IO_NOTIFY_ATOMICS = 0;
|
|
116
|
+
export const SAB_IO_NOTIFY_MESSAGE = 1;
|
|
117
|
+
|
|
118
|
+
/** `ptr` addresses the instance's attached memory. */
|
|
119
|
+
export const SAB_IO_DATA_MEMORY = 0;
|
|
120
|
+
/** `ptr` addresses the slot's own data area (the scratch path). */
|
|
121
|
+
export const SAB_IO_DATA_SLOT = 1;
|
|
122
|
+
|
|
123
|
+
// ---- operations -----------------------------------------------------------
|
|
124
|
+
export const SAB_IO_OP_OPEN = 1;
|
|
125
|
+
export const SAB_IO_OP_READ = 2;
|
|
126
|
+
export const SAB_IO_OP_WRITE = 3;
|
|
127
|
+
export const SAB_IO_OP_TRUNCATE = 4;
|
|
128
|
+
export const SAB_IO_OP_SYNC = 5;
|
|
129
|
+
export const SAB_IO_OP_SIZE = 6;
|
|
130
|
+
export const SAB_IO_OP_CLOSE = 7;
|
|
131
|
+
|
|
132
|
+
export const DEFAULT_SAB_IO_SLOTS = 64;
|
|
133
|
+
export const DEFAULT_SAB_IO_DATA_BYTES = 64 * 1024;
|
|
134
|
+
export const DEFAULT_SAB_IO_MAX_INSTANCES = 64;
|
|
135
|
+
|
|
136
|
+
/** Bound on one blocking wait. Waits loop forever in slices; they never time out. */
|
|
137
|
+
export const SAB_IO_WAIT_SLICE_MS = 250;
|
|
138
|
+
/** How long a blocking client polls for its answer before it sleeps. */
|
|
139
|
+
export const DEFAULT_SAB_IO_CLIENT_SPIN_MICROS = 20;
|
|
140
|
+
|
|
141
|
+
function nowMs() {
|
|
142
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function alignUp(value, alignment) {
|
|
146
|
+
return Math.ceil(value / alignment) * alignment;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function assertSharedArrayBuffer(buffer) {
|
|
150
|
+
if (
|
|
151
|
+
typeof SharedArrayBuffer !== "function" ||
|
|
152
|
+
!(buffer instanceof SharedArrayBuffer)
|
|
153
|
+
) {
|
|
154
|
+
throw new TypeError(
|
|
155
|
+
"The SAB I/O channel requires a SharedArrayBuffer (cross-origin isolation in browsers).",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Allocate the shared buffer of one I/O channel.
|
|
162
|
+
*
|
|
163
|
+
* @param {object} [options]
|
|
164
|
+
* @param {number} [options.slots=64] concurrent requests; size it to at least
|
|
165
|
+
* the number of guest threads plus async callers using this channel.
|
|
166
|
+
* @param {number} [options.dataBytes=65536] per-slot data area: paths, and the
|
|
167
|
+
* scratch path for unattached callers. Must hold the longest path (4096).
|
|
168
|
+
* @param {number} [options.maxInstances=64] instance ids 0..maxInstances-1.
|
|
169
|
+
*/
|
|
170
|
+
export function createSabIoChannelBuffer(options = {}) {
|
|
171
|
+
if (typeof SharedArrayBuffer !== "function") {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"SharedArrayBuffer is unavailable; the I/O channel requires it " +
|
|
174
|
+
"(browsers additionally require cross-origin isolation: COOP+COEP).",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const slots = Number.isInteger(options.slots) ? options.slots : DEFAULT_SAB_IO_SLOTS;
|
|
178
|
+
const dataBytes = Number.isInteger(options.dataBytes)
|
|
179
|
+
? options.dataBytes
|
|
180
|
+
: DEFAULT_SAB_IO_DATA_BYTES;
|
|
181
|
+
const maxInstances = Number.isInteger(options.maxInstances)
|
|
182
|
+
? options.maxInstances
|
|
183
|
+
: DEFAULT_SAB_IO_MAX_INSTANCES;
|
|
184
|
+
if (slots < 1 || slots > 4096) {
|
|
185
|
+
throw new RangeError("slots must be an integer in 1..4096.");
|
|
186
|
+
}
|
|
187
|
+
if (dataBytes < FLATSQL_IO_MAX_PATH_BYTES || dataBytes % 8 !== 0) {
|
|
188
|
+
throw new RangeError(
|
|
189
|
+
`dataBytes must be a multiple of 8 and at least ${FLATSQL_IO_MAX_PATH_BYTES}.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (maxInstances < 1 || maxInstances > SAB_IO_SCRATCH_INSTANCE) {
|
|
193
|
+
throw new RangeError(`maxInstances must be in 1..${SAB_IO_SCRATCH_INSTANCE}.`);
|
|
194
|
+
}
|
|
195
|
+
const slotBytes = alignUp(SLOT_HEADER_BYTES + dataBytes, 64);
|
|
196
|
+
const slotsOffset = alignUp(HEADER_BYTES + maxInstances * 4, 64);
|
|
197
|
+
const buffer = new SharedArrayBuffer(slotsOffset + slots * slotBytes);
|
|
198
|
+
const header = new Int32Array(buffer, 0, HEADER_WORDS);
|
|
199
|
+
header[SAB_IO_H_MAGIC] = SAB_IO_MAGIC;
|
|
200
|
+
header[SAB_IO_H_VERSION] = SAB_IO_VERSION;
|
|
201
|
+
header[SAB_IO_H_SLOT_COUNT] = slots;
|
|
202
|
+
header[SAB_IO_H_SLOT_BYTES] = slotBytes;
|
|
203
|
+
header[SAB_IO_H_MAX_INSTANCES] = maxInstances;
|
|
204
|
+
header[SAB_IO_H_DATA_BYTES] = dataBytes;
|
|
205
|
+
// A random channel id names the BroadcastChannels of message-mode doorbells.
|
|
206
|
+
header[SAB_IO_H_CHANNEL_ID] = (Math.random() * 0x7fffffff) | 0 || 1;
|
|
207
|
+
return buffer;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Views and geometry of a channel buffer. */
|
|
211
|
+
export function describeSabIoChannel(buffer) {
|
|
212
|
+
assertSharedArrayBuffer(buffer);
|
|
213
|
+
const header = new Int32Array(buffer, 0, HEADER_WORDS);
|
|
214
|
+
if (header[SAB_IO_H_MAGIC] !== SAB_IO_MAGIC || header[SAB_IO_H_VERSION] !== SAB_IO_VERSION) {
|
|
215
|
+
throw new TypeError("Not a SAB I/O channel buffer (bad magic or version).");
|
|
216
|
+
}
|
|
217
|
+
const slotCount = header[SAB_IO_H_SLOT_COUNT];
|
|
218
|
+
const slotBytes = header[SAB_IO_H_SLOT_BYTES];
|
|
219
|
+
const maxInstances = header[SAB_IO_H_MAX_INSTANCES];
|
|
220
|
+
const dataBytes = header[SAB_IO_H_DATA_BYTES];
|
|
221
|
+
const slotsOffset = alignUp(HEADER_BYTES + maxInstances * 4, 64);
|
|
222
|
+
const instances = new Int32Array(buffer, HEADER_BYTES, maxInstances);
|
|
223
|
+
const slots = [];
|
|
224
|
+
for (let index = 0; index < slotCount; index += 1) {
|
|
225
|
+
const base = slotsOffset + index * slotBytes;
|
|
226
|
+
slots.push({
|
|
227
|
+
index,
|
|
228
|
+
base,
|
|
229
|
+
i32: new Int32Array(buffer, base, SLOT_HEADER_BYTES / 4),
|
|
230
|
+
f64: new Float64Array(buffer, base, SLOT_HEADER_BYTES / 8),
|
|
231
|
+
data: new Uint8Array(buffer, base + SLOT_HEADER_BYTES, dataBytes),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
buffer,
|
|
236
|
+
header,
|
|
237
|
+
instances,
|
|
238
|
+
slots,
|
|
239
|
+
slotCount,
|
|
240
|
+
slotBytes,
|
|
241
|
+
dataBytes,
|
|
242
|
+
maxInstances,
|
|
243
|
+
channelId: header[SAB_IO_H_CHANNEL_ID],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** BroadcastChannel names used by message-mode doorbells and completions. */
|
|
248
|
+
export function sabIoDoorbellChannelName(channelId) {
|
|
249
|
+
return `sdm-sab-io-doorbell-${channelId}`;
|
|
250
|
+
}
|
|
251
|
+
export function sabIoCompletionChannelName(channelId) {
|
|
252
|
+
return `sdm-sab-io-done-${channelId}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function ringDoorbell(layout, messenger) {
|
|
256
|
+
Atomics.add(layout.header, SAB_IO_H_DOORBELL, 1);
|
|
257
|
+
Atomics.notify(layout.header, SAB_IO_H_DOORBELL);
|
|
258
|
+
if (Atomics.load(layout.header, SAB_IO_H_DOORBELL_MODE) === SAB_IO_DOORBELL_MESSAGE) {
|
|
259
|
+
messenger()?.postMessage(0);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function lazyBroadcast(name) {
|
|
264
|
+
let channel = null;
|
|
265
|
+
return {
|
|
266
|
+
get() {
|
|
267
|
+
if (!channel && typeof BroadcastChannel === "function") {
|
|
268
|
+
channel = new BroadcastChannel(name);
|
|
269
|
+
}
|
|
270
|
+
return channel;
|
|
271
|
+
},
|
|
272
|
+
close() {
|
|
273
|
+
channel?.close();
|
|
274
|
+
channel = null;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const pathEncoder = typeof TextEncoder === "function" ? new TextEncoder() : null;
|
|
280
|
+
|
|
281
|
+
function randomOwnerToken() {
|
|
282
|
+
return ((Math.random() * 0x7ffffffe) | 0) + 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Claim a FREE slot. Returns the slot or null. Never blocks. */
|
|
286
|
+
function tryClaimSlot(layout, owner) {
|
|
287
|
+
const start = (Atomics.add(layout.header, SAB_IO_H_CLAIM_HINT, 1) >>> 0) % layout.slotCount;
|
|
288
|
+
for (let step = 0; step < layout.slotCount; step += 1) {
|
|
289
|
+
const slot = layout.slots[(start + step) % layout.slotCount];
|
|
290
|
+
if (
|
|
291
|
+
Atomics.compareExchange(slot.i32, SAB_IO_S_STATE, SAB_IO_SLOT_FREE, SAB_IO_SLOT_IDLE) ===
|
|
292
|
+
SAB_IO_SLOT_FREE
|
|
293
|
+
) {
|
|
294
|
+
Atomics.store(slot.i32, SAB_IO_S_OWNER, owner);
|
|
295
|
+
return slot;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function releaseSlot(layout, slot) {
|
|
302
|
+
Atomics.store(slot.i32, SAB_IO_S_OWNER, 0);
|
|
303
|
+
Atomics.store(slot.i32, SAB_IO_S_STATE, SAB_IO_SLOT_FREE);
|
|
304
|
+
Atomics.add(layout.header, SAB_IO_H_SLOT_RELEASES, 1);
|
|
305
|
+
Atomics.notify(layout.header, SAB_IO_H_SLOT_RELEASES);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Fill a slot's request fields. `fields.path` (bytes) is copied into the slot
|
|
310
|
+
* data area, so the server never decodes a view over guest memory.
|
|
311
|
+
*/
|
|
312
|
+
function writeRequest(slot, op, fields) {
|
|
313
|
+
const i32 = slot.i32;
|
|
314
|
+
i32[SAB_IO_S_OP] = op;
|
|
315
|
+
i32[SAB_IO_S_INSTANCE] = fields.instanceId ?? SAB_IO_SCRATCH_INSTANCE;
|
|
316
|
+
i32[SAB_IO_S_HANDLE] = fields.handle ?? -1;
|
|
317
|
+
i32[SAB_IO_S_FLAGS] = fields.flags ?? 0;
|
|
318
|
+
i32[SAB_IO_S_PTR] = fields.ptr ?? 0;
|
|
319
|
+
i32[SAB_IO_S_LEN] = fields.len ?? 0;
|
|
320
|
+
i32[SAB_IO_S_DATA_MODE] = fields.dataInSlot ? SAB_IO_DATA_SLOT : SAB_IO_DATA_MEMORY;
|
|
321
|
+
i32[SAB_IO_S_STATUS] = 0;
|
|
322
|
+
slot.f64[SAB_IO_S_F64_OFFSET] = fields.offset ?? 0;
|
|
323
|
+
slot.f64[SAB_IO_S_F64_RESULT] = 0;
|
|
324
|
+
if (fields.path) {
|
|
325
|
+
slot.data.set(fields.path, 0);
|
|
326
|
+
i32[SAB_IO_S_PATH_LEN] = fields.path.length;
|
|
327
|
+
} else {
|
|
328
|
+
i32[SAB_IO_S_PATH_LEN] = 0;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Status of a request, as the server returns it (i32 ops). */
|
|
333
|
+
function statusOf(slot) {
|
|
334
|
+
return Atomics.load(slot.i32, SAB_IO_S_STATUS);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Blocking client for one guest thread (or any Worker that may block). Every
|
|
339
|
+
* method returns a status or a count; none throws. Each request claims a free
|
|
340
|
+
* slot and releases it once its result is read.
|
|
341
|
+
*
|
|
342
|
+
* Pointers (`ptr`) address the attached memory of `instanceId` when that
|
|
343
|
+
* instance is ATTACHED; otherwise the client moves the bytes through the slot
|
|
344
|
+
* data area itself using `getMemory()` (the scratch path). Callers without a
|
|
345
|
+
* wasm memory use readInto/writeFrom with plain Uint8Arrays.
|
|
346
|
+
*
|
|
347
|
+
* @param {object} options
|
|
348
|
+
* @param {SharedArrayBuffer} options.buffer channel buffer
|
|
349
|
+
* @param {number} [options.instanceId] the calling instance (revocation and
|
|
350
|
+
* memory resolution). Defaults to the scratch instance.
|
|
351
|
+
* @param {() => (WebAssembly.Memory|null)} [options.getMemory] the caller's
|
|
352
|
+
* memory, for the scratch path of pointer-based requests.
|
|
353
|
+
* @param {number} [options.spinMicros=20] poll for the answer this long before
|
|
354
|
+
* sleeping (0 disables).
|
|
355
|
+
*/
|
|
356
|
+
export function createSabIoClient(options = {}) {
|
|
357
|
+
assertSharedArrayBuffer(options.buffer);
|
|
358
|
+
const layout = describeSabIoChannel(options.buffer);
|
|
359
|
+
const instanceId = Number.isInteger(options.instanceId)
|
|
360
|
+
? options.instanceId
|
|
361
|
+
: SAB_IO_SCRATCH_INSTANCE;
|
|
362
|
+
if (
|
|
363
|
+
instanceId !== SAB_IO_SCRATCH_INSTANCE &&
|
|
364
|
+
(instanceId < 0 || instanceId >= layout.maxInstances)
|
|
365
|
+
) {
|
|
366
|
+
throw new RangeError(`instanceId ${instanceId} is outside 0..${layout.maxInstances - 1}.`);
|
|
367
|
+
}
|
|
368
|
+
const getMemory = typeof options.getMemory === "function" ? options.getMemory : () => null;
|
|
369
|
+
const spinMicros = Number.isFinite(options.spinMicros)
|
|
370
|
+
? Math.max(0, options.spinMicros)
|
|
371
|
+
: DEFAULT_SAB_IO_CLIENT_SPIN_MICROS;
|
|
372
|
+
const owner = randomOwnerToken();
|
|
373
|
+
const doorbell = lazyBroadcast(sabIoDoorbellChannelName(layout.channelId));
|
|
374
|
+
let requests = 0;
|
|
375
|
+
|
|
376
|
+
// A slot is claimed per request and released as soon as its result is read.
|
|
377
|
+
// A client therefore holds no slot while idle, and a terminated worker leaks
|
|
378
|
+
// none (only a request in flight at termination, which the server still
|
|
379
|
+
// completes; reclaimSabIoSlots frees those).
|
|
380
|
+
function claim() {
|
|
381
|
+
for (;;) {
|
|
382
|
+
const releases = Atomics.load(layout.header, SAB_IO_H_SLOT_RELEASES);
|
|
383
|
+
const slot = tryClaimSlot(layout, owner);
|
|
384
|
+
if (slot) return slot;
|
|
385
|
+
// Every slot is busy. Wait for a release, in bounded slices.
|
|
386
|
+
Atomics.wait(layout.header, SAB_IO_H_SLOT_RELEASES, releases, SAB_IO_WAIT_SLICE_MS);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function roundTrip(slot, op, fields) {
|
|
391
|
+
const seq = (Atomics.load(slot.i32, SAB_IO_S_SEQ) + 1) | 0 || 1;
|
|
392
|
+
writeRequest(slot, op, fields);
|
|
393
|
+
Atomics.store(slot.i32, SAB_IO_S_NOTIFY, SAB_IO_NOTIFY_ATOMICS);
|
|
394
|
+
Atomics.store(slot.i32, SAB_IO_S_SEQ, seq);
|
|
395
|
+
Atomics.store(slot.i32, SAB_IO_S_STATE, SAB_IO_SLOT_PENDING);
|
|
396
|
+
ringDoorbell(layout, doorbell.get);
|
|
397
|
+
// Spin briefly first: a fast answer then costs no sleep and wake-up.
|
|
398
|
+
const spinUntil = spinMicros > 0 ? nowMs() + spinMicros / 1000 : 0;
|
|
399
|
+
for (;;) {
|
|
400
|
+
const state = Atomics.load(slot.i32, SAB_IO_S_STATE);
|
|
401
|
+
if (state === SAB_IO_SLOT_DONE && Atomics.load(slot.i32, SAB_IO_S_DONE_SEQ) === seq) {
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
if (spinUntil > 0 && nowMs() < spinUntil) continue;
|
|
405
|
+
Atomics.wait(slot.i32, SAB_IO_S_STATE, state, SAB_IO_WAIT_SLICE_MS);
|
|
406
|
+
}
|
|
407
|
+
requests += 1;
|
|
408
|
+
return { status: statusOf(slot), result: slot.f64[SAB_IO_S_F64_RESULT] };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function request(op, fields) {
|
|
412
|
+
const slot = claim();
|
|
413
|
+
try {
|
|
414
|
+
return roundTrip(slot, op, fields);
|
|
415
|
+
} finally {
|
|
416
|
+
releaseSlot(layout, slot);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function attached() {
|
|
421
|
+
return (
|
|
422
|
+
instanceId !== SAB_IO_SCRATCH_INSTANCE &&
|
|
423
|
+
Atomics.load(layout.instances, instanceId) === SAB_IO_INSTANCE_ATTACHED
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function revoked() {
|
|
428
|
+
if (instanceId === SAB_IO_SCRATCH_INSTANCE) return false;
|
|
429
|
+
const state = Atomics.load(layout.instances, instanceId);
|
|
430
|
+
return state === SAB_IO_INSTANCE_REVOKING || state === SAB_IO_INSTANCE_REVOKED;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Move bytes between a caller view and the file through the slot data area,
|
|
434
|
+
// in data-area-sized pieces. `op` is SAB_IO_OP_READ or SAB_IO_OP_WRITE.
|
|
435
|
+
function scratchTransfer(op, handle, view, offset) {
|
|
436
|
+
let done = 0;
|
|
437
|
+
while (done < view.length) {
|
|
438
|
+
const piece = Math.min(layout.dataBytes, view.length - done);
|
|
439
|
+
const slot = claim();
|
|
440
|
+
let status;
|
|
441
|
+
try {
|
|
442
|
+
if (op === SAB_IO_OP_WRITE) {
|
|
443
|
+
slot.data.set(view.subarray(done, done + piece), 0);
|
|
444
|
+
}
|
|
445
|
+
status = roundTrip(slot, op, {
|
|
446
|
+
instanceId,
|
|
447
|
+
dataInSlot: true,
|
|
448
|
+
handle,
|
|
449
|
+
ptr: 0,
|
|
450
|
+
len: piece,
|
|
451
|
+
offset: offset + done,
|
|
452
|
+
}).status;
|
|
453
|
+
if (op === SAB_IO_OP_READ && status > 0) {
|
|
454
|
+
view.set(slot.data.subarray(0, status), done);
|
|
455
|
+
}
|
|
456
|
+
} finally {
|
|
457
|
+
releaseSlot(layout, slot);
|
|
458
|
+
}
|
|
459
|
+
if (status < 0) {
|
|
460
|
+
return done > 0 && op === SAB_IO_OP_READ ? done : status;
|
|
461
|
+
}
|
|
462
|
+
done += status;
|
|
463
|
+
if (status < piece) {
|
|
464
|
+
break; // short read at EOF (or a short write the host reported)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return done;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function memoryView(ptr, len) {
|
|
471
|
+
const memory = getMemory();
|
|
472
|
+
const buffer = memory?.buffer ?? memory;
|
|
473
|
+
if (!buffer || ptr < 0 || len < 0 || ptr + len > buffer.byteLength) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
return new Uint8Array(buffer, ptr, len);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
instanceId,
|
|
481
|
+
get requests() {
|
|
482
|
+
return requests;
|
|
483
|
+
},
|
|
484
|
+
/** Open (or probe/unlink) `path` (UTF-8 bytes, or a string). */
|
|
485
|
+
open(path, flags) {
|
|
486
|
+
const pathBytes = typeof path === "string" ? pathEncoder.encode(path) : path;
|
|
487
|
+
if (!(pathBytes instanceof Uint8Array) || pathBytes.length === 0) {
|
|
488
|
+
return FLATSQL_IO_ERR_GENERIC;
|
|
489
|
+
}
|
|
490
|
+
if (pathBytes.length > Math.min(layout.dataBytes, FLATSQL_IO_MAX_PATH_BYTES)) {
|
|
491
|
+
return FLATSQL_IO_ERR_GENERIC;
|
|
492
|
+
}
|
|
493
|
+
return request(SAB_IO_OP_OPEN, { instanceId, flags: flags | 0, path: pathBytes })
|
|
494
|
+
.status;
|
|
495
|
+
},
|
|
496
|
+
/** Read into the caller's memory at `ptr`. */
|
|
497
|
+
read(handle, ptr, len, offset) {
|
|
498
|
+
if (len === 0) return 0;
|
|
499
|
+
if (attached()) {
|
|
500
|
+
return request(SAB_IO_OP_READ, { instanceId, handle, ptr, len, offset }).status;
|
|
501
|
+
}
|
|
502
|
+
const view = memoryView(ptr, len);
|
|
503
|
+
if (!view) return revoked() ? FLATSQL_IO_ERR_ACCESS : FLATSQL_IO_ERR_GENERIC;
|
|
504
|
+
return scratchTransfer(SAB_IO_OP_READ, handle, view, offset);
|
|
505
|
+
},
|
|
506
|
+
/** Write from the caller's memory at `ptr`. */
|
|
507
|
+
write(handle, ptr, len, offset) {
|
|
508
|
+
if (len === 0) return 0;
|
|
509
|
+
if (attached()) {
|
|
510
|
+
return request(SAB_IO_OP_WRITE, { instanceId, handle, ptr, len, offset }).status;
|
|
511
|
+
}
|
|
512
|
+
const view = memoryView(ptr, len);
|
|
513
|
+
if (!view) return revoked() ? FLATSQL_IO_ERR_ACCESS : FLATSQL_IO_ERR_GENERIC;
|
|
514
|
+
return scratchTransfer(SAB_IO_OP_WRITE, handle, view, offset);
|
|
515
|
+
},
|
|
516
|
+
/** Read into a plain Uint8Array (scratch path). */
|
|
517
|
+
readInto(handle, view, offset) {
|
|
518
|
+
if (view.length === 0) return 0;
|
|
519
|
+
return scratchTransfer(SAB_IO_OP_READ, handle, view, offset);
|
|
520
|
+
},
|
|
521
|
+
/** Write a plain Uint8Array (scratch path). */
|
|
522
|
+
writeFrom(handle, view, offset) {
|
|
523
|
+
if (view.length === 0) return 0;
|
|
524
|
+
return scratchTransfer(SAB_IO_OP_WRITE, handle, view, offset);
|
|
525
|
+
},
|
|
526
|
+
truncate(handle, size) {
|
|
527
|
+
return request(SAB_IO_OP_TRUNCATE, { instanceId, handle, offset: size }).status;
|
|
528
|
+
},
|
|
529
|
+
sync(handle) {
|
|
530
|
+
return request(SAB_IO_OP_SYNC, { instanceId, handle }).status;
|
|
531
|
+
},
|
|
532
|
+
size(handle) {
|
|
533
|
+
const { status, result } = request(SAB_IO_OP_SIZE, { instanceId, handle });
|
|
534
|
+
return status < 0 ? status : result;
|
|
535
|
+
},
|
|
536
|
+
close(handle) {
|
|
537
|
+
return request(SAB_IO_OP_CLOSE, { instanceId, handle }).status;
|
|
538
|
+
},
|
|
539
|
+
/** The thread is exiting: close the doorbell channel. */
|
|
540
|
+
release() {
|
|
541
|
+
doorbell.close();
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Non-blocking client for contexts that must not block: a page, the engine
|
|
548
|
+
* worker's event loop, or one I/O worker forwarding to another. Each request
|
|
549
|
+
* claims a slot, waits with `Atomics.waitAsync` (or a completion message where
|
|
550
|
+
* waitAsync is missing, 22.3a-5), and releases it. Data always moves through
|
|
551
|
+
* the slot data area.
|
|
552
|
+
*
|
|
553
|
+
* Methods resolve to a status or count; `read` resolves to a Uint8Array or a
|
|
554
|
+
* negative status. Nothing rejects.
|
|
555
|
+
*/
|
|
556
|
+
export function createSabIoAsyncClient(options = {}) {
|
|
557
|
+
assertSharedArrayBuffer(options.buffer);
|
|
558
|
+
const layout = describeSabIoChannel(options.buffer);
|
|
559
|
+
const instanceId = Number.isInteger(options.instanceId)
|
|
560
|
+
? options.instanceId
|
|
561
|
+
: SAB_IO_SCRATCH_INSTANCE;
|
|
562
|
+
const owner = randomOwnerToken();
|
|
563
|
+
const hasWaitAsync = typeof Atomics.waitAsync === "function" && options.forceMessages !== true;
|
|
564
|
+
const doorbell = lazyBroadcast(sabIoDoorbellChannelName(layout.channelId));
|
|
565
|
+
const completion = hasWaitAsync
|
|
566
|
+
? null
|
|
567
|
+
: lazyBroadcast(sabIoCompletionChannelName(layout.channelId));
|
|
568
|
+
const completionWaiters = new Set();
|
|
569
|
+
let completionListening = false;
|
|
570
|
+
|
|
571
|
+
function listenForCompletions() {
|
|
572
|
+
if (completionListening || !completion) return;
|
|
573
|
+
const channel = completion.get();
|
|
574
|
+
if (!channel) return;
|
|
575
|
+
completionListening = true;
|
|
576
|
+
channel.onmessage = () => {
|
|
577
|
+
for (const wake of Array.from(completionWaiters)) wake();
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function sleep(ms) {
|
|
582
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function claim() {
|
|
586
|
+
for (;;) {
|
|
587
|
+
const releases = Atomics.load(layout.header, SAB_IO_H_SLOT_RELEASES);
|
|
588
|
+
const slot = tryClaimSlot(layout, owner);
|
|
589
|
+
if (slot) return slot;
|
|
590
|
+
if (hasWaitAsync) {
|
|
591
|
+
const waited = Atomics.waitAsync(
|
|
592
|
+
layout.header,
|
|
593
|
+
SAB_IO_H_SLOT_RELEASES,
|
|
594
|
+
releases,
|
|
595
|
+
SAB_IO_WAIT_SLICE_MS,
|
|
596
|
+
);
|
|
597
|
+
if (waited.async) await waited.value;
|
|
598
|
+
} else {
|
|
599
|
+
await sleep(1);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function waitDone(slot, seq) {
|
|
605
|
+
for (;;) {
|
|
606
|
+
const state = Atomics.load(slot.i32, SAB_IO_S_STATE);
|
|
607
|
+
if (state === SAB_IO_SLOT_DONE && Atomics.load(slot.i32, SAB_IO_S_DONE_SEQ) === seq) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (hasWaitAsync) {
|
|
611
|
+
const waited = Atomics.waitAsync(slot.i32, SAB_IO_S_STATE, state, SAB_IO_WAIT_SLICE_MS);
|
|
612
|
+
if (waited.async) await waited.value;
|
|
613
|
+
} else {
|
|
614
|
+
listenForCompletions();
|
|
615
|
+
await new Promise((resolve) => {
|
|
616
|
+
const wake = () => {
|
|
617
|
+
completionWaiters.delete(wake);
|
|
618
|
+
clearTimeout(timer);
|
|
619
|
+
resolve();
|
|
620
|
+
};
|
|
621
|
+
const timer = setTimeout(wake, SAB_IO_WAIT_SLICE_MS);
|
|
622
|
+
completionWaiters.add(wake);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let seqCounter = 0;
|
|
629
|
+
async function roundTrip(op, fields, before, after) {
|
|
630
|
+
const slot = await claim();
|
|
631
|
+
try {
|
|
632
|
+
const seq = (seqCounter = (seqCounter + 1) | 0 || 1);
|
|
633
|
+
writeRequest(slot, op, fields);
|
|
634
|
+
before?.(slot);
|
|
635
|
+
Atomics.store(
|
|
636
|
+
slot.i32,
|
|
637
|
+
SAB_IO_S_NOTIFY,
|
|
638
|
+
hasWaitAsync ? SAB_IO_NOTIFY_ATOMICS : SAB_IO_NOTIFY_MESSAGE,
|
|
639
|
+
);
|
|
640
|
+
Atomics.store(slot.i32, SAB_IO_S_SEQ, seq);
|
|
641
|
+
Atomics.store(slot.i32, SAB_IO_S_STATE, SAB_IO_SLOT_PENDING);
|
|
642
|
+
ringDoorbell(layout, doorbell.get);
|
|
643
|
+
await waitDone(slot, seq);
|
|
644
|
+
const status = statusOf(slot);
|
|
645
|
+
const result = slot.f64[SAB_IO_S_F64_RESULT];
|
|
646
|
+
const extra = after ? after(slot, status) : undefined;
|
|
647
|
+
return { status, result, extra };
|
|
648
|
+
} finally {
|
|
649
|
+
releaseSlot(layout, slot);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const encoder = new TextEncoder();
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
instanceId,
|
|
657
|
+
async open(path, flags) {
|
|
658
|
+
const bytes = typeof path === "string" ? encoder.encode(path) : path;
|
|
659
|
+
if (!(bytes instanceof Uint8Array) || bytes.length === 0) return FLATSQL_IO_ERR_GENERIC;
|
|
660
|
+
if (bytes.length > Math.min(layout.dataBytes, FLATSQL_IO_MAX_PATH_BYTES)) {
|
|
661
|
+
return FLATSQL_IO_ERR_GENERIC;
|
|
662
|
+
}
|
|
663
|
+
return (await roundTrip(SAB_IO_OP_OPEN, { instanceId, flags: flags | 0, path: bytes }))
|
|
664
|
+
.status;
|
|
665
|
+
},
|
|
666
|
+
/** Resolves to the bytes read (possibly short at EOF) or a negative status. */
|
|
667
|
+
async read(handle, length, offset) {
|
|
668
|
+
const out = new Uint8Array(length);
|
|
669
|
+
let done = 0;
|
|
670
|
+
while (done < length) {
|
|
671
|
+
const piece = Math.min(layout.dataBytes, length - done);
|
|
672
|
+
const { status } = await roundTrip(
|
|
673
|
+
SAB_IO_OP_READ,
|
|
674
|
+
{ instanceId, dataInSlot: true, handle, ptr: 0, len: piece, offset: offset + done },
|
|
675
|
+
null,
|
|
676
|
+
(slot, st) => {
|
|
677
|
+
if (st > 0) out.set(slot.data.subarray(0, st), done);
|
|
678
|
+
},
|
|
679
|
+
);
|
|
680
|
+
if (status < 0) return done > 0 ? out.subarray(0, done) : status;
|
|
681
|
+
done += status;
|
|
682
|
+
if (status < piece) break;
|
|
683
|
+
}
|
|
684
|
+
return out.subarray(0, done);
|
|
685
|
+
},
|
|
686
|
+
async write(handle, bytes, offset) {
|
|
687
|
+
let done = 0;
|
|
688
|
+
while (done < bytes.length) {
|
|
689
|
+
const piece = Math.min(layout.dataBytes, bytes.length - done);
|
|
690
|
+
const { status } = await roundTrip(
|
|
691
|
+
SAB_IO_OP_WRITE,
|
|
692
|
+
{ instanceId, dataInSlot: true, handle, ptr: 0, len: piece, offset: offset + done },
|
|
693
|
+
(slot) => slot.data.set(bytes.subarray(done, done + piece), 0),
|
|
694
|
+
);
|
|
695
|
+
if (status < 0) return status;
|
|
696
|
+
done += status;
|
|
697
|
+
if (status < piece) break;
|
|
698
|
+
}
|
|
699
|
+
return done;
|
|
700
|
+
},
|
|
701
|
+
async truncate(handle, size) {
|
|
702
|
+
return (await roundTrip(SAB_IO_OP_TRUNCATE, { instanceId, handle, offset: size })).status;
|
|
703
|
+
},
|
|
704
|
+
async sync(handle) {
|
|
705
|
+
return (await roundTrip(SAB_IO_OP_SYNC, { instanceId, handle })).status;
|
|
706
|
+
},
|
|
707
|
+
async size(handle) {
|
|
708
|
+
const { status, result } = await roundTrip(SAB_IO_OP_SIZE, { instanceId, handle });
|
|
709
|
+
return status < 0 ? status : result;
|
|
710
|
+
},
|
|
711
|
+
async close(handle) {
|
|
712
|
+
return (await roundTrip(SAB_IO_OP_CLOSE, { instanceId, handle })).status;
|
|
713
|
+
},
|
|
714
|
+
close$() {
|
|
715
|
+
doorbell.close();
|
|
716
|
+
completion?.close();
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Instance state as published in the channel header. */
|
|
722
|
+
export function sabIoInstanceState(buffer, instanceId) {
|
|
723
|
+
const layout = describeSabIoChannel(buffer);
|
|
724
|
+
return Atomics.load(layout.instances, instanceId);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Revoke an instance (A36). Requests from it then complete with ACCESS, and
|
|
729
|
+
* the server closes its handles. Resolves once the server has acknowledged, so
|
|
730
|
+
* no byte of that instance is written after it resolves. If the server is not
|
|
731
|
+
* running (dead or never started), the acknowledgement is immediate: there is
|
|
732
|
+
* nobody left to write.
|
|
733
|
+
*/
|
|
734
|
+
export async function revokeSabIoInstance(buffer, instanceId, { pollMs = 5 } = {}) {
|
|
735
|
+
const layout = describeSabIoChannel(buffer);
|
|
736
|
+
if (instanceId < 0 || instanceId >= layout.maxInstances) {
|
|
737
|
+
throw new RangeError(`instanceId ${instanceId} is outside 0..${layout.maxInstances - 1}.`);
|
|
738
|
+
}
|
|
739
|
+
Atomics.store(layout.instances, instanceId, SAB_IO_INSTANCE_REVOKING);
|
|
740
|
+
Atomics.add(layout.header, SAB_IO_H_DOORBELL, 1);
|
|
741
|
+
Atomics.notify(layout.header, SAB_IO_H_DOORBELL);
|
|
742
|
+
if (Atomics.load(layout.header, SAB_IO_H_DOORBELL_MODE) === SAB_IO_DOORBELL_MESSAGE) {
|
|
743
|
+
const channel =
|
|
744
|
+
typeof BroadcastChannel === "function"
|
|
745
|
+
? new BroadcastChannel(sabIoDoorbellChannelName(layout.channelId))
|
|
746
|
+
: null;
|
|
747
|
+
channel?.postMessage(0);
|
|
748
|
+
channel?.close();
|
|
749
|
+
}
|
|
750
|
+
for (;;) {
|
|
751
|
+
const state = Atomics.load(layout.instances, instanceId);
|
|
752
|
+
if (state === SAB_IO_INSTANCE_REVOKED) return;
|
|
753
|
+
if (Atomics.load(layout.header, SAB_IO_H_SERVER_STATE) !== SAB_IO_SERVER_RUNNING) {
|
|
754
|
+
Atomics.store(layout.instances, instanceId, SAB_IO_INSTANCE_REVOKED);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (typeof Atomics.waitAsync === "function") {
|
|
758
|
+
const waited = Atomics.waitAsync(layout.instances, instanceId, state, SAB_IO_WAIT_SLICE_MS);
|
|
759
|
+
if (waited.async) await waited.value;
|
|
760
|
+
} else {
|
|
761
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** Clear a revoked instance id so it can be reused by a replacement instance. */
|
|
767
|
+
export function resetSabIoInstance(buffer, instanceId) {
|
|
768
|
+
const layout = describeSabIoChannel(buffer);
|
|
769
|
+
Atomics.store(layout.instances, instanceId, SAB_IO_INSTANCE_NONE);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Free the slots an instance left behind when its threads were terminated
|
|
774
|
+
* mid-request (A36 step 3). Only DONE (completed, never read) and IDLE
|
|
775
|
+
* (claimed, never submitted) slots are freed: a PENDING or SERVICING slot is
|
|
776
|
+
* still owned by the server, which completes it; call again afterwards.
|
|
777
|
+
* Call only after every thread of the instance is gone. Returns the count.
|
|
778
|
+
*/
|
|
779
|
+
export function reclaimSabIoSlots(buffer, instanceId) {
|
|
780
|
+
const layout = describeSabIoChannel(buffer);
|
|
781
|
+
let freed = 0;
|
|
782
|
+
for (const slot of layout.slots) {
|
|
783
|
+
if (Atomics.load(slot.i32, SAB_IO_S_INSTANCE) !== instanceId) continue;
|
|
784
|
+
for (const state of [SAB_IO_SLOT_DONE, SAB_IO_SLOT_IDLE]) {
|
|
785
|
+
if (Atomics.compareExchange(slot.i32, SAB_IO_S_STATE, state, SAB_IO_SLOT_FREE) === state) {
|
|
786
|
+
Atomics.store(slot.i32, SAB_IO_S_OWNER, 0);
|
|
787
|
+
freed += 1;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (freed > 0) {
|
|
793
|
+
Atomics.add(layout.header, SAB_IO_H_SLOT_RELEASES, freed);
|
|
794
|
+
Atomics.notify(layout.header, SAB_IO_H_SLOT_RELEASES);
|
|
795
|
+
}
|
|
796
|
+
return freed;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Supervisor half of A36: the I/O worker died (its `onerror` fired, or it was
|
|
801
|
+
* terminated). Mark the server DEAD and complete every PENDING or SERVICING
|
|
802
|
+
* slot with `status`, so no guest thread stays blocked. Returns the number of
|
|
803
|
+
* requests failed. A replacement server resets the state to RUNNING.
|
|
804
|
+
*/
|
|
805
|
+
export function failPendingSabIoRequests(buffer, status = FLATSQL_IO_ERR_IO) {
|
|
806
|
+
const layout = describeSabIoChannel(buffer);
|
|
807
|
+
Atomics.store(layout.header, SAB_IO_H_SERVER_STATE, SAB_IO_SERVER_DEAD);
|
|
808
|
+
let failed = 0;
|
|
809
|
+
for (const slot of layout.slots) {
|
|
810
|
+
const state = Atomics.load(slot.i32, SAB_IO_S_STATE);
|
|
811
|
+
if (state === SAB_IO_SLOT_PENDING || state === SAB_IO_SLOT_SERVICING) {
|
|
812
|
+
completeSabIoSlot(layout, slot, status, 0);
|
|
813
|
+
failed += 1;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return failed;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
let completionMessenger = null;
|
|
820
|
+
function completionChannelFor(layout) {
|
|
821
|
+
if (typeof BroadcastChannel !== "function") return null;
|
|
822
|
+
if (!completionMessenger || completionMessenger.id !== layout.channelId) {
|
|
823
|
+
completionMessenger?.channel.close();
|
|
824
|
+
completionMessenger = {
|
|
825
|
+
id: layout.channelId,
|
|
826
|
+
channel: new BroadcastChannel(sabIoCompletionChannelName(layout.channelId)),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return completionMessenger.channel;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Complete one slot: publish the status (and an f64 result for SIZE), mark it
|
|
834
|
+
* DONE for the request's sequence number, and wake its client.
|
|
835
|
+
*/
|
|
836
|
+
export function completeSabIoSlot(layout, slot, status, f64Result = 0) {
|
|
837
|
+
slot.f64[SAB_IO_S_F64_RESULT] = f64Result;
|
|
838
|
+
Atomics.store(slot.i32, SAB_IO_S_STATUS, status | 0);
|
|
839
|
+
Atomics.store(slot.i32, SAB_IO_S_DONE_SEQ, Atomics.load(slot.i32, SAB_IO_S_SEQ));
|
|
840
|
+
Atomics.store(slot.i32, SAB_IO_S_STATE, SAB_IO_SLOT_DONE);
|
|
841
|
+
Atomics.notify(slot.i32, SAB_IO_S_STATE);
|
|
842
|
+
if (Atomics.load(slot.i32, SAB_IO_S_NOTIFY) === SAB_IO_NOTIFY_MESSAGE) {
|
|
843
|
+
completionChannelFor(layout)?.postMessage(slot.index);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export const SAB_IO_STATUS_REVOKED = FLATSQL_IO_ERR_ACCESS;
|
|
848
|
+
export const SAB_IO_STATUS_DEAD = FLATSQL_IO_ERR_IO;
|