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,280 @@
|
|
|
1
|
+
// OPFS backend for the FlatSQL I/O server (docs/architecture/flatsql-partition-store.md
|
|
2
|
+
// §5.5, A7, A13, 22.3a-6).
|
|
3
|
+
//
|
|
4
|
+
// Every open is async by construction (getDirectoryHandle, getFileHandle,
|
|
5
|
+
// createSyncAccessHandle), and runs inside the I/O worker, which awaits it
|
|
6
|
+
// without blocking any other guest thread. The files it returns ARE
|
|
7
|
+
// FileSystemSyncAccessHandles, whose read/write/truncate/flush/getSize are
|
|
8
|
+
// synchronous and serve the guest's blocking imports directly.
|
|
9
|
+
//
|
|
10
|
+
// Handle modes (A7). A sync access handle is exclusive per file by default. A
|
|
11
|
+
// reader I/O worker can only read a file the writer I/O worker holds when both
|
|
12
|
+
// open it in "readwrite-unsafe" mode. Chromium supports the mode; Firefox 155
|
|
13
|
+
// and WebKit 26.6 ignore it (measured: the second handle fails). The owner
|
|
14
|
+
// ruled (§22.4-6) that a browser without shared handle modes gets no local
|
|
15
|
+
// store; `probeOpfsSharedHandleModes` is the gate's measurement.
|
|
16
|
+
//
|
|
17
|
+
// Directory durability on OPFS is best effort (22.3a-4); CREATE_PARENTS
|
|
18
|
+
// creates the directories and cannot fsync them.
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
FLATSQL_IO_CREATE,
|
|
22
|
+
FLATSQL_IO_CREATE_PARENTS,
|
|
23
|
+
FLATSQL_IO_ERR_GENERIC,
|
|
24
|
+
FLATSQL_IO_EXCL,
|
|
25
|
+
FLATSQL_IO_TRUNC,
|
|
26
|
+
FlatsqlIoStatusError,
|
|
27
|
+
} from "./flatsqlIoContract.js";
|
|
28
|
+
|
|
29
|
+
export const OPFS_SHARED_HANDLE_MODE = "readwrite-unsafe";
|
|
30
|
+
|
|
31
|
+
function notFound(message) {
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
error.name = "NotFoundError";
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sleep(ms) {
|
|
38
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Does this context support shared sync access handles? Opens two
|
|
43
|
+
* "readwrite-unsafe" handles on one probe file; both must succeed and report
|
|
44
|
+
* the mode. Resolves to { supported, detail }.
|
|
45
|
+
*/
|
|
46
|
+
export async function probeOpfsSharedHandleModes(root, name = ".sdm-mode-probe") {
|
|
47
|
+
let first = null;
|
|
48
|
+
let second = null;
|
|
49
|
+
try {
|
|
50
|
+
const file = await root.getFileHandle(name, { create: true });
|
|
51
|
+
first = await file.createSyncAccessHandle({ mode: OPFS_SHARED_HANDLE_MODE });
|
|
52
|
+
if (first.mode !== OPFS_SHARED_HANDLE_MODE) {
|
|
53
|
+
return { supported: false, detail: "mode option ignored" };
|
|
54
|
+
}
|
|
55
|
+
second = await file.createSyncAccessHandle({ mode: OPFS_SHARED_HANDLE_MODE });
|
|
56
|
+
return { supported: true, detail: "two readwrite-unsafe handles" };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { supported: false, detail: `${error?.name ?? "Error"}: ${error?.message ?? error}` };
|
|
59
|
+
} finally {
|
|
60
|
+
try {
|
|
61
|
+
second?.close();
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
first?.close();
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await root.removeEntry(name);
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Does this context's sync access handle accept views over a
|
|
80
|
+
* SharedArrayBuffer (direct shared-memory transfer)? Resolves to a boolean.
|
|
81
|
+
*/
|
|
82
|
+
export async function probeOpfsSharedViews(root, name = ".sdm-view-probe") {
|
|
83
|
+
let handle = null;
|
|
84
|
+
try {
|
|
85
|
+
const file = await root.getFileHandle(name, { create: true });
|
|
86
|
+
handle = await file.createSyncAccessHandle();
|
|
87
|
+
const shared = new Uint8Array(new SharedArrayBuffer(16));
|
|
88
|
+
shared.fill(0x5a);
|
|
89
|
+
const written = handle.write(shared, { at: 0 });
|
|
90
|
+
const back = new Uint8Array(new SharedArrayBuffer(16));
|
|
91
|
+
const read = handle.read(back, { at: 0 });
|
|
92
|
+
return written === 16 && read === 16 && back[15] === 0x5a;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
} finally {
|
|
96
|
+
try {
|
|
97
|
+
handle?.close();
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await root.removeEntry(name);
|
|
103
|
+
} catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create the OPFS backend.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} [options]
|
|
113
|
+
* @param {string} [options.rootDirectory] a directory below the origin's OPFS
|
|
114
|
+
* root that every path is confined to (created on init).
|
|
115
|
+
* @param {"auto"|"shared"|"exclusive"} [options.handleMode="auto"] "shared"
|
|
116
|
+
* opens every handle "readwrite-unsafe" (requires support), "exclusive"
|
|
117
|
+
* uses the default exclusive handles, "auto" picks shared when supported.
|
|
118
|
+
* @param {(path: string) => number} [options.openDelayMs] test hook: delay an
|
|
119
|
+
* open by this many milliseconds (acceptance: an open that resolves after
|
|
120
|
+
* 500 ms blocks only its caller).
|
|
121
|
+
* @param {FileSystemDirectoryHandle} [options.root] use this directory as the
|
|
122
|
+
* root instead of navigator.storage.getDirectory().
|
|
123
|
+
* @param {{ attempts: number, baseMs?: number }} [options.busyRetry] retry a
|
|
124
|
+
* sync-handle creation refused because another context still holds the file
|
|
125
|
+
* (A37 leader takeover), with exponential backoff. Default: no retry.
|
|
126
|
+
*/
|
|
127
|
+
export function createOpfsIoBackend(options = {}) {
|
|
128
|
+
let root = options.root ?? null;
|
|
129
|
+
let sharedModes = false;
|
|
130
|
+
let sharedViews = null;
|
|
131
|
+
const handleMode = options.handleMode ?? "auto";
|
|
132
|
+
const openDelayMs = typeof options.openDelayMs === "function" ? options.openDelayMs : null;
|
|
133
|
+
const directoryCache = new Map();
|
|
134
|
+
// A37: a new leader retries NoModificationAllowedError (WebKit:
|
|
135
|
+
// InvalidStateError) with backoff while the previous leader's handles close.
|
|
136
|
+
const busyRetry = {
|
|
137
|
+
attempts: Math.max(0, options.busyRetry?.attempts ?? 0),
|
|
138
|
+
baseMs: Math.max(1, options.busyRetry?.baseMs ?? 10),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
async function createAccessHandle(fileHandle) {
|
|
142
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
143
|
+
try {
|
|
144
|
+
return sharedModes
|
|
145
|
+
? await fileHandle.createSyncAccessHandle({ mode: OPFS_SHARED_HANDLE_MODE })
|
|
146
|
+
: await fileHandle.createSyncAccessHandle();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const busy =
|
|
149
|
+
error?.name === "NoModificationAllowedError" || error?.name === "InvalidStateError";
|
|
150
|
+
if (!busy || attempt >= busyRetry.attempts) throw error;
|
|
151
|
+
await sleep(busyRetry.baseMs * 2 ** Math.min(attempt, 8));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function init() {
|
|
157
|
+
if (!root) {
|
|
158
|
+
if (typeof navigator === "undefined" || !navigator.storage?.getDirectory) {
|
|
159
|
+
throw new FlatsqlIoStatusError(FLATSQL_IO_ERR_GENERIC, "OPFS is unavailable here");
|
|
160
|
+
}
|
|
161
|
+
root = await navigator.storage.getDirectory();
|
|
162
|
+
if (options.rootDirectory) {
|
|
163
|
+
for (const part of String(options.rootDirectory).split("/").filter(Boolean)) {
|
|
164
|
+
root = await root.getDirectoryHandle(part, { create: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const probe = await probeOpfsSharedHandleModes(root);
|
|
169
|
+
sharedViews = await probeOpfsSharedViews(root);
|
|
170
|
+
if (handleMode === "shared" && !probe.supported) {
|
|
171
|
+
throw new FlatsqlIoStatusError(
|
|
172
|
+
FLATSQL_IO_ERR_GENERIC,
|
|
173
|
+
`shared OPFS handle modes are unsupported here (${probe.detail})`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
sharedModes = handleMode !== "exclusive" && probe.supported;
|
|
177
|
+
directoryCache.set("", root);
|
|
178
|
+
return { sharedModes, sharedModesDetail: probe.detail, sharedViews };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function directoryFor(components, create) {
|
|
182
|
+
const key = components.join("/");
|
|
183
|
+
const cached = directoryCache.get(key);
|
|
184
|
+
if (cached) return cached;
|
|
185
|
+
let dir = root;
|
|
186
|
+
let prefix = "";
|
|
187
|
+
for (const part of components) {
|
|
188
|
+
prefix = prefix ? `${prefix}/${part}` : part;
|
|
189
|
+
const known = directoryCache.get(prefix);
|
|
190
|
+
if (known) {
|
|
191
|
+
dir = known;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
dir = await dir.getDirectoryHandle(part, { create });
|
|
195
|
+
directoryCache.set(prefix, dir);
|
|
196
|
+
}
|
|
197
|
+
return dir;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function fileHandleIfExists(dir, name) {
|
|
201
|
+
try {
|
|
202
|
+
return await dir.getFileHandle(name);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error?.name === "NotFoundError") return null;
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
kind: "opfs",
|
|
211
|
+
init,
|
|
212
|
+
get sharedModes() {
|
|
213
|
+
return sharedModes;
|
|
214
|
+
},
|
|
215
|
+
get sharedViews() {
|
|
216
|
+
return sharedViews;
|
|
217
|
+
},
|
|
218
|
+
async openFile(path, components, flags) {
|
|
219
|
+
if (!root) await init();
|
|
220
|
+
if (openDelayMs) {
|
|
221
|
+
const delay = openDelayMs(path);
|
|
222
|
+
if (delay > 0) await sleep(delay);
|
|
223
|
+
}
|
|
224
|
+
const create = (flags & FLATSQL_IO_CREATE) !== 0;
|
|
225
|
+
const parents = create && (flags & FLATSQL_IO_CREATE_PARENTS) !== 0;
|
|
226
|
+
const dir = await directoryFor(components.slice(0, -1), parents);
|
|
227
|
+
const name = components[components.length - 1];
|
|
228
|
+
let fileHandle = null;
|
|
229
|
+
if (flags & FLATSQL_IO_EXCL) {
|
|
230
|
+
if (await fileHandleIfExists(dir, name)) {
|
|
231
|
+
throw new FlatsqlIoStatusError(FLATSQL_IO_ERR_GENERIC, `${path} exists`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
fileHandle = await dir.getFileHandle(name, { create });
|
|
235
|
+
const access = await createAccessHandle(fileHandle);
|
|
236
|
+
if (flags & FLATSQL_IO_TRUNC) {
|
|
237
|
+
access.truncate(0);
|
|
238
|
+
}
|
|
239
|
+
return access;
|
|
240
|
+
},
|
|
241
|
+
async exists(path, components) {
|
|
242
|
+
if (!root) await init();
|
|
243
|
+
let dir;
|
|
244
|
+
try {
|
|
245
|
+
dir = await directoryFor(components.slice(0, -1), false);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error?.name === "NotFoundError" || error?.name === "TypeMismatchError") return false;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
const name = components[components.length - 1];
|
|
251
|
+
if (await fileHandleIfExists(dir, name)) return true;
|
|
252
|
+
try {
|
|
253
|
+
await dir.getDirectoryHandle(name);
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async remove(path, components) {
|
|
260
|
+
if (!root) await init();
|
|
261
|
+
let dir;
|
|
262
|
+
try {
|
|
263
|
+
dir = await directoryFor(components.slice(0, -1), false);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error?.name === "TypeMismatchError") throw notFound(path);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
await dir.removeEntry(components[components.length - 1]);
|
|
269
|
+
},
|
|
270
|
+
/** Remove everything below the root (tests, and a store wipe). */
|
|
271
|
+
async clear() {
|
|
272
|
+
if (!root) await init();
|
|
273
|
+
for await (const name of root.keys()) {
|
|
274
|
+
await root.removeEntry(name, { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
directoryCache.clear();
|
|
277
|
+
directoryCache.set("", root);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// The FlatSQL I/O worker entry (docs/architecture/flatsql-partition-store.md
|
|
2
|
+
// §5.5, A7, A36, A38, A39).
|
|
3
|
+
//
|
|
4
|
+
// One dedicated worker that holds every file handle for the guest threads that
|
|
5
|
+
// talk to it over a SAB request ring (sabIoChannel.js). It never blocks: sync
|
|
6
|
+
// reads and writes run directly on OPFS sync access handles, into the guests'
|
|
7
|
+
// shared memory; opens, unlinks and probes are awaited on the event loop while
|
|
8
|
+
// the requesting guest thread waits on its own slot.
|
|
9
|
+
//
|
|
10
|
+
// Roles (A7): a WRITER I/O worker per writer holds that writer's active files
|
|
11
|
+
// and splits writes into chunks of 256 KiB or less; READER I/O workers (1, or 2
|
|
12
|
+
// sharded by path hash when hardwareConcurrency >= 8) hold sealed and immutable
|
|
13
|
+
// files. The role only sets defaults; the protocol is the same.
|
|
14
|
+
//
|
|
15
|
+
// Backends: "opfs" (the local store) or "memory" (the dashboard window store:
|
|
16
|
+
// nothing reaches OPFS, and `reset` drops it on a UI-state change).
|
|
17
|
+
//
|
|
18
|
+
// Runs as a browser Worker (module, or classic from the self-contained blob
|
|
19
|
+
// bundle, A39) and as a Node worker_threads worker (tests, memory backend).
|
|
20
|
+
//
|
|
21
|
+
// Controller messages (see flatsqlIoWorkers.js):
|
|
22
|
+
// {t:"init", buffer, backend, role, chunkBytes, doorbell, mirror, rootDirectory,
|
|
23
|
+
// handleMode, memoryMaxBytes, openDelay, lock, busyRetry}
|
|
24
|
+
// -> {t:"ready", ok, info|error}
|
|
25
|
+
// {t:"attach", instanceId, memory} -> {t:"attached", instanceId, ok}
|
|
26
|
+
// {t:"detach", instanceId}
|
|
27
|
+
// {t:"preopen", id, paths, flags} -> {t:"preopened", id, statuses}
|
|
28
|
+
// {t:"release-preopen", paths}
|
|
29
|
+
// {t:"stats", id} -> {t:"stats", id, stats, inventory}
|
|
30
|
+
// {t:"reset", id} -> {t:"reset", id}
|
|
31
|
+
// {t:"clear", id} -> {t:"cleared", id}
|
|
32
|
+
// {t:"stop", id} -> {t:"stopped", id}
|
|
33
|
+
|
|
34
|
+
import { createFlatsqlIoServer, DEFAULT_FLATSQL_IO_CHUNK_BYTES } from "./flatsqlIoServer.js";
|
|
35
|
+
import { createFlatsqlIoMemoryBackend } from "./flatsqlIoMemoryBackend.js";
|
|
36
|
+
import { createOpfsIoBackend } from "./opfsIoBackend.js";
|
|
37
|
+
import { createSabIoMirrorMatcher } from "./sabIoMirror.js";
|
|
38
|
+
import { NODE_BUILTIN_PREFIX } from "./nodeBuiltinSpecifier.js";
|
|
39
|
+
|
|
40
|
+
const IS_NODE =
|
|
41
|
+
typeof process !== "undefined" && !!process.release && process.release.name === "node";
|
|
42
|
+
|
|
43
|
+
let server = null;
|
|
44
|
+
let backend = null;
|
|
45
|
+
let releaseLock = null;
|
|
46
|
+
|
|
47
|
+
// A37: the store's Web Lock is taken inside the writer I/O worker, so it lives
|
|
48
|
+
// exactly as long as the handles; `stop` closes the handles first, then
|
|
49
|
+
// releases it. Resolves true when held, false when `ifAvailable` found it taken.
|
|
50
|
+
function acquireLock({ name, ifAvailable = false }) {
|
|
51
|
+
if (typeof navigator === "undefined" || !navigator.locks) {
|
|
52
|
+
return Promise.reject(new Error("Web Locks are unavailable in this context"));
|
|
53
|
+
}
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
navigator.locks
|
|
56
|
+
.request(name, { mode: "exclusive", ifAvailable }, (lock) => {
|
|
57
|
+
if (!lock) {
|
|
58
|
+
resolve(false);
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
resolve(true);
|
|
62
|
+
return new Promise((release) => {
|
|
63
|
+
releaseLock = release;
|
|
64
|
+
});
|
|
65
|
+
})
|
|
66
|
+
.catch(reject);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function describeError(error) {
|
|
71
|
+
return `${error?.name ?? "Error"}: ${error?.message ?? String(error)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildOpenDelay(openDelay) {
|
|
75
|
+
if (!openDelay || !openDelay.pattern || !(openDelay.ms > 0)) return undefined;
|
|
76
|
+
const pattern = new RegExp(openDelay.pattern);
|
|
77
|
+
return (path) => (pattern.test(path) ? openDelay.ms : 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleInit(message, post) {
|
|
81
|
+
const role = message.role === "reader" ? "reader" : "writer";
|
|
82
|
+
if (message.lock?.name) {
|
|
83
|
+
const held = await acquireLock(message.lock);
|
|
84
|
+
if (!held) {
|
|
85
|
+
const error = new Error(`the store lock ${message.lock.name} is held by another context`);
|
|
86
|
+
error.name = "LockUnavailableError";
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (message.backend === "memory") {
|
|
91
|
+
backend = createFlatsqlIoMemoryBackend({ maxBytes: message.memoryMaxBytes });
|
|
92
|
+
} else {
|
|
93
|
+
backend = createOpfsIoBackend({
|
|
94
|
+
rootDirectory: message.rootDirectory,
|
|
95
|
+
handleMode: message.handleMode ?? "auto",
|
|
96
|
+
openDelayMs: buildOpenDelay(message.openDelay),
|
|
97
|
+
busyRetry: message.busyRetry ?? undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const info = { backend: backend.kind, role, lock: message.lock?.name ?? null };
|
|
101
|
+
if (backend.init) {
|
|
102
|
+
Object.assign(info, await backend.init());
|
|
103
|
+
}
|
|
104
|
+
const mirror = message.mirror?.buffer
|
|
105
|
+
? {
|
|
106
|
+
buffer: message.mirror.buffer,
|
|
107
|
+
match: createSabIoMirrorMatcher(message.mirror.suffixes ?? ["/h.fsh"]),
|
|
108
|
+
write: message.mirror.write ?? role === "writer",
|
|
109
|
+
}
|
|
110
|
+
: null;
|
|
111
|
+
server = createFlatsqlIoServer({
|
|
112
|
+
buffer: message.buffer,
|
|
113
|
+
backend,
|
|
114
|
+
chunkBytes: message.chunkBytes ?? DEFAULT_FLATSQL_IO_CHUNK_BYTES,
|
|
115
|
+
doorbell: message.doorbell ?? "auto",
|
|
116
|
+
mirror,
|
|
117
|
+
onEvent(event) {
|
|
118
|
+
if (event.type === "loop-error" || event.type === "step-error") {
|
|
119
|
+
post({ t: "event", event: { type: event.type, error: describeError(event.error) } });
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
info.doorbellMode = server.doorbellMode === 1 ? "message" : "atomics";
|
|
124
|
+
// The loop runs until stop; a crash of the loop itself is a worker error the
|
|
125
|
+
// supervisor sees (A36).
|
|
126
|
+
server.start().catch((error) => {
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
throw error;
|
|
129
|
+
}, 0);
|
|
130
|
+
});
|
|
131
|
+
return info;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function dispatch(message, post) {
|
|
135
|
+
switch (message?.t) {
|
|
136
|
+
case "init":
|
|
137
|
+
try {
|
|
138
|
+
const info = await handleInit(message, post);
|
|
139
|
+
post({ t: "ready", ok: true, info });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
releaseLock?.();
|
|
142
|
+
releaseLock = null;
|
|
143
|
+
post({
|
|
144
|
+
t: "ready",
|
|
145
|
+
ok: false,
|
|
146
|
+
error: describeError(error),
|
|
147
|
+
lockUnavailable: error?.name === "LockUnavailableError",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
case "attach":
|
|
152
|
+
post({
|
|
153
|
+
t: "attached",
|
|
154
|
+
instanceId: message.instanceId,
|
|
155
|
+
ok: !!server?.attachMemory(message.instanceId, message.memory),
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
case "detach":
|
|
159
|
+
server?.detachMemory(message.instanceId);
|
|
160
|
+
return;
|
|
161
|
+
case "preopen": {
|
|
162
|
+
const statuses = server ? await server.preopen(message.paths ?? [], message.flags) : [];
|
|
163
|
+
post({ t: "preopened", id: message.id, statuses });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
case "release-preopen":
|
|
167
|
+
server?.releasePreopen(message.paths ?? []);
|
|
168
|
+
return;
|
|
169
|
+
case "stats":
|
|
170
|
+
post({
|
|
171
|
+
t: "stats",
|
|
172
|
+
id: message.id,
|
|
173
|
+
stats: server ? { ...server.stats } : null,
|
|
174
|
+
inventory: server ? server.inventory() : null,
|
|
175
|
+
usage: backend?.usage ? backend.usage() : null,
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
case "reset":
|
|
179
|
+
backend?.reset?.();
|
|
180
|
+
post({ t: "reset", id: message.id });
|
|
181
|
+
return;
|
|
182
|
+
case "clear":
|
|
183
|
+
try {
|
|
184
|
+
await backend?.clear?.();
|
|
185
|
+
post({ t: "cleared", id: message.id, ok: true });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
post({ t: "cleared", id: message.id, ok: false, error: describeError(error) });
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
case "stop":
|
|
191
|
+
await server?.stop();
|
|
192
|
+
server = null;
|
|
193
|
+
// Handles are closed; only now may another context take the store.
|
|
194
|
+
releaseLock?.();
|
|
195
|
+
releaseLock = null;
|
|
196
|
+
post({ t: "stopped", id: message.id });
|
|
197
|
+
return;
|
|
198
|
+
default:
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function startBrowser() {
|
|
203
|
+
const post = (value) => self.postMessage(value);
|
|
204
|
+
self.onmessage = (event) => {
|
|
205
|
+
dispatch(event.data, post);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function startNode() {
|
|
210
|
+
const specifier = NODE_BUILTIN_PREFIX + "worker_threads";
|
|
211
|
+
const { parentPort } = await import(/* @vite-ignore */ /* webpackIgnore: true */ specifier);
|
|
212
|
+
if (!parentPort) return;
|
|
213
|
+
const post = (value) => parentPort.postMessage(value);
|
|
214
|
+
parentPort.on("message", (message) => {
|
|
215
|
+
dispatch(message, post);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (IS_NODE) {
|
|
220
|
+
startNode();
|
|
221
|
+
} else if (typeof self !== "undefined" && typeof self.postMessage === "function" && typeof window === "undefined") {
|
|
222
|
+
startBrowser();
|
|
223
|
+
}
|