space-data-module-sdk 0.8.20 → 0.8.22
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 +100 -0
- package/docs/isomorphic-pthreads.md +335 -0
- package/package.json +7 -2
- package/src/browser.js +6 -0
- package/src/compiler/invokeGlue.js +38 -0
- 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/browserModuleHarness.js +24 -5
- 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/isomorphicLoader.js +10 -2
- 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 +21 -11
- package/src/host/wasiThreadHost.js +261 -43
- package/src/host/wasiThreadWorker.mjs +46 -8
- package/src/host/wasiThreadWorkerRuntime.js +54 -0
- package/src/index.d.ts +349 -0
- package/src/runtime/constants.js +6 -0
- package/src/testing/native/wasmedge_wasi_threads_runner.c +13 -5
- package/src/testing/parityHarness.js +7 -1
- package/src/testing/parityLanes.js +19 -0
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
// The server half of the SAB I/O channel: the loop an I/O worker runs
|
|
2
|
+
// (docs/architecture/flatsql-partition-store.md §5.5, A7, A36, A38).
|
|
3
|
+
//
|
|
4
|
+
// It holds every file handle, so guest threads never touch a handle and an
|
|
5
|
+
// open that must `await` (OPFS getFileHandle / createSyncAccessHandle) never
|
|
6
|
+
// blocks anything but its own caller. The loop:
|
|
7
|
+
//
|
|
8
|
+
// 1. waits for the doorbell (Atomics.waitAsync, or a BroadcastChannel
|
|
9
|
+
// message where waitAsync is missing);
|
|
10
|
+
// 2. applies revocations published in the channel header;
|
|
11
|
+
// 3. claims every PENDING slot (-> SERVICING) as a job;
|
|
12
|
+
// 4. runs jobs round-robin. Reads and writes run in steps of at most
|
|
13
|
+
// `chunkBytes` (256 KiB, A7), and the slots are rescanned between steps,
|
|
14
|
+
// so a small read never queues behind a large write. An open is started
|
|
15
|
+
// and parked; it re-enters the ready queue when its promise settles.
|
|
16
|
+
//
|
|
17
|
+
// Backend-agnostic: a backend opens files and answers namespace questions; the
|
|
18
|
+
// files it returns follow FileSystemSyncAccessHandle's synchronous shape
|
|
19
|
+
// (read/write with {at}, truncate, flush, getSize, close). The OPFS backend
|
|
20
|
+
// (opfsIoBackend.js) returns real sync access handles; the memory backend
|
|
21
|
+
// (flatsqlIoMemoryBackend.js) returns in-memory files for the dashboard window
|
|
22
|
+
// store (§5.5, 22.3a-8).
|
|
23
|
+
//
|
|
24
|
+
// One file object per path, shared by every virtual handle on that path
|
|
25
|
+
// (22.3a-6): OPFS allows one sync handle per file per context by default.
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
FLATSQL_IO_CREATE,
|
|
29
|
+
FLATSQL_IO_CREATE_PARENTS,
|
|
30
|
+
FLATSQL_IO_DELETE_ON_CLOSE,
|
|
31
|
+
FLATSQL_IO_ERR_ACCESS,
|
|
32
|
+
FLATSQL_IO_ERR_BADHANDLE,
|
|
33
|
+
FLATSQL_IO_ERR_BUSY,
|
|
34
|
+
FLATSQL_IO_ERR_GENERIC,
|
|
35
|
+
FLATSQL_IO_ERR_IO,
|
|
36
|
+
FLATSQL_IO_ERR_NOENT,
|
|
37
|
+
FLATSQL_IO_EXCL,
|
|
38
|
+
FLATSQL_IO_OPEN_DEFERRED,
|
|
39
|
+
FLATSQL_IO_PROBE,
|
|
40
|
+
FLATSQL_IO_READ,
|
|
41
|
+
FLATSQL_IO_TRUNC,
|
|
42
|
+
FLATSQL_IO_UNLINK,
|
|
43
|
+
FLATSQL_IO_UNLINK_IF_UNUSED,
|
|
44
|
+
FLATSQL_IO_WRITE,
|
|
45
|
+
decodeFlatsqlIoPath,
|
|
46
|
+
splitFlatsqlIoPath,
|
|
47
|
+
flatsqlIoStatusOf,
|
|
48
|
+
} from "./flatsqlIoContract.js";
|
|
49
|
+
import {
|
|
50
|
+
SAB_IO_DATA_SLOT,
|
|
51
|
+
SAB_IO_DOORBELL_ATOMICS,
|
|
52
|
+
SAB_IO_DOORBELL_MESSAGE,
|
|
53
|
+
SAB_IO_H_DOORBELL,
|
|
54
|
+
SAB_IO_H_DOORBELL_MODE,
|
|
55
|
+
SAB_IO_H_SERVED,
|
|
56
|
+
SAB_IO_H_SERVER_EPOCH,
|
|
57
|
+
SAB_IO_H_SERVER_STATE,
|
|
58
|
+
SAB_IO_INSTANCE_ATTACHED,
|
|
59
|
+
SAB_IO_INSTANCE_NONE,
|
|
60
|
+
SAB_IO_INSTANCE_REVOKED,
|
|
61
|
+
SAB_IO_INSTANCE_REVOKING,
|
|
62
|
+
SAB_IO_OP_CLOSE,
|
|
63
|
+
SAB_IO_OP_OPEN,
|
|
64
|
+
SAB_IO_OP_READ,
|
|
65
|
+
SAB_IO_OP_SIZE,
|
|
66
|
+
SAB_IO_OP_SYNC,
|
|
67
|
+
SAB_IO_OP_TRUNCATE,
|
|
68
|
+
SAB_IO_OP_WRITE,
|
|
69
|
+
SAB_IO_SCRATCH_INSTANCE,
|
|
70
|
+
SAB_IO_SERVER_RUNNING,
|
|
71
|
+
SAB_IO_SERVER_STOPPED,
|
|
72
|
+
SAB_IO_SLOT_PENDING,
|
|
73
|
+
SAB_IO_SLOT_SERVICING,
|
|
74
|
+
SAB_IO_S_DATA_MODE,
|
|
75
|
+
SAB_IO_S_F64_OFFSET,
|
|
76
|
+
SAB_IO_S_FLAGS,
|
|
77
|
+
SAB_IO_S_HANDLE,
|
|
78
|
+
SAB_IO_S_INSTANCE,
|
|
79
|
+
SAB_IO_S_LEN,
|
|
80
|
+
SAB_IO_S_OP,
|
|
81
|
+
SAB_IO_S_PATH_LEN,
|
|
82
|
+
SAB_IO_S_PTR,
|
|
83
|
+
SAB_IO_S_STATE,
|
|
84
|
+
completeSabIoSlot,
|
|
85
|
+
describeSabIoChannel,
|
|
86
|
+
sabIoDoorbellChannelName,
|
|
87
|
+
} from "./sabIoChannel.js";
|
|
88
|
+
import {
|
|
89
|
+
clearSabIoMirror,
|
|
90
|
+
sabIoMirrorEntryBytes,
|
|
91
|
+
updateSabIoMirror,
|
|
92
|
+
} from "./sabIoMirror.js";
|
|
93
|
+
|
|
94
|
+
/** A7: writer I/O workers split writes into chunks of 256 KiB or less. */
|
|
95
|
+
export const DEFAULT_FLATSQL_IO_CHUNK_BYTES = 256 * 1024;
|
|
96
|
+
/** How long an idle server polls for the next request before it sleeps. */
|
|
97
|
+
export const DEFAULT_FLATSQL_IO_SPIN_MICROS = 50;
|
|
98
|
+
/** Longest the loop runs jobs before yielding to its event loop (messages). */
|
|
99
|
+
const RUN_BUDGET_MS = 8;
|
|
100
|
+
/** Bound on one idle wait, so a lost message can never stall the loop. */
|
|
101
|
+
const IDLE_WAIT_MS = 250;
|
|
102
|
+
/** Virtual handle ids are positive i32 below 2^24 (room for routing bits). */
|
|
103
|
+
const MAX_HANDLE_ID = 0xffffff;
|
|
104
|
+
|
|
105
|
+
const STEP_DONE = 0;
|
|
106
|
+
const STEP_AGAIN = 1;
|
|
107
|
+
const STEP_PARKED = 2;
|
|
108
|
+
|
|
109
|
+
function now() {
|
|
110
|
+
return typeof performance !== "undefined" && performance.now
|
|
111
|
+
? performance.now()
|
|
112
|
+
: Date.now();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function makeMacrotaskYield() {
|
|
116
|
+
if (typeof MessageChannel === "function") {
|
|
117
|
+
const channel = new MessageChannel();
|
|
118
|
+
const queue = [];
|
|
119
|
+
channel.port1.onmessage = () => queue.shift()?.();
|
|
120
|
+
// Node: keep the port from holding the worker open on its own.
|
|
121
|
+
channel.port1.unref?.();
|
|
122
|
+
channel.port2.unref?.();
|
|
123
|
+
return () =>
|
|
124
|
+
new Promise((resolve) => {
|
|
125
|
+
queue.push(resolve);
|
|
126
|
+
channel.port2.postMessage(0);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create the server loop over one channel buffer.
|
|
134
|
+
*
|
|
135
|
+
* @param {object} options
|
|
136
|
+
* @param {SharedArrayBuffer} options.buffer the channel (sabIoChannel.js).
|
|
137
|
+
* @param {object} options.backend `{ kind, openFile(path, components, flags),
|
|
138
|
+
* exists(path, components), remove(path, components) }`; each may return a
|
|
139
|
+
* value or a promise, and throw DOMExceptions / Node errors / status errors.
|
|
140
|
+
* @param {number} [options.chunkBytes=262144] largest read or write step.
|
|
141
|
+
* @param {"auto"|"atomics"|"message"} [options.doorbell="auto"] how clients
|
|
142
|
+
* wake the loop. "auto" uses Atomics.waitAsync when present.
|
|
143
|
+
* @param {object} [options.mirror] `{ buffer, match(path), write }` small-file
|
|
144
|
+
* mirror (sabIoMirror.js, the A7 head mirror). When `write` is not false,
|
|
145
|
+
* writes to matching paths are copied into it; a writer I/O worker writes,
|
|
146
|
+
* reader I/O workers only share the buffer.
|
|
147
|
+
* @param {number} [options.spinMicros=50] poll the doorbell this long before
|
|
148
|
+
* sleeping (0 disables).
|
|
149
|
+
* @param {(event: object) => void} [options.onEvent] diagnostics (never throws).
|
|
150
|
+
*/
|
|
151
|
+
export function createFlatsqlIoServer(options = {}) {
|
|
152
|
+
const layout = describeSabIoChannel(options.buffer);
|
|
153
|
+
const backend = options.backend;
|
|
154
|
+
if (!backend || typeof backend.openFile !== "function") {
|
|
155
|
+
throw new TypeError("createFlatsqlIoServer requires a backend with openFile().");
|
|
156
|
+
}
|
|
157
|
+
const chunkBytes =
|
|
158
|
+
Number.isInteger(options.chunkBytes) && options.chunkBytes > 0
|
|
159
|
+
? options.chunkBytes
|
|
160
|
+
: DEFAULT_FLATSQL_IO_CHUNK_BYTES;
|
|
161
|
+
const requestedDoorbell = options.doorbell ?? "auto";
|
|
162
|
+
const doorbellMode =
|
|
163
|
+
requestedDoorbell === "message" ||
|
|
164
|
+
(requestedDoorbell === "auto" && typeof Atomics.waitAsync !== "function")
|
|
165
|
+
? SAB_IO_DOORBELL_MESSAGE
|
|
166
|
+
: SAB_IO_DOORBELL_ATOMICS;
|
|
167
|
+
const mirror = options.mirror ?? null;
|
|
168
|
+
const spinMicros = Number.isFinite(options.spinMicros)
|
|
169
|
+
? Math.max(0, options.spinMicros)
|
|
170
|
+
: DEFAULT_FLATSQL_IO_SPIN_MICROS;
|
|
171
|
+
const onEvent = typeof options.onEvent === "function" ? options.onEvent : null;
|
|
172
|
+
const yieldMacrotask = makeMacrotaskYield();
|
|
173
|
+
|
|
174
|
+
const memories = new Map();
|
|
175
|
+
const entries = new Map();
|
|
176
|
+
const handles = new Map();
|
|
177
|
+
const handlesByInstance = new Map();
|
|
178
|
+
const pathChains = new Map();
|
|
179
|
+
const ready = [];
|
|
180
|
+
const claimedSlots = new Set();
|
|
181
|
+
let nextHandleId = 1;
|
|
182
|
+
let running = false;
|
|
183
|
+
let loopPromise = null;
|
|
184
|
+
let messageWake = null;
|
|
185
|
+
let doorbellChannel = null;
|
|
186
|
+
let scratch = null;
|
|
187
|
+
let sharedViewsRejected = false;
|
|
188
|
+
|
|
189
|
+
const stats = {
|
|
190
|
+
served: 0,
|
|
191
|
+
opens: 0,
|
|
192
|
+
deferredOpens: 0,
|
|
193
|
+
parked: 0,
|
|
194
|
+
chunks: 0,
|
|
195
|
+
busy: 0,
|
|
196
|
+
revoked: 0,
|
|
197
|
+
sharedViewsRejected: false,
|
|
198
|
+
maxReady: 0,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
function emit(event) {
|
|
202
|
+
if (!onEvent) return;
|
|
203
|
+
try {
|
|
204
|
+
onEvent(event);
|
|
205
|
+
} catch {
|
|
206
|
+
// diagnostics never break the loop
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- waking -------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
function wake() {
|
|
213
|
+
if (doorbellMode === SAB_IO_DOORBELL_ATOMICS) {
|
|
214
|
+
Atomics.add(layout.header, SAB_IO_H_DOORBELL, 1);
|
|
215
|
+
Atomics.notify(layout.header, SAB_IO_H_DOORBELL);
|
|
216
|
+
} else {
|
|
217
|
+
const resolve = messageWake;
|
|
218
|
+
messageWake = null;
|
|
219
|
+
resolve?.();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function waitForWork(observed) {
|
|
224
|
+
if (doorbellMode === SAB_IO_DOORBELL_ATOMICS) {
|
|
225
|
+
const waited = Atomics.waitAsync(layout.header, SAB_IO_H_DOORBELL, observed, IDLE_WAIT_MS);
|
|
226
|
+
if (waited.async) await waited.value;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (Atomics.load(layout.header, SAB_IO_H_DOORBELL) !== observed) return;
|
|
230
|
+
await new Promise((resolve) => {
|
|
231
|
+
const timer = setTimeout(() => {
|
|
232
|
+
if (messageWake === done) messageWake = null;
|
|
233
|
+
resolve();
|
|
234
|
+
}, IDLE_WAIT_MS);
|
|
235
|
+
const done = () => {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
resolve();
|
|
238
|
+
};
|
|
239
|
+
messageWake = done;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---- per-path serialization of namespace operations ----------------------
|
|
244
|
+
|
|
245
|
+
function chainPath(path, task) {
|
|
246
|
+
const previous = pathChains.get(path) ?? Promise.resolve();
|
|
247
|
+
const next = previous.then(task, task);
|
|
248
|
+
const tail = next.then(
|
|
249
|
+
() => undefined,
|
|
250
|
+
() => undefined,
|
|
251
|
+
);
|
|
252
|
+
pathChains.set(path, tail);
|
|
253
|
+
tail.then(() => {
|
|
254
|
+
if (pathChains.get(path) === tail) pathChains.delete(path);
|
|
255
|
+
});
|
|
256
|
+
return next;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---- handles ------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
function allocateHandleId() {
|
|
262
|
+
for (let attempts = 0; attempts < MAX_HANDLE_ID; attempts += 1) {
|
|
263
|
+
const id = nextHandleId;
|
|
264
|
+
nextHandleId = nextHandleId >= MAX_HANDLE_ID ? 1 : nextHandleId + 1;
|
|
265
|
+
if (!handles.has(id)) return id;
|
|
266
|
+
}
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function addHandle(entry, instanceId, flags) {
|
|
271
|
+
const id = allocateHandleId();
|
|
272
|
+
if (id < 0) return FLATSQL_IO_ERR_GENERIC;
|
|
273
|
+
const writeFlag = (flags & FLATSQL_IO_WRITE) !== 0;
|
|
274
|
+
const handle = {
|
|
275
|
+
id,
|
|
276
|
+
entry,
|
|
277
|
+
instanceId,
|
|
278
|
+
canWrite: writeFlag,
|
|
279
|
+
// The Go host opens O_WRONLY for WRITE without READ; everything else
|
|
280
|
+
// can read. Every SDK host matches that.
|
|
281
|
+
canRead: !writeFlag || (flags & FLATSQL_IO_READ) !== 0,
|
|
282
|
+
};
|
|
283
|
+
handles.set(id, handle);
|
|
284
|
+
entry.refs += 1;
|
|
285
|
+
if (flags & FLATSQL_IO_DELETE_ON_CLOSE) entry.deleteOnClose = true;
|
|
286
|
+
let set = handlesByInstance.get(instanceId);
|
|
287
|
+
if (!set) {
|
|
288
|
+
set = new Set();
|
|
289
|
+
handlesByInstance.set(instanceId, set);
|
|
290
|
+
}
|
|
291
|
+
set.add(id);
|
|
292
|
+
return id;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function releaseEntry(entry) {
|
|
296
|
+
if (entry.refs > 0 || entry.pins > 0) return;
|
|
297
|
+
if (entries.get(entry.path) === entry) entries.delete(entry.path);
|
|
298
|
+
if (entry.file) {
|
|
299
|
+
try {
|
|
300
|
+
entry.file.close();
|
|
301
|
+
} catch {
|
|
302
|
+
// closing a handle whose context is gone is not an error here
|
|
303
|
+
}
|
|
304
|
+
entry.file = null;
|
|
305
|
+
}
|
|
306
|
+
if (entry.deleteOnClose && entry.openStatus === 0) {
|
|
307
|
+
chainPath(entry.path, async () => {
|
|
308
|
+
try {
|
|
309
|
+
await backend.remove(entry.path, entry.components);
|
|
310
|
+
} catch {
|
|
311
|
+
// advisory
|
|
312
|
+
}
|
|
313
|
+
if (mirror && mirror.write !== false) clearSabIoMirror(mirror.buffer, entry.path);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function closeHandle(handle) {
|
|
319
|
+
if (!handles.delete(handle.id)) return;
|
|
320
|
+
handlesByInstance.get(handle.instanceId)?.delete(handle.id);
|
|
321
|
+
handle.entry.refs -= 1;
|
|
322
|
+
releaseEntry(handle.entry);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---- memory -------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
function instanceState(instanceId) {
|
|
328
|
+
if (instanceId === SAB_IO_SCRATCH_INSTANCE) return SAB_IO_INSTANCE_ATTACHED;
|
|
329
|
+
if (instanceId < 0 || instanceId >= layout.maxInstances) return -1;
|
|
330
|
+
return Atomics.load(layout.instances, instanceId);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isRevoked(instanceId) {
|
|
334
|
+
const state = instanceState(instanceId);
|
|
335
|
+
return state === SAB_IO_INSTANCE_REVOKING || state === SAB_IO_INSTANCE_REVOKED;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function resolveView(job) {
|
|
339
|
+
const { slot, ptr, len } = job;
|
|
340
|
+
if (len < 0 || ptr < 0) return null;
|
|
341
|
+
if (job.dataMode === SAB_IO_DATA_SLOT) {
|
|
342
|
+
if (ptr + len > slot.data.length) return null;
|
|
343
|
+
return slot.data.subarray(ptr, ptr + len);
|
|
344
|
+
}
|
|
345
|
+
const memory = memories.get(job.instanceId);
|
|
346
|
+
// Re-read memory.buffer on every request: a grown memory has a new buffer.
|
|
347
|
+
const buffer = memory?.buffer ?? memory;
|
|
348
|
+
if (!buffer || ptr + len > buffer.byteLength) return null;
|
|
349
|
+
return new Uint8Array(buffer, ptr, len);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function scratchFor(length) {
|
|
353
|
+
if (!scratch || scratch.length < length) {
|
|
354
|
+
scratch = new Uint8Array(Math.max(length, chunkBytes));
|
|
355
|
+
}
|
|
356
|
+
return scratch.subarray(0, length);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Some user agents reject views over a SharedArrayBuffer in sync-handle
|
|
360
|
+
// read/write. Detect that once and move bytes through a private scratch.
|
|
361
|
+
function fileRead(file, view, at) {
|
|
362
|
+
if (!sharedViewsRejected) {
|
|
363
|
+
try {
|
|
364
|
+
return file.read(view, { at });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (!(error instanceof TypeError) || !(view.buffer instanceof SharedArrayBuffer)) {
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
sharedViewsRejected = true;
|
|
370
|
+
stats.sharedViewsRejected = true;
|
|
371
|
+
emit({ type: "shared-views-rejected" });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const temp = scratchFor(view.length);
|
|
375
|
+
const n = file.read(temp, { at });
|
|
376
|
+
view.set(temp.subarray(0, n), 0);
|
|
377
|
+
return n;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function fileWrite(file, view, at) {
|
|
381
|
+
if (!sharedViewsRejected) {
|
|
382
|
+
try {
|
|
383
|
+
return file.write(view, { at });
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (!(error instanceof TypeError) || !(view.buffer instanceof SharedArrayBuffer)) {
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
sharedViewsRejected = true;
|
|
389
|
+
stats.sharedViewsRejected = true;
|
|
390
|
+
emit({ type: "shared-views-rejected" });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const temp = scratchFor(view.length);
|
|
394
|
+
temp.set(view, 0);
|
|
395
|
+
return file.write(temp, { at });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---- jobs ---------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
function complete(job, status, f64 = 0) {
|
|
401
|
+
claimedSlots.delete(job.slot.index);
|
|
402
|
+
stats.served += 1;
|
|
403
|
+
Atomics.add(layout.header, SAB_IO_H_SERVED, 1);
|
|
404
|
+
completeSabIoSlot(layout, job.slot, status, f64);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function makeJob(slot) {
|
|
408
|
+
const i32 = slot.i32;
|
|
409
|
+
return {
|
|
410
|
+
slot,
|
|
411
|
+
op: i32[SAB_IO_S_OP],
|
|
412
|
+
instanceId: i32[SAB_IO_S_INSTANCE],
|
|
413
|
+
handleId: i32[SAB_IO_S_HANDLE],
|
|
414
|
+
flags: i32[SAB_IO_S_FLAGS],
|
|
415
|
+
ptr: i32[SAB_IO_S_PTR],
|
|
416
|
+
len: i32[SAB_IO_S_LEN],
|
|
417
|
+
dataMode: i32[SAB_IO_S_DATA_MODE],
|
|
418
|
+
pathLen: i32[SAB_IO_S_PATH_LEN],
|
|
419
|
+
offset: slot.f64[SAB_IO_S_F64_OFFSET],
|
|
420
|
+
done: 0,
|
|
421
|
+
phase: 0,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function scanSlots() {
|
|
426
|
+
let found = 0;
|
|
427
|
+
for (const slot of layout.slots) {
|
|
428
|
+
if (claimedSlots.has(slot.index)) continue;
|
|
429
|
+
if (Atomics.load(slot.i32, SAB_IO_S_STATE) !== SAB_IO_SLOT_PENDING) continue;
|
|
430
|
+
if (
|
|
431
|
+
Atomics.compareExchange(
|
|
432
|
+
slot.i32,
|
|
433
|
+
SAB_IO_S_STATE,
|
|
434
|
+
SAB_IO_SLOT_PENDING,
|
|
435
|
+
SAB_IO_SLOT_SERVICING,
|
|
436
|
+
) !== SAB_IO_SLOT_PENDING
|
|
437
|
+
) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
claimedSlots.add(slot.index);
|
|
441
|
+
ready.push(makeJob(slot));
|
|
442
|
+
found += 1;
|
|
443
|
+
}
|
|
444
|
+
if (ready.length > stats.maxReady) stats.maxReady = ready.length;
|
|
445
|
+
return found;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function applyRevocations() {
|
|
449
|
+
for (let id = 0; id < layout.maxInstances; id += 1) {
|
|
450
|
+
if (Atomics.load(layout.instances, id) !== SAB_IO_INSTANCE_REVOKING) continue;
|
|
451
|
+
const owned = handlesByInstance.get(id);
|
|
452
|
+
if (owned) {
|
|
453
|
+
for (const handleId of Array.from(owned)) {
|
|
454
|
+
const handle = handles.get(handleId);
|
|
455
|
+
if (handle) closeHandle(handle);
|
|
456
|
+
}
|
|
457
|
+
handlesByInstance.delete(id);
|
|
458
|
+
}
|
|
459
|
+
memories.delete(id);
|
|
460
|
+
// Requests of the instance still queued or parked fail at their next
|
|
461
|
+
// step (every step checks isRevoked), so nothing of it runs after this.
|
|
462
|
+
stats.revoked += 1;
|
|
463
|
+
Atomics.store(layout.instances, id, SAB_IO_INSTANCE_REVOKED);
|
|
464
|
+
Atomics.notify(layout.instances, id);
|
|
465
|
+
emit({ type: "revoked", instanceId: id });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parkOnEntry(job, entry) {
|
|
470
|
+
stats.parked += 1;
|
|
471
|
+
entry.waiters.push(job);
|
|
472
|
+
return STEP_PARKED;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function settleEntry(entry, status, file) {
|
|
476
|
+
entry.opening = null;
|
|
477
|
+
entry.openStatus = status;
|
|
478
|
+
entry.file = file ?? null;
|
|
479
|
+
if (status === 0 && mirror && mirror.write !== false && mirror.match(entry.path) && entry.file) {
|
|
480
|
+
// Seed the mirror with the file's current bytes, so a reader that
|
|
481
|
+
// consults it sees exactly what a read of the file would return.
|
|
482
|
+
try {
|
|
483
|
+
const size = entry.file.getSize();
|
|
484
|
+
const seed = new Uint8Array(Math.min(size, sabIoMirrorEntryBytes(mirror.buffer)));
|
|
485
|
+
if (seed.length > 0) fileRead(entry.file, seed, 0);
|
|
486
|
+
updateSabIoMirror(mirror.buffer, entry.path, 0, seed, { truncateTo: size });
|
|
487
|
+
} catch {
|
|
488
|
+
// a mirror that cannot be seeded stays absent; readers use the file
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const waiters = entry.waiters;
|
|
492
|
+
entry.waiters = [];
|
|
493
|
+
for (const job of waiters) {
|
|
494
|
+
if (job.op === SAB_IO_OP_OPEN && job.phase === 1 && job.entry === entry) {
|
|
495
|
+
// The opener that created this entry: hand out its handle now, so no
|
|
496
|
+
// namespace operation can slip between the open and the handle.
|
|
497
|
+
if (status < 0) job.result = status;
|
|
498
|
+
else if (isRevoked(job.instanceId)) job.result = FLATSQL_IO_ERR_ACCESS;
|
|
499
|
+
else job.result = addHandle(entry, job.instanceId, job.flags);
|
|
500
|
+
job.phase = 2;
|
|
501
|
+
}
|
|
502
|
+
ready.push(job);
|
|
503
|
+
}
|
|
504
|
+
if (status < 0 && entry.refs === 0 && entry.pins === 0 && entries.get(entry.path) === entry) {
|
|
505
|
+
entries.delete(entry.path);
|
|
506
|
+
} else if (status === 0 && entry.refs === 0 && entry.pins === 0) {
|
|
507
|
+
// Nobody took a handle (its opener was revoked meanwhile): close the
|
|
508
|
+
// file, or it would pin the path against UNLINK_IF_UNUSED forever.
|
|
509
|
+
// Openers still in the ready queue simply open it again.
|
|
510
|
+
releaseEntry(entry);
|
|
511
|
+
}
|
|
512
|
+
wake();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function startOpen(entry, flags) {
|
|
516
|
+
stats.opens += 1;
|
|
517
|
+
entry.opening = chainPath(entry.path, async () => {
|
|
518
|
+
try {
|
|
519
|
+
const file = await backend.openFile(entry.path, entry.components, flags);
|
|
520
|
+
settleEntry(entry, 0, file);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
settleEntry(entry, flatsqlIoStatusOf(error, { during: "open" }), null);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function stepOpen(job) {
|
|
528
|
+
if (job.phase === 2) {
|
|
529
|
+
// Parked on a namespace operation that has now settled.
|
|
530
|
+
complete(job, job.result);
|
|
531
|
+
return STEP_DONE;
|
|
532
|
+
}
|
|
533
|
+
if (isRevoked(job.instanceId)) {
|
|
534
|
+
complete(job, FLATSQL_IO_ERR_ACCESS);
|
|
535
|
+
return STEP_DONE;
|
|
536
|
+
}
|
|
537
|
+
if (job.path === undefined) {
|
|
538
|
+
const bytes = job.slot.data.slice(0, Math.max(0, job.pathLen));
|
|
539
|
+
const path = decodeFlatsqlIoPath(bytes);
|
|
540
|
+
if (typeof path !== "string") {
|
|
541
|
+
complete(job, path);
|
|
542
|
+
return STEP_DONE;
|
|
543
|
+
}
|
|
544
|
+
const components = splitFlatsqlIoPath(path);
|
|
545
|
+
if (!Array.isArray(components)) {
|
|
546
|
+
complete(job, components);
|
|
547
|
+
return STEP_DONE;
|
|
548
|
+
}
|
|
549
|
+
job.path = components.join("/");
|
|
550
|
+
job.components = components;
|
|
551
|
+
}
|
|
552
|
+
const { path, components, flags } = job;
|
|
553
|
+
const entry = entries.get(path);
|
|
554
|
+
|
|
555
|
+
if (flags & (FLATSQL_IO_PROBE | FLATSQL_IO_UNLINK | FLATSQL_IO_UNLINK_IF_UNUSED)) {
|
|
556
|
+
if (entry?.opening) return parkOnEntry(job, entry);
|
|
557
|
+
if (flags & FLATSQL_IO_PROBE) {
|
|
558
|
+
if (entry && entry.openStatus === 0) {
|
|
559
|
+
complete(job, 0);
|
|
560
|
+
return STEP_DONE;
|
|
561
|
+
}
|
|
562
|
+
return runNamespace(job, async () => {
|
|
563
|
+
const exists = await backend.exists(path, components);
|
|
564
|
+
return exists ? 0 : FLATSQL_IO_ERR_NOENT;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
// UNLINK / UNLINK_IF_UNUSED
|
|
568
|
+
if (entry && entry.refs > 0) {
|
|
569
|
+
// OPFS cannot remove a file with an open sync handle, so a plain
|
|
570
|
+
// UNLINK of an in-use path is BUSY here too (POSIX hosts allow it).
|
|
571
|
+
stats.busy += 1;
|
|
572
|
+
complete(job, FLATSQL_IO_ERR_BUSY);
|
|
573
|
+
return STEP_DONE;
|
|
574
|
+
}
|
|
575
|
+
if (entry && entry.pins > 0) {
|
|
576
|
+
entry.pins = 0;
|
|
577
|
+
releaseEntry(entry);
|
|
578
|
+
}
|
|
579
|
+
return runNamespace(job, async () => {
|
|
580
|
+
await backend.remove(path, components);
|
|
581
|
+
if (mirror && mirror.write !== false) clearSabIoMirror(mirror.buffer, path);
|
|
582
|
+
return 0;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (entry) {
|
|
587
|
+
if (entry.opening) {
|
|
588
|
+
if (flags & FLATSQL_IO_OPEN_DEFERRED) {
|
|
589
|
+
stats.deferredOpens += 1;
|
|
590
|
+
const id = addHandle(entry, job.instanceId, flags);
|
|
591
|
+
complete(job, id);
|
|
592
|
+
return STEP_DONE;
|
|
593
|
+
}
|
|
594
|
+
return parkOnEntry(job, entry);
|
|
595
|
+
}
|
|
596
|
+
if (entry.openStatus === 0) {
|
|
597
|
+
if (flags & FLATSQL_IO_EXCL) {
|
|
598
|
+
complete(job, FLATSQL_IO_ERR_GENERIC);
|
|
599
|
+
return STEP_DONE;
|
|
600
|
+
}
|
|
601
|
+
if (flags & FLATSQL_IO_TRUNC && flags & FLATSQL_IO_WRITE) {
|
|
602
|
+
try {
|
|
603
|
+
entry.file.truncate(0);
|
|
604
|
+
if (mirror && mirror.write !== false && mirror.match(path)) {
|
|
605
|
+
updateSabIoMirror(mirror.buffer, path, 0, new Uint8Array(0), { truncateTo: 0 });
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
complete(job, flatsqlIoStatusOf(error));
|
|
609
|
+
return STEP_DONE;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const id = addHandle(entry, job.instanceId, flags);
|
|
613
|
+
complete(job, id);
|
|
614
|
+
return STEP_DONE;
|
|
615
|
+
}
|
|
616
|
+
// A failed deferred open still referenced by handles: start afresh.
|
|
617
|
+
if (entries.get(path) === entry) entries.delete(path);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const fresh = {
|
|
621
|
+
path,
|
|
622
|
+
components,
|
|
623
|
+
file: null,
|
|
624
|
+
opening: null,
|
|
625
|
+
openStatus: 0,
|
|
626
|
+
refs: 0,
|
|
627
|
+
pins: 0,
|
|
628
|
+
deleteOnClose: false,
|
|
629
|
+
waiters: [],
|
|
630
|
+
};
|
|
631
|
+
entries.set(path, fresh);
|
|
632
|
+
startOpen(fresh, flags);
|
|
633
|
+
if (flags & FLATSQL_IO_OPEN_DEFERRED) {
|
|
634
|
+
stats.deferredOpens += 1;
|
|
635
|
+
const id = addHandle(fresh, job.instanceId, flags);
|
|
636
|
+
complete(job, id);
|
|
637
|
+
return STEP_DONE;
|
|
638
|
+
}
|
|
639
|
+
job.phase = 1;
|
|
640
|
+
job.entry = fresh;
|
|
641
|
+
return parkOnEntry(job, fresh);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function runNamespace(job, task) {
|
|
645
|
+
job.phase = 2;
|
|
646
|
+
chainPath(job.path, async () => {
|
|
647
|
+
try {
|
|
648
|
+
job.result = await task();
|
|
649
|
+
} catch (error) {
|
|
650
|
+
job.result = flatsqlIoStatusOf(error);
|
|
651
|
+
}
|
|
652
|
+
ready.push(job);
|
|
653
|
+
wake();
|
|
654
|
+
});
|
|
655
|
+
stats.parked += 1;
|
|
656
|
+
return STEP_PARKED;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function stepHandleOp(job) {
|
|
660
|
+
if (isRevoked(job.instanceId)) {
|
|
661
|
+
complete(job, FLATSQL_IO_ERR_ACCESS);
|
|
662
|
+
return STEP_DONE;
|
|
663
|
+
}
|
|
664
|
+
const handle = handles.get(job.handleId);
|
|
665
|
+
if (!handle) {
|
|
666
|
+
complete(job, FLATSQL_IO_ERR_BADHANDLE);
|
|
667
|
+
return STEP_DONE;
|
|
668
|
+
}
|
|
669
|
+
const entry = handle.entry;
|
|
670
|
+
if (entry.opening) return parkOnEntry(job, entry);
|
|
671
|
+
if (job.op === SAB_IO_OP_CLOSE) {
|
|
672
|
+
closeHandle(handle);
|
|
673
|
+
complete(job, 0);
|
|
674
|
+
return STEP_DONE;
|
|
675
|
+
}
|
|
676
|
+
if (entry.openStatus < 0 || !entry.file) {
|
|
677
|
+
complete(job, entry.openStatus < 0 ? entry.openStatus : FLATSQL_IO_ERR_BADHANDLE);
|
|
678
|
+
return STEP_DONE;
|
|
679
|
+
}
|
|
680
|
+
const file = entry.file;
|
|
681
|
+
try {
|
|
682
|
+
switch (job.op) {
|
|
683
|
+
case SAB_IO_OP_READ: {
|
|
684
|
+
if (!handle.canRead) {
|
|
685
|
+
complete(job, FLATSQL_IO_ERR_IO);
|
|
686
|
+
return STEP_DONE;
|
|
687
|
+
}
|
|
688
|
+
const view = resolveView(job);
|
|
689
|
+
if (!view) {
|
|
690
|
+
complete(job, FLATSQL_IO_ERR_GENERIC);
|
|
691
|
+
return STEP_DONE;
|
|
692
|
+
}
|
|
693
|
+
const piece = Math.min(chunkBytes, job.len - job.done);
|
|
694
|
+
const n = fileRead(file, view.subarray(job.done, job.done + piece), job.offset + job.done);
|
|
695
|
+
stats.chunks += 1;
|
|
696
|
+
job.done += n;
|
|
697
|
+
if (n < piece || job.done >= job.len) {
|
|
698
|
+
complete(job, job.done);
|
|
699
|
+
return STEP_DONE;
|
|
700
|
+
}
|
|
701
|
+
return STEP_AGAIN;
|
|
702
|
+
}
|
|
703
|
+
case SAB_IO_OP_WRITE: {
|
|
704
|
+
if (!handle.canWrite) {
|
|
705
|
+
complete(job, FLATSQL_IO_ERR_IO);
|
|
706
|
+
return STEP_DONE;
|
|
707
|
+
}
|
|
708
|
+
const view = resolveView(job);
|
|
709
|
+
if (!view) {
|
|
710
|
+
complete(job, FLATSQL_IO_ERR_GENERIC);
|
|
711
|
+
return STEP_DONE;
|
|
712
|
+
}
|
|
713
|
+
const piece = Math.min(chunkBytes, job.len - job.done);
|
|
714
|
+
const chunk = view.subarray(job.done, job.done + piece);
|
|
715
|
+
const at = job.offset + job.done;
|
|
716
|
+
const n = fileWrite(file, chunk, at);
|
|
717
|
+
stats.chunks += 1;
|
|
718
|
+
if (mirror && mirror.write !== false && mirror.match(entry.path)) {
|
|
719
|
+
updateSabIoMirror(mirror.buffer, entry.path, at, chunk.subarray(0, n));
|
|
720
|
+
}
|
|
721
|
+
job.done += n;
|
|
722
|
+
if (n < piece || job.done >= job.len) {
|
|
723
|
+
complete(job, job.done);
|
|
724
|
+
return STEP_DONE;
|
|
725
|
+
}
|
|
726
|
+
return STEP_AGAIN;
|
|
727
|
+
}
|
|
728
|
+
case SAB_IO_OP_TRUNCATE: {
|
|
729
|
+
if (!handle.canWrite) {
|
|
730
|
+
complete(job, FLATSQL_IO_ERR_IO);
|
|
731
|
+
return STEP_DONE;
|
|
732
|
+
}
|
|
733
|
+
file.truncate(job.offset);
|
|
734
|
+
if (mirror && mirror.write !== false && mirror.match(entry.path)) {
|
|
735
|
+
updateSabIoMirror(mirror.buffer, entry.path, 0, new Uint8Array(0), {
|
|
736
|
+
truncateTo: job.offset,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
complete(job, 0);
|
|
740
|
+
return STEP_DONE;
|
|
741
|
+
}
|
|
742
|
+
case SAB_IO_OP_SYNC:
|
|
743
|
+
file.flush();
|
|
744
|
+
complete(job, 0);
|
|
745
|
+
return STEP_DONE;
|
|
746
|
+
case SAB_IO_OP_SIZE:
|
|
747
|
+
complete(job, 0, file.getSize());
|
|
748
|
+
return STEP_DONE;
|
|
749
|
+
default:
|
|
750
|
+
complete(job, FLATSQL_IO_ERR_GENERIC);
|
|
751
|
+
return STEP_DONE;
|
|
752
|
+
}
|
|
753
|
+
} catch (error) {
|
|
754
|
+
const status = flatsqlIoStatusOf(error);
|
|
755
|
+
complete(job, job.done > 0 && job.op === SAB_IO_OP_READ ? job.done : status);
|
|
756
|
+
return STEP_DONE;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function step(job) {
|
|
761
|
+
try {
|
|
762
|
+
if (job.op === SAB_IO_OP_OPEN) {
|
|
763
|
+
return stepOpen(job);
|
|
764
|
+
}
|
|
765
|
+
if (
|
|
766
|
+
job.op === SAB_IO_OP_READ ||
|
|
767
|
+
job.op === SAB_IO_OP_WRITE ||
|
|
768
|
+
job.op === SAB_IO_OP_TRUNCATE ||
|
|
769
|
+
job.op === SAB_IO_OP_SYNC ||
|
|
770
|
+
job.op === SAB_IO_OP_SIZE ||
|
|
771
|
+
job.op === SAB_IO_OP_CLOSE
|
|
772
|
+
) {
|
|
773
|
+
return stepHandleOp(job);
|
|
774
|
+
}
|
|
775
|
+
complete(job, FLATSQL_IO_ERR_GENERIC);
|
|
776
|
+
return STEP_DONE;
|
|
777
|
+
} catch (error) {
|
|
778
|
+
// A host bug must never leave a guest blocked.
|
|
779
|
+
emit({ type: "step-error", error });
|
|
780
|
+
complete(job, FLATSQL_IO_ERR_IO);
|
|
781
|
+
return STEP_DONE;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function runReady() {
|
|
786
|
+
const started = now();
|
|
787
|
+
while (ready.length > 0) {
|
|
788
|
+
const job = ready.shift();
|
|
789
|
+
const outcome = step(job);
|
|
790
|
+
if (outcome === STEP_AGAIN) ready.push(job);
|
|
791
|
+
applyRevocations();
|
|
792
|
+
scanSlots();
|
|
793
|
+
if (now() - started > RUN_BUDGET_MS) return false;
|
|
794
|
+
}
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function loop() {
|
|
799
|
+
Atomics.store(layout.header, SAB_IO_H_DOORBELL_MODE, doorbellMode);
|
|
800
|
+
Atomics.add(layout.header, SAB_IO_H_SERVER_EPOCH, 1);
|
|
801
|
+
Atomics.store(layout.header, SAB_IO_H_SERVER_STATE, SAB_IO_SERVER_RUNNING);
|
|
802
|
+
while (running) {
|
|
803
|
+
const observed = Atomics.load(layout.header, SAB_IO_H_DOORBELL);
|
|
804
|
+
applyRevocations();
|
|
805
|
+
scanSlots();
|
|
806
|
+
const drained = runReady();
|
|
807
|
+
if (!running) break;
|
|
808
|
+
if (!drained || ready.length > 0) {
|
|
809
|
+
await yieldMacrotask();
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
// Adaptive wait: poll the doorbell briefly before sleeping. Back-to-back
|
|
813
|
+
// requests (a lane reading index blocks) are then served without a
|
|
814
|
+
// thread wake-up, which on a busy machine costs far more than the spin.
|
|
815
|
+
if (spinMicros > 0) {
|
|
816
|
+
const until = now() + spinMicros / 1000;
|
|
817
|
+
while (now() < until && Atomics.load(layout.header, SAB_IO_H_DOORBELL) === observed) {
|
|
818
|
+
// spin
|
|
819
|
+
}
|
|
820
|
+
if (Atomics.load(layout.header, SAB_IO_H_DOORBELL) !== observed) continue;
|
|
821
|
+
}
|
|
822
|
+
await waitForWork(observed);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
doorbellMode,
|
|
828
|
+
stats,
|
|
829
|
+
/** Start the loop. Resolves when it stops. */
|
|
830
|
+
start() {
|
|
831
|
+
if (running) return loopPromise;
|
|
832
|
+
running = true;
|
|
833
|
+
if (doorbellMode === SAB_IO_DOORBELL_MESSAGE && typeof BroadcastChannel === "function") {
|
|
834
|
+
doorbellChannel = new BroadcastChannel(sabIoDoorbellChannelName(layout.channelId));
|
|
835
|
+
doorbellChannel.onmessage = () => {
|
|
836
|
+
const resolve = messageWake;
|
|
837
|
+
messageWake = null;
|
|
838
|
+
resolve?.();
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
loopPromise = loop().catch((error) => {
|
|
842
|
+
emit({ type: "loop-error", error });
|
|
843
|
+
throw error;
|
|
844
|
+
});
|
|
845
|
+
return loopPromise;
|
|
846
|
+
},
|
|
847
|
+
/** Stop the loop and close every file. */
|
|
848
|
+
async stop() {
|
|
849
|
+
running = false;
|
|
850
|
+
wake();
|
|
851
|
+
try {
|
|
852
|
+
await loopPromise;
|
|
853
|
+
} catch {
|
|
854
|
+
// already reported
|
|
855
|
+
}
|
|
856
|
+
doorbellChannel?.close();
|
|
857
|
+
doorbellChannel = null;
|
|
858
|
+
for (const entry of entries.values()) {
|
|
859
|
+
try {
|
|
860
|
+
entry.file?.close();
|
|
861
|
+
} catch {
|
|
862
|
+
// ignore
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
entries.clear();
|
|
866
|
+
handles.clear();
|
|
867
|
+
handlesByInstance.clear();
|
|
868
|
+
Atomics.store(layout.header, SAB_IO_H_SERVER_STATE, SAB_IO_SERVER_STOPPED);
|
|
869
|
+
},
|
|
870
|
+
/**
|
|
871
|
+
* Attach an instance's shared memory, so its requests move bytes directly
|
|
872
|
+
* between that memory and the file. Refused for a revoked id (reset it
|
|
873
|
+
* first with resetSabIoInstance).
|
|
874
|
+
*/
|
|
875
|
+
attachMemory(instanceId, memory) {
|
|
876
|
+
if (!Number.isInteger(instanceId) || instanceId < 0 || instanceId >= layout.maxInstances) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
const state = Atomics.load(layout.instances, instanceId);
|
|
880
|
+
if (state === SAB_IO_INSTANCE_REVOKING || state === SAB_IO_INSTANCE_REVOKED) return false;
|
|
881
|
+
memories.set(instanceId, memory);
|
|
882
|
+
Atomics.store(layout.instances, instanceId, SAB_IO_INSTANCE_ATTACHED);
|
|
883
|
+
return true;
|
|
884
|
+
},
|
|
885
|
+
detachMemory(instanceId) {
|
|
886
|
+
memories.delete(instanceId);
|
|
887
|
+
if (Atomics.load(layout.instances, instanceId) === SAB_IO_INSTANCE_ATTACHED) {
|
|
888
|
+
Atomics.store(layout.instances, instanceId, SAB_IO_INSTANCE_NONE);
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
/**
|
|
892
|
+
* Pre-open paths (A38 "at start, the I/O workers open every active file
|
|
893
|
+
* the registry names in parallel"; §5.5 next-segment pre-open). The files
|
|
894
|
+
* stay open, pinned, until releasePreopen or an UNLINK; a guest open of a
|
|
895
|
+
* pre-opened path returns at once. Resolves to one status per path.
|
|
896
|
+
*/
|
|
897
|
+
async preopen(paths, flags = FLATSQL_IO_READ | FLATSQL_IO_WRITE | FLATSQL_IO_CREATE) {
|
|
898
|
+
const results = await Promise.all(
|
|
899
|
+
paths.map(async (rawPath) => {
|
|
900
|
+
const components = splitFlatsqlIoPath(String(rawPath));
|
|
901
|
+
if (!Array.isArray(components)) return components;
|
|
902
|
+
const path = components.join("/");
|
|
903
|
+
let entry = entries.get(path);
|
|
904
|
+
if (!entry) {
|
|
905
|
+
entry = {
|
|
906
|
+
path,
|
|
907
|
+
components,
|
|
908
|
+
file: null,
|
|
909
|
+
opening: null,
|
|
910
|
+
openStatus: 0,
|
|
911
|
+
refs: 0,
|
|
912
|
+
pins: 0,
|
|
913
|
+
deleteOnClose: false,
|
|
914
|
+
waiters: [],
|
|
915
|
+
};
|
|
916
|
+
entries.set(path, entry);
|
|
917
|
+
startOpen(entry, flags & ~FLATSQL_IO_OPEN_DEFERRED);
|
|
918
|
+
}
|
|
919
|
+
entry.pins += 1;
|
|
920
|
+
if (entry.opening) await entry.opening;
|
|
921
|
+
if (entry.openStatus < 0) {
|
|
922
|
+
entry.pins = Math.max(0, entry.pins - 1);
|
|
923
|
+
return entry.openStatus;
|
|
924
|
+
}
|
|
925
|
+
return 0;
|
|
926
|
+
}),
|
|
927
|
+
);
|
|
928
|
+
wake();
|
|
929
|
+
return results;
|
|
930
|
+
},
|
|
931
|
+
/** Drop pre-open pins; files with no handle close. */
|
|
932
|
+
releasePreopen(paths) {
|
|
933
|
+
for (const rawPath of paths) {
|
|
934
|
+
const components = splitFlatsqlIoPath(String(rawPath));
|
|
935
|
+
if (!Array.isArray(components)) continue;
|
|
936
|
+
const entry = entries.get(components.join("/"));
|
|
937
|
+
if (entry && entry.pins > 0) {
|
|
938
|
+
entry.pins -= 1;
|
|
939
|
+
releaseEntry(entry);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
/** Open files and handles, for diagnostics and tests. */
|
|
944
|
+
inventory() {
|
|
945
|
+
return {
|
|
946
|
+
files: Array.from(entries.values()).map((entry) => ({
|
|
947
|
+
path: entry.path,
|
|
948
|
+
refs: entry.refs,
|
|
949
|
+
pins: entry.pins,
|
|
950
|
+
opening: !!entry.opening,
|
|
951
|
+
openStatus: entry.openStatus,
|
|
952
|
+
})),
|
|
953
|
+
handles: handles.size,
|
|
954
|
+
};
|
|
955
|
+
},
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export const FLATSQL_IO_SERVER_CREATE_FLAGS =
|
|
960
|
+
FLATSQL_IO_READ | FLATSQL_IO_WRITE | FLATSQL_IO_CREATE | FLATSQL_IO_CREATE_PARENTS;
|