@oliphaunt/wasix-ts 0.1.0
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/ARCHITECTURE.md +655 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/lib/archive.d.ts +21 -0
- package/lib/archive.js +336 -0
- package/lib/asset-source.d.ts +4 -0
- package/lib/asset-source.js +15 -0
- package/lib/byte-channel.d.ts +27 -0
- package/lib/byte-channel.js +170 -0
- package/lib/client-common.d.ts +6 -0
- package/lib/client-common.js +36 -0
- package/lib/client.d.ts +7 -0
- package/lib/client.js +24 -0
- package/lib/database-root.d.ts +19 -0
- package/lib/database-root.js +139 -0
- package/lib/database.d.ts +135 -0
- package/lib/database.js +1039 -0
- package/lib/descriptor-validation.d.ts +10 -0
- package/lib/descriptor-validation.js +75 -0
- package/lib/direct-client-common.d.ts +50 -0
- package/lib/direct-client-common.js +717 -0
- package/lib/direct-client.d.ts +4 -0
- package/lib/direct-client.js +13 -0
- package/lib/direct.node.d.ts +2 -0
- package/lib/direct.node.js +2 -0
- package/lib/errors.d.ts +25 -0
- package/lib/errors.js +34 -0
- package/lib/extension-descriptor.d.ts +14 -0
- package/lib/extension-descriptor.js +419 -0
- package/lib/extensions.d.ts +68 -0
- package/lib/extensions.js +769 -0
- package/lib/host/LICENSE +21 -0
- package/lib/host/index.d.mts +123 -0
- package/lib/host/index.mjs +11 -0
- package/lib/host/provenance.json +16 -0
- package/lib/host/wasmer_js_bg.wasm +0 -0
- package/lib/host/worker.mjs +11 -0
- package/lib/host-runtime.d.ts +5 -0
- package/lib/host-runtime.js +13 -0
- package/lib/icu-descriptor.d.ts +3 -0
- package/lib/icu-descriptor.js +92 -0
- package/lib/index.bun.d.ts +2 -0
- package/lib/index.bun.js +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.deno.d.ts +2 -0
- package/lib/index.deno.js +2 -0
- package/lib/index.js +2 -0
- package/lib/index.node.d.ts +2 -0
- package/lib/index.node.js +2 -0
- package/lib/internal-common.d.ts +12 -0
- package/lib/internal-common.js +274 -0
- package/lib/internal.d.ts +5 -0
- package/lib/internal.js +42 -0
- package/lib/internal.node.d.ts +5 -0
- package/lib/internal.node.js +8 -0
- package/lib/native-addon.d.ts +111 -0
- package/lib/native-addon.js +223 -0
- package/lib/native-server.d.ts +21 -0
- package/lib/native-server.js +109 -0
- package/lib/native-session.d.ts +60 -0
- package/lib/native-session.js +565 -0
- package/lib/node-actor.d.ts +7 -0
- package/lib/node-actor.js +10 -0
- package/lib/node-client-common.d.ts +9 -0
- package/lib/node-client-common.js +35 -0
- package/lib/node-client.d.ts +4 -0
- package/lib/node-client.js +13 -0
- package/lib/node-direct.d.ts +7 -0
- package/lib/node-direct.js +10 -0
- package/lib/node-worker-options.d.ts +5 -0
- package/lib/node-worker-options.js +65 -0
- package/lib/node-worker-port.d.ts +4 -0
- package/lib/node-worker-port.js +116 -0
- package/lib/node-worker.d.ts +1 -0
- package/lib/node-worker.js +38 -0
- package/lib/pgwire-connection.d.ts +60 -0
- package/lib/pgwire-connection.js +528 -0
- package/lib/pgwire.d.ts +3 -0
- package/lib/pgwire.js +105 -0
- package/lib/physical-archive.d.ts +29 -0
- package/lib/physical-archive.js +527 -0
- package/lib/protocol.d.ts +1 -0
- package/lib/protocol.js +1 -0
- package/lib/public.d.ts +4 -0
- package/lib/public.js +3 -0
- package/lib/query.d.ts +1 -0
- package/lib/query.js +1 -0
- package/lib/rpc.d.ts +203 -0
- package/lib/rpc.js +84 -0
- package/lib/runtime-descriptor.d.ts +3 -0
- package/lib/runtime-descriptor.js +79 -0
- package/lib/server.node.d.ts +1 -0
- package/lib/server.node.js +1 -0
- package/lib/startup-config.d.ts +2 -0
- package/lib/startup-config.js +19 -0
- package/lib/storage/bun.d.ts +6 -0
- package/lib/storage/bun.js +6 -0
- package/lib/storage/deno.d.ts +7 -0
- package/lib/storage/deno.js +7 -0
- package/lib/storage/incremental-storage.d.ts +25 -0
- package/lib/storage/incremental-storage.js +154 -0
- package/lib/storage/indexed-db-provider.d.ts +40 -0
- package/lib/storage/indexed-db-provider.js +259 -0
- package/lib/storage/indexed-db.d.ts +9 -0
- package/lib/storage/indexed-db.js +11 -0
- package/lib/storage/node.d.ts +10 -0
- package/lib/storage/node.js +13 -0
- package/lib/storage/opfs-pool.d.ts +31 -0
- package/lib/storage/opfs-pool.js +1271 -0
- package/lib/storage/opfs-provider.d.ts +4 -0
- package/lib/storage/opfs-provider.js +257 -0
- package/lib/storage/opfs.d.ts +8 -0
- package/lib/storage/opfs.js +10 -0
- package/lib/storage/restore-cleanup.d.ts +4 -0
- package/lib/storage/restore-cleanup.js +22 -0
- package/lib/storage/web-lock.d.ts +2 -0
- package/lib/storage/web-lock.js +64 -0
- package/lib/storage-provider.d.ts +47 -0
- package/lib/storage-provider.js +141 -0
- package/lib/storage-snapshot.d.ts +44 -0
- package/lib/storage-snapshot.js +274 -0
- package/lib/storage.d.ts +46 -0
- package/lib/storage.js +83 -0
- package/lib/tool-runtime.d.ts +43 -0
- package/lib/tool-runtime.js +93 -0
- package/lib/tool-worker-common.d.ts +48 -0
- package/lib/tool-worker-common.js +97 -0
- package/lib/tool-worker.d.ts +1 -0
- package/lib/tool-worker.js +10 -0
- package/lib/types.d.ts +230 -0
- package/lib/types.js +1 -0
- package/lib/wasix-runtime.d.ts +24 -0
- package/lib/wasix-runtime.js +186 -0
- package/lib/worker-client.d.ts +4 -0
- package/lib/worker-client.js +45 -0
- package/lib/worker-dispatch.d.ts +11 -0
- package/lib/worker-dispatch.js +174 -0
- package/lib/worker-entry.bun.d.ts +2 -0
- package/lib/worker-entry.bun.js +2 -0
- package/lib/worker-entry.d.ts +2 -0
- package/lib/worker-entry.deno.d.ts +2 -0
- package/lib/worker-entry.deno.js +2 -0
- package/lib/worker-entry.js +2 -0
- package/lib/worker-entry.node.d.ts +2 -0
- package/lib/worker-entry.node.js +2 -0
- package/lib/worker-node-client.d.ts +4 -0
- package/lib/worker-node-client.js +55 -0
- package/lib/worker-rpc.d.ts +36 -0
- package/lib/worker-rpc.js +422 -0
- package/lib/worker-transfer.d.ts +6 -0
- package/lib/worker-transfer.js +11 -0
- package/lib/worker.d.ts +1 -0
- package/lib/worker.js +21 -0
- package/lib/zstd.d.ts +4 -0
- package/lib/zstd.js +12 -0
- package/node_modules/@oliphaunt/js-core/README.md +7 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.d.ts +1 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.js +22 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/query.d.ts +255 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/query.js +2068 -0
- package/node_modules/@oliphaunt/js-core/dist/module/package.json +3 -0
- package/node_modules/@oliphaunt/js-core/dist/module/protocol.d.ts +1 -0
- package/node_modules/@oliphaunt/js-core/dist/module/protocol.js +19 -0
- package/node_modules/@oliphaunt/js-core/dist/module/query.d.ts +255 -0
- package/node_modules/@oliphaunt/js-core/dist/module/query.js +2039 -0
- package/node_modules/@oliphaunt/js-core/package.json +21 -0
- package/package.json +122 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WasixClusterSeedLoader, WasixPhysicalIdentity, WasixStorageLease } from '../storage-provider.js';
|
|
2
|
+
import type { StoredSnapshot } from '../storage-snapshot.js';
|
|
3
|
+
export declare function acquireOpfsStorage(name: string, loadClusterSeed: WasixClusterSeedLoader, physicalIdentity: WasixPhysicalIdentity): Promise<WasixStorageLease>;
|
|
4
|
+
export declare function restoreOpfsStorage(name: string, snapshot: StoredSnapshot, physicalIdentity: WasixPhysicalIdentity): Promise<void>;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { composeWasixStorageFailure, WasixStorageError } from '../errors.js';
|
|
2
|
+
import { acquireIncrementalStorage } from './incremental-storage.js';
|
|
3
|
+
import { applyPooledOpfsDelta, DirectOpfsPool, inspectPooledOpfsDatabase, openPooledDatabaseDirectory, openPooledOpfsRoot, preparePooledOpfsDatabase, } from './opfs-pool.js';
|
|
4
|
+
import { releaseRestoreLock } from './restore-cleanup.js';
|
|
5
|
+
import { acquireExclusiveWebLock } from './web-lock.js';
|
|
6
|
+
const DIRECT_BRIDGE_CAPACITY = 1024 * 1024;
|
|
7
|
+
export async function acquireOpfsStorage(name, loadClusterSeed, physicalIdentity) {
|
|
8
|
+
const label = `OPFS storage ${JSON.stringify(name)}`;
|
|
9
|
+
let lock;
|
|
10
|
+
let lockHandedToFallback = false;
|
|
11
|
+
let directPool;
|
|
12
|
+
let directAttempted = false;
|
|
13
|
+
try {
|
|
14
|
+
lock = await acquireExclusiveWebLock(`@oliphaunt/wasix-ts:opfs:${name}`, label);
|
|
15
|
+
if (canUseDirectOpfsPool()) {
|
|
16
|
+
directAttempted = true;
|
|
17
|
+
try {
|
|
18
|
+
directPool = await DirectOpfsPool.open(name, loadClusterSeed, physicalIdentity);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (!isDirectPoolUnavailable(error))
|
|
22
|
+
throw normalizeOpfsOpenError(name, error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (directPool !== undefined) {
|
|
26
|
+
return new DirectOpfsLease(name, lock, directPool);
|
|
27
|
+
}
|
|
28
|
+
const database = await openPooledDatabaseDirectory(name);
|
|
29
|
+
let initializationState = 'new';
|
|
30
|
+
lockHandedToFallback = true;
|
|
31
|
+
return await acquireIncrementalStorage(label, loadClusterSeed, {
|
|
32
|
+
writeFailureCommitState: 'unknown',
|
|
33
|
+
acquireLock: async () => lock,
|
|
34
|
+
async openStore() {
|
|
35
|
+
return {
|
|
36
|
+
async read() {
|
|
37
|
+
// A failed direct acquisition has already reset an unpublished
|
|
38
|
+
// generation. Placements that never attempted it reset here.
|
|
39
|
+
const opened = await (directAttempted
|
|
40
|
+
? inspectPooledOpfsDatabase(database, name, physicalIdentity)
|
|
41
|
+
: preparePooledOpfsDatabase(database, name, loadClusterSeed, physicalIdentity));
|
|
42
|
+
initializationState = opened.state;
|
|
43
|
+
return opened.snapshot;
|
|
44
|
+
},
|
|
45
|
+
initializationState: () => initializationState,
|
|
46
|
+
apply: (delta) => applyPooledOpfsDelta(database, name, physicalIdentity, delta),
|
|
47
|
+
close() { },
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (lock !== undefined && !lockHandedToFallback) {
|
|
54
|
+
try {
|
|
55
|
+
await lock.release();
|
|
56
|
+
}
|
|
57
|
+
catch (releaseError) {
|
|
58
|
+
const primary = normalizeOpfsOpenError(name, error);
|
|
59
|
+
throw composeWasixStorageFailure(primary, 'ownership release also failed', releaseError);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw normalizeOpfsOpenError(name, error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function restoreOpfsStorage(name, snapshot, physicalIdentity) {
|
|
66
|
+
const label = `OPFS storage ${JSON.stringify(name)}`;
|
|
67
|
+
const lock = await acquireExclusiveWebLock(`@oliphaunt/wasix-ts:opfs:${name}`, label);
|
|
68
|
+
let root;
|
|
69
|
+
let cleanupDestination = false;
|
|
70
|
+
let destinationExisted = false;
|
|
71
|
+
let commitState = 'unchanged';
|
|
72
|
+
let failure;
|
|
73
|
+
try {
|
|
74
|
+
root = await openPooledOpfsRoot();
|
|
75
|
+
let database;
|
|
76
|
+
try {
|
|
77
|
+
database = await root.getDirectoryHandle(name);
|
|
78
|
+
destinationExisted = true;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!isNotFound(error))
|
|
82
|
+
throw error;
|
|
83
|
+
database = await root.getDirectoryHandle(name, { create: true });
|
|
84
|
+
}
|
|
85
|
+
const destination = await inspectPooledOpfsDatabase(database, name, physicalIdentity);
|
|
86
|
+
if (destination.state === 'existing') {
|
|
87
|
+
throw new WasixStorageError(`${label} already exists`, {
|
|
88
|
+
code: 'incomplete',
|
|
89
|
+
commitState: 'unchanged',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
cleanupDestination = true;
|
|
93
|
+
if (destination.snapshot !== undefined) {
|
|
94
|
+
// An initializing generation was never published and is disposable.
|
|
95
|
+
// Replace it instead of merging a restore over its unpublished seed contents.
|
|
96
|
+
await root.removeEntry(name, { recursive: true });
|
|
97
|
+
database = await root.getDirectoryHandle(name, { create: true });
|
|
98
|
+
}
|
|
99
|
+
commitState = 'unknown';
|
|
100
|
+
await applyPooledOpfsDelta(database, name, physicalIdentity, {
|
|
101
|
+
directories: snapshot.directories,
|
|
102
|
+
files: snapshot.files,
|
|
103
|
+
deleted: [],
|
|
104
|
+
});
|
|
105
|
+
cleanupDestination = false;
|
|
106
|
+
commitState = 'persisted';
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
failure = error;
|
|
110
|
+
if (cleanupDestination && root !== undefined) {
|
|
111
|
+
try {
|
|
112
|
+
await root.removeEntry(name, { recursive: true });
|
|
113
|
+
if (destinationExisted) {
|
|
114
|
+
await root.getDirectoryHandle(name, { create: true });
|
|
115
|
+
}
|
|
116
|
+
commitState = 'unchanged';
|
|
117
|
+
}
|
|
118
|
+
catch (cleanupError) {
|
|
119
|
+
failure = new AggregateError([error, cleanupError], `${label} restore cleanup failed`);
|
|
120
|
+
commitState = 'unknown';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
failure = await releaseRestoreLock(lock, label, commitState, failure);
|
|
126
|
+
}
|
|
127
|
+
if (failure !== undefined)
|
|
128
|
+
throw failure;
|
|
129
|
+
}
|
|
130
|
+
class DirectOpfsLease {
|
|
131
|
+
state;
|
|
132
|
+
#name;
|
|
133
|
+
#lock;
|
|
134
|
+
#pool;
|
|
135
|
+
#directory;
|
|
136
|
+
#closed = false;
|
|
137
|
+
constructor(name, lock, pool) {
|
|
138
|
+
this.#name = name;
|
|
139
|
+
this.state = pool.state;
|
|
140
|
+
this.#lock = lock;
|
|
141
|
+
this.#pool = pool;
|
|
142
|
+
}
|
|
143
|
+
createPgdataDirectory = async (DirectoryConstructor) => {
|
|
144
|
+
if (this.#closed)
|
|
145
|
+
throw this.#unavailable('is closed', 'unchanged');
|
|
146
|
+
if (this.#directory !== undefined) {
|
|
147
|
+
throw this.#unavailable('already materialized its direct filesystem', 'unchanged');
|
|
148
|
+
}
|
|
149
|
+
if (typeof DirectoryConstructor.createSync !== 'function') {
|
|
150
|
+
throw this.#unavailable('requires a host with the direct filesystem bridge', 'unchanged');
|
|
151
|
+
}
|
|
152
|
+
const directory = DirectoryConstructor.createSync(this.#pool, DIRECT_BRIDGE_CAPACITY);
|
|
153
|
+
// Take ownership before validating the bridge so the normal close path
|
|
154
|
+
// releases a failed materialization exactly once.
|
|
155
|
+
this.#directory = directory;
|
|
156
|
+
const pgVersion = (await directory.readTextFile('PG_VERSION')).trim();
|
|
157
|
+
if (pgVersion.length === 0) {
|
|
158
|
+
throw this.#unavailable('direct filesystem returned an empty PG_VERSION', 'unchanged');
|
|
159
|
+
}
|
|
160
|
+
return directory;
|
|
161
|
+
};
|
|
162
|
+
async sync(_directory, boundary) {
|
|
163
|
+
if (this.#closed)
|
|
164
|
+
throw this.#unavailable('is closed', 'unchanged');
|
|
165
|
+
try {
|
|
166
|
+
await this.#pool.sync(boundary);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error instanceof WasixStorageError)
|
|
170
|
+
throw error;
|
|
171
|
+
throw new WasixStorageError(`could not persist OPFS storage ${JSON.stringify(this.#name)}: ${describeError(error)}`, { code: 'publication-failed', commitState: 'unknown', cause: error });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async close(_directory, outcome) {
|
|
175
|
+
if (this.#closed)
|
|
176
|
+
return;
|
|
177
|
+
this.#closed = true;
|
|
178
|
+
let failure;
|
|
179
|
+
let commitState = 'unknown';
|
|
180
|
+
if (outcome === 'clean') {
|
|
181
|
+
try {
|
|
182
|
+
await this.#pool.sync('close');
|
|
183
|
+
commitState = 'persisted';
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
failure =
|
|
187
|
+
error instanceof WasixStorageError
|
|
188
|
+
? error
|
|
189
|
+
: new WasixStorageError(`OPFS storage ${JSON.stringify(this.#name)} could not persist on close`, { code: 'publication-failed', commitState: 'unknown', cause: error });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
await this.#pool.close(false);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const cleanup = this.#unavailable(`could not close its pool: ${describeError(error)}`, commitState, error);
|
|
197
|
+
failure =
|
|
198
|
+
failure === undefined
|
|
199
|
+
? cleanup
|
|
200
|
+
: composeWasixStorageFailure(failure, 'pool cleanup also failed', cleanup);
|
|
201
|
+
}
|
|
202
|
+
const directory = this.#directory;
|
|
203
|
+
this.#directory = undefined;
|
|
204
|
+
try {
|
|
205
|
+
directory?.free();
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const release = this.#unavailable(`could not release its host filesystem: ${describeError(error)}`, commitState, error);
|
|
209
|
+
failure =
|
|
210
|
+
failure === undefined
|
|
211
|
+
? release
|
|
212
|
+
: composeWasixStorageFailure(failure, 'host filesystem release also failed', release);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await this.#lock.release();
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const release = this.#unavailable(`closed but its ownership lock could not be released: ${describeError(error)}`, commitState, error);
|
|
219
|
+
failure =
|
|
220
|
+
failure === undefined
|
|
221
|
+
? release
|
|
222
|
+
: composeWasixStorageFailure(failure, 'ownership release also failed', release);
|
|
223
|
+
}
|
|
224
|
+
if (failure !== undefined)
|
|
225
|
+
throw failure;
|
|
226
|
+
}
|
|
227
|
+
#unavailable(detail, commitState, cause) {
|
|
228
|
+
return new WasixStorageError(`OPFS storage ${JSON.stringify(this.#name)} ${detail}`, {
|
|
229
|
+
code: 'unavailable',
|
|
230
|
+
commitState,
|
|
231
|
+
...(cause === undefined ? {} : { cause }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function canUseDirectOpfsPool() {
|
|
236
|
+
return (typeof document === 'undefined' && globalThis.navigator?.storage?.getDirectory !== undefined);
|
|
237
|
+
}
|
|
238
|
+
function isDirectPoolUnavailable(error) {
|
|
239
|
+
const name = errorName(error);
|
|
240
|
+
return (name === 'NotSupportedError' ||
|
|
241
|
+
name === 'NoModificationAllowedError' ||
|
|
242
|
+
name === 'QuotaExceededError');
|
|
243
|
+
}
|
|
244
|
+
function normalizeOpfsOpenError(name, error) {
|
|
245
|
+
if (error instanceof WasixStorageError)
|
|
246
|
+
return error;
|
|
247
|
+
return new WasixStorageError(`could not open OPFS storage ${JSON.stringify(name)}: ${describeError(error)}`, { code: 'unavailable', commitState: 'unchanged', cause: error });
|
|
248
|
+
}
|
|
249
|
+
function isNotFound(error) {
|
|
250
|
+
return errorName(error) === 'NotFoundError';
|
|
251
|
+
}
|
|
252
|
+
function errorName(error) {
|
|
253
|
+
return typeof error === 'object' && error !== null && 'name' in error ? String(error.name) : '';
|
|
254
|
+
}
|
|
255
|
+
function describeError(error) {
|
|
256
|
+
return error instanceof Error ? error.message : String(error);
|
|
257
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type PersistentWasixStorage } from '../storage.js';
|
|
2
|
+
/**
|
|
3
|
+
* Select an origin-private opaque database pool. Worker execution uses
|
|
4
|
+
* same-realm synchronous exact-range I/O; other placements publish to the
|
|
5
|
+
* same format through the portable journaled path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function opfs(name: string): PersistentWasixStorage;
|
|
8
|
+
export default opfs;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineOpfsStorage } from '../storage.js';
|
|
2
|
+
/**
|
|
3
|
+
* Select an origin-private opaque database pool. Worker execution uses
|
|
4
|
+
* same-realm synchronous exact-range I/O; other placements publish to the
|
|
5
|
+
* same format through the portable journaled path.
|
|
6
|
+
*/
|
|
7
|
+
export function opfs(name) {
|
|
8
|
+
return defineOpfsStorage(name);
|
|
9
|
+
}
|
|
10
|
+
export default opfs;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type WasixStorageCommitState } from '../errors.js';
|
|
2
|
+
import type { ExclusiveStorageLock } from './incremental-storage.js';
|
|
3
|
+
/** Preserve a restore failure while making ownership-release failure observable. */
|
|
4
|
+
export declare function releaseRestoreLock(lock: ExclusiveStorageLock, label: string, commitState: WasixStorageCommitState, primary: unknown): Promise<unknown>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { composeWasixStorageFailure, WasixStorageError, } from '../errors.js';
|
|
2
|
+
/** Preserve a restore failure while making ownership-release failure observable. */
|
|
3
|
+
export async function releaseRestoreLock(lock, label, commitState, primary) {
|
|
4
|
+
try {
|
|
5
|
+
await lock.release();
|
|
6
|
+
return primary;
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const release = new WasixStorageError(`${label} ownership lock could not be released`, {
|
|
10
|
+
code: 'unavailable',
|
|
11
|
+
commitState,
|
|
12
|
+
cause: error,
|
|
13
|
+
});
|
|
14
|
+
if (primary instanceof Error) {
|
|
15
|
+
return composeWasixStorageFailure(primary, 'ownership release also failed', release);
|
|
16
|
+
}
|
|
17
|
+
if (primary !== undefined) {
|
|
18
|
+
return new AggregateError([primary, release], `${label} ownership release also failed`);
|
|
19
|
+
}
|
|
20
|
+
return release;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { WasixStorageError } from '../errors.js';
|
|
2
|
+
export async function acquireExclusiveWebLock(lockName, label) {
|
|
3
|
+
const locks = globalThis.navigator.locks;
|
|
4
|
+
if (locks === undefined) {
|
|
5
|
+
throw new WasixStorageError(`${label} requires the browser Web Locks API`, {
|
|
6
|
+
code: 'unavailable',
|
|
7
|
+
commitState: 'unchanged',
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
let releaseHold;
|
|
11
|
+
const hold = new Promise((resolve) => {
|
|
12
|
+
releaseHold = resolve;
|
|
13
|
+
});
|
|
14
|
+
let acquisitionSettled = false;
|
|
15
|
+
let resolveAcquisition;
|
|
16
|
+
let rejectAcquisition;
|
|
17
|
+
const acquired = new Promise((resolve, reject) => {
|
|
18
|
+
resolveAcquisition = resolve;
|
|
19
|
+
rejectAcquisition = reject;
|
|
20
|
+
});
|
|
21
|
+
const request = locks
|
|
22
|
+
.request(lockName, { mode: 'exclusive', ifAvailable: true }, async (lock) => {
|
|
23
|
+
acquisitionSettled = true;
|
|
24
|
+
if (lock === null) {
|
|
25
|
+
resolveAcquisition?.(false);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
resolveAcquisition?.(true);
|
|
29
|
+
await hold;
|
|
30
|
+
})
|
|
31
|
+
.catch((error) => {
|
|
32
|
+
if (!acquisitionSettled)
|
|
33
|
+
rejectAcquisition?.(error);
|
|
34
|
+
throw error;
|
|
35
|
+
});
|
|
36
|
+
let available;
|
|
37
|
+
try {
|
|
38
|
+
available = await acquired;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
void request.catch(() => undefined);
|
|
42
|
+
throw new WasixStorageError(`could not acquire ownership of ${label}: ${describeError(error)}`, { code: 'unavailable', commitState: 'unchanged', cause: error });
|
|
43
|
+
}
|
|
44
|
+
if (!available) {
|
|
45
|
+
await request;
|
|
46
|
+
throw new WasixStorageError(`${label} is already open in this origin`, {
|
|
47
|
+
code: 'busy',
|
|
48
|
+
commitState: 'unchanged',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
let released = false;
|
|
52
|
+
return {
|
|
53
|
+
async release() {
|
|
54
|
+
if (released)
|
|
55
|
+
return;
|
|
56
|
+
released = true;
|
|
57
|
+
releaseHold?.();
|
|
58
|
+
await request;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function describeError(error) {
|
|
63
|
+
return error instanceof Error ? error.message : String(error);
|
|
64
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { WasixDirectoryMount } from './archive.js';
|
|
2
|
+
import type { Directory } from './host/index.mjs';
|
|
3
|
+
import type { SerializedWasixStorage } from './storage.js';
|
|
4
|
+
import type { StoredSnapshot } from './storage-snapshot.js';
|
|
5
|
+
/** Stable fields that determine whether a WASIX runtime may open stored PGDATA. */
|
|
6
|
+
export type WasixPhysicalIdentity = Readonly<{
|
|
7
|
+
schema: 'oliphaunt-physical-format-v1';
|
|
8
|
+
engineFamily: 'wasix';
|
|
9
|
+
postgresMajor: number;
|
|
10
|
+
physicalFormat: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const WASIX_PHYSICAL_IDENTITY: WasixPhysicalIdentity;
|
|
13
|
+
export type StorageDirectoryEntry = Readonly<{
|
|
14
|
+
type: 'dir' | 'file' | 'unknown';
|
|
15
|
+
name: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** Narrow surface currently offered by Wasmer's in-memory `Directory`. */
|
|
18
|
+
export type StorageDirectory = {
|
|
19
|
+
readDir(path: string): Promise<StorageDirectoryEntry[]>;
|
|
20
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
21
|
+
/** Host-patched mutation journal. Older/untracked directories fall back to a full scan. */
|
|
22
|
+
changedPaths?(): readonly string[] | Promise<readonly string[]>;
|
|
23
|
+
clearChanges?(): void | Promise<void>;
|
|
24
|
+
entryType?(path: string): string | Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
/** Internal publication strength; `full` is used for initialization, not SQL dispatch. */
|
|
27
|
+
export type WasixStorageSyncBoundary = 'operation' | 'full' | 'close';
|
|
28
|
+
/** Load the package-owned seed only after exclusive storage inspection finds a new root. */
|
|
29
|
+
export type WasixClusterSeedLoader = () => Promise<WasixDirectoryMount>;
|
|
30
|
+
export type WasixStorageLease = {
|
|
31
|
+
/** Whether PGDATA came from the packaged cluster seed or persistent storage. */
|
|
32
|
+
readonly state: 'new' | 'existing';
|
|
33
|
+
/** Initial contents for a portable `/base` Wasmer memory mount. */
|
|
34
|
+
readonly mount?: WasixDirectoryMount;
|
|
35
|
+
/** Optional direct PGDATA materializer; portable providers omit it. */
|
|
36
|
+
createPgdataDirectory?(DirectoryConstructor: typeof Directory): Promise<Directory>;
|
|
37
|
+
/** Complete the provider's PostgreSQL-safe persistence boundary. */
|
|
38
|
+
sync(directory: StorageDirectory, boundary: WasixStorageSyncBoundary): Promise<void>;
|
|
39
|
+
close(directory: StorageDirectory | undefined, outcome: 'clean' | 'failed'): Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
/** Restore a validated snapshot into a closed, empty persistent destination. */
|
|
42
|
+
export declare function restoreWasixStorage(storage: SerializedWasixStorage, snapshot: StoredSnapshot, identity: WasixPhysicalIdentity): Promise<void>;
|
|
43
|
+
export declare function acquireWasixStorage(storage: SerializedWasixStorage, loadClusterSeed: WasixClusterSeedLoader, identity: WasixPhysicalIdentity): Promise<WasixStorageLease>;
|
|
44
|
+
export declare function assertWasixPhysicalIdentity(identity: WasixPhysicalIdentity): WasixPhysicalIdentity;
|
|
45
|
+
export declare function physicalIdentityMatches(stored: unknown, selected: WasixPhysicalIdentity): boolean;
|
|
46
|
+
/** Stable JSON for hashing or comparing JSON-compatible runtime identities. */
|
|
47
|
+
export declare function canonicalJson(value: unknown): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DATABASE_ROOT_POSTGRES_MAJOR, WASIX_PHYSICAL_FORMAT } from './database-root.js';
|
|
2
|
+
import { WasixStorageError } from './errors.js';
|
|
3
|
+
import { validateIndexedDbDatabaseName, validateOpfsDatabaseName } from './storage.js';
|
|
4
|
+
export const WASIX_PHYSICAL_IDENTITY = Object.freeze({
|
|
5
|
+
schema: 'oliphaunt-physical-format-v1',
|
|
6
|
+
engineFamily: 'wasix',
|
|
7
|
+
postgresMajor: DATABASE_ROOT_POSTGRES_MAJOR,
|
|
8
|
+
physicalFormat: WASIX_PHYSICAL_FORMAT,
|
|
9
|
+
});
|
|
10
|
+
/** Restore a validated snapshot into a closed, empty persistent destination. */
|
|
11
|
+
export async function restoreWasixStorage(storage, snapshot, identity) {
|
|
12
|
+
switch (storage.kind) {
|
|
13
|
+
case 'memory':
|
|
14
|
+
throw new WasixStorageError('physical restore requires persistent WASIX storage', {
|
|
15
|
+
code: 'unavailable',
|
|
16
|
+
commitState: 'unchanged',
|
|
17
|
+
});
|
|
18
|
+
case 'indexed-db': {
|
|
19
|
+
validateIndexedDbDatabaseName(storage.name);
|
|
20
|
+
const { restoreIndexedDbStorage } = await import('./storage/indexed-db-provider.js');
|
|
21
|
+
return restoreIndexedDbStorage(storage.name, snapshot, identity);
|
|
22
|
+
}
|
|
23
|
+
case 'opfs': {
|
|
24
|
+
validateOpfsDatabaseName(storage.name);
|
|
25
|
+
const { restoreOpfsStorage } = await import('./storage/opfs-provider.js');
|
|
26
|
+
return restoreOpfsStorage(storage.name, snapshot, identity);
|
|
27
|
+
}
|
|
28
|
+
case 'directory':
|
|
29
|
+
throw new WasixStorageError('directory restore is owned directly by the native Rust runtime', { code: 'unavailable', commitState: 'unchanged' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function acquireWasixStorage(storage, loadClusterSeed, identity) {
|
|
33
|
+
if (storage.schema !== 'oliphaunt-wasix-storage-v1') {
|
|
34
|
+
throw new WasixStorageError('WASIX storage descriptor has an unsupported schema', {
|
|
35
|
+
code: 'unavailable',
|
|
36
|
+
commitState: 'unchanged',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
switch (storage.kind) {
|
|
40
|
+
case 'memory':
|
|
41
|
+
return memoryLease(await loadClusterSeed());
|
|
42
|
+
case 'indexed-db': {
|
|
43
|
+
validateIndexedDbDatabaseName(storage.name);
|
|
44
|
+
const { acquireIndexedDbStorage } = await import('./storage/indexed-db-provider.js');
|
|
45
|
+
return acquireIndexedDbStorage(storage.name, loadClusterSeed, identity);
|
|
46
|
+
}
|
|
47
|
+
case 'opfs': {
|
|
48
|
+
validateOpfsDatabaseName(storage.name);
|
|
49
|
+
const { acquireOpfsStorage } = await import('./storage/opfs-provider.js');
|
|
50
|
+
return acquireOpfsStorage(storage.name, loadClusterSeed, identity);
|
|
51
|
+
}
|
|
52
|
+
case 'directory':
|
|
53
|
+
throw new WasixStorageError('directory storage is owned directly by the native Rust runtime', {
|
|
54
|
+
code: 'unavailable',
|
|
55
|
+
commitState: 'unchanged',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function assertWasixPhysicalIdentity(identity) {
|
|
60
|
+
if (identity.schema !== WASIX_PHYSICAL_IDENTITY.schema ||
|
|
61
|
+
identity.engineFamily !== WASIX_PHYSICAL_IDENTITY.engineFamily ||
|
|
62
|
+
identity.postgresMajor !== WASIX_PHYSICAL_IDENTITY.postgresMajor ||
|
|
63
|
+
identity.physicalFormat !== WASIX_PHYSICAL_IDENTITY.physicalFormat) {
|
|
64
|
+
throw new TypeError('WASIX storage has an unsupported physical identity');
|
|
65
|
+
}
|
|
66
|
+
return identity;
|
|
67
|
+
}
|
|
68
|
+
export function physicalIdentityMatches(stored, selected) {
|
|
69
|
+
const storedKey = parseWasixPhysicalIdentity(stored);
|
|
70
|
+
if (storedKey === undefined) {
|
|
71
|
+
throw new TypeError('WASIX storage has malformed physical identity metadata');
|
|
72
|
+
}
|
|
73
|
+
const selectedKey = assertWasixPhysicalIdentity(selected);
|
|
74
|
+
return (storedKey.schema === selectedKey.schema &&
|
|
75
|
+
storedKey.engineFamily === selectedKey.engineFamily &&
|
|
76
|
+
storedKey.postgresMajor === selectedKey.postgresMajor &&
|
|
77
|
+
storedKey.physicalFormat === selectedKey.physicalFormat);
|
|
78
|
+
}
|
|
79
|
+
function parseWasixPhysicalIdentity(value) {
|
|
80
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
81
|
+
return undefined;
|
|
82
|
+
const key = value;
|
|
83
|
+
const keys = Object.keys(key).sort();
|
|
84
|
+
if (keys.length !== 4 ||
|
|
85
|
+
keys[0] !== 'engineFamily' ||
|
|
86
|
+
keys[1] !== 'physicalFormat' ||
|
|
87
|
+
keys[2] !== 'postgresMajor' ||
|
|
88
|
+
keys[3] !== 'schema' ||
|
|
89
|
+
key.schema !== 'oliphaunt-physical-format-v1' ||
|
|
90
|
+
key.engineFamily !== 'wasix' ||
|
|
91
|
+
!Number.isInteger(key.postgresMajor) ||
|
|
92
|
+
key.postgresMajor <= 0 ||
|
|
93
|
+
typeof key.physicalFormat !== 'string' ||
|
|
94
|
+
key.physicalFormat.length === 0) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return key;
|
|
98
|
+
}
|
|
99
|
+
/** Stable JSON for hashing or comparing JSON-compatible runtime identities. */
|
|
100
|
+
export function canonicalJson(value) {
|
|
101
|
+
const active = new Set();
|
|
102
|
+
const canonicalize = (candidate) => {
|
|
103
|
+
if (candidate === null || typeof candidate === 'string' || typeof candidate === 'boolean') {
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
if (typeof candidate === 'number') {
|
|
107
|
+
if (!Number.isFinite(candidate)) {
|
|
108
|
+
throw new TypeError('JSON identity contains a non-finite number');
|
|
109
|
+
}
|
|
110
|
+
return candidate;
|
|
111
|
+
}
|
|
112
|
+
if (typeof candidate !== 'object') {
|
|
113
|
+
throw new TypeError('identity is not JSON-compatible');
|
|
114
|
+
}
|
|
115
|
+
if (active.has(candidate)) {
|
|
116
|
+
throw new TypeError('identity contains a cycle');
|
|
117
|
+
}
|
|
118
|
+
active.add(candidate);
|
|
119
|
+
try {
|
|
120
|
+
if (Array.isArray(candidate)) {
|
|
121
|
+
return candidate.map(canonicalize);
|
|
122
|
+
}
|
|
123
|
+
const record = candidate;
|
|
124
|
+
return Object.fromEntries(Object.keys(record)
|
|
125
|
+
.sort()
|
|
126
|
+
.map((key) => [key, canonicalize(record[key])]));
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
active.delete(candidate);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return JSON.stringify(canonicalize(value));
|
|
133
|
+
}
|
|
134
|
+
function memoryLease(clusterSeed) {
|
|
135
|
+
return {
|
|
136
|
+
state: 'new',
|
|
137
|
+
mount: clusterSeed,
|
|
138
|
+
async sync() { },
|
|
139
|
+
async close() { },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { WasixDirectoryMount } from './archive.js';
|
|
2
|
+
import type { WasixStorageError } from './errors.js';
|
|
3
|
+
import type { StorageDirectory } from './storage-provider.js';
|
|
4
|
+
export declare const VOLATILE_DATABASE_FILES: Set<string>;
|
|
5
|
+
export type StoredSnapshot = {
|
|
6
|
+
schema: 'oliphaunt-wasix-directory-snapshot-v1';
|
|
7
|
+
directories: string[];
|
|
8
|
+
files: {
|
|
9
|
+
path: string;
|
|
10
|
+
bytes: Uint8Array;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
13
|
+
/** A current-state delta. Deletions and upserts are committed as one boundary. */
|
|
14
|
+
export type StorageDelta = {
|
|
15
|
+
directories: string[];
|
|
16
|
+
files: {
|
|
17
|
+
path: string;
|
|
18
|
+
bytes: Uint8Array;
|
|
19
|
+
}[];
|
|
20
|
+
deleted: string[];
|
|
21
|
+
};
|
|
22
|
+
export type StorageEntryType = 'dir' | 'file';
|
|
23
|
+
export declare function splitStorageDeltaDeletes(delta: StorageDelta): {
|
|
24
|
+
replacements: string[];
|
|
25
|
+
removals: string[];
|
|
26
|
+
};
|
|
27
|
+
export type SnapshotErrorContext = Readonly<{
|
|
28
|
+
label: string;
|
|
29
|
+
corrupt(detail: string, cause?: unknown): WasixStorageError;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function snapshotStorageDirectory(directory: StorageDirectory, filter?: Readonly<{
|
|
32
|
+
skipFile?: (path: string) => boolean;
|
|
33
|
+
skipDirectoryContents?: (path: string) => boolean;
|
|
34
|
+
}>): Promise<StoredSnapshot>;
|
|
35
|
+
/**
|
|
36
|
+
* Read only journaled PGDATA paths, falling back to a complete snapshot when
|
|
37
|
+
* the host does not expose mutation tracking or no durable generation exists.
|
|
38
|
+
*/
|
|
39
|
+
export declare function snapshotStorageDelta(directory: StorageDirectory, persistedEntries: ReadonlyMap<string, StorageEntryType>, forceFull?: boolean): Promise<StorageDelta>;
|
|
40
|
+
export declare function applyStorageDeltaToEntries(persistedEntries: Map<string, StorageEntryType>, delta: StorageDelta): void;
|
|
41
|
+
export declare function validateStoredSnapshot(value: unknown, expectedPostgresMajor: string, context: SnapshotErrorContext): StoredSnapshot;
|
|
42
|
+
export declare function snapshotToMount(snapshot: StoredSnapshot): WasixDirectoryMount;
|
|
43
|
+
export declare function requireRecord(value: unknown, label: string): Record<string, unknown>;
|
|
44
|
+
export declare function validateDirectoryEntryName(name: string): void;
|