@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,717 @@
|
|
|
1
|
+
import { WasixDatabaseImpl, createWasixDeferred, normalizeWasixDatabaseIdentity, } from './database.js';
|
|
2
|
+
import { WasixStorageError } from './errors.js';
|
|
3
|
+
import { prepareWasixRuntime } from './extensions.js';
|
|
4
|
+
import { assertSuccessfulStartupResponse, startupPacket } from './pgwire.js';
|
|
5
|
+
import { BackupModeExitUnconfirmedError, createPhysicalArchive } from './physical-archive.js';
|
|
6
|
+
import { simpleQuery } from './protocol.js';
|
|
7
|
+
import { assertSuccessfulQueryResponse, PostgresError } from './query.js';
|
|
8
|
+
import { acquireWasixStorage, canonicalJson, } from './storage-provider.js';
|
|
9
|
+
import { serveWasixProtocolConnection, SynchronousPgDumpConnection, } from './pgwire-connection.js';
|
|
10
|
+
import { materializeWasixToolMounts, prepareWasixToolAsset, releaseWasixToolMounts, validateWasixToolDescriptor, wasixToolAssetIdentity, wasixToolRunOptions, } from './tool-runtime.js';
|
|
11
|
+
import { normalizeWasixStartupGUCs } from './startup-config.js';
|
|
12
|
+
import { compileWasixModule, composeLifecycleFailure, configureWasixDatabase, configureWasixRole, describeError, materializeWasixMounts, wasixPostgresArgs, wasixPostgresEnvironment, } from './wasix-runtime.js';
|
|
13
|
+
const preparedRuntimes = new Map();
|
|
14
|
+
const MAX_PREPARED_RUNTIMES = 1;
|
|
15
|
+
const initializedHosts = new WeakMap();
|
|
16
|
+
const CHROMIUM_SYNC_WASM_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
const DIRECT_INSTANCE_DEADLINE_MS = 120_000;
|
|
18
|
+
const PROTOCOL_STREAM_COMPLETE = 0;
|
|
19
|
+
const PROTOCOL_STREAM_CALLBACK_ABORTED = 1;
|
|
20
|
+
const defaultDependencies = {
|
|
21
|
+
prepareRuntime: prepareRuntimeCached,
|
|
22
|
+
acquireStorage: acquireWasixStorage,
|
|
23
|
+
compileModule: compileWasixModule,
|
|
24
|
+
};
|
|
25
|
+
/** @internal Open PostgreSQL in the caller realm; guest calls are synchronous after setup. */
|
|
26
|
+
export async function openWasixDirect(options, host, environment = 'browser-main') {
|
|
27
|
+
const session = await DirectWasixSession.open(options, host, defaultDependencies, environment);
|
|
28
|
+
return new WasixDatabaseImpl(session);
|
|
29
|
+
}
|
|
30
|
+
/** @internal Open a direct-memory session inside the dedicated browser worker realm. */
|
|
31
|
+
export function openBrowserWorkerSession(options, host) {
|
|
32
|
+
return DirectWasixSession.open(options, host, defaultDependencies, 'browser-worker');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Owns the caller-realm guest, its mounted PGDATA, and one terminal teardown.
|
|
36
|
+
* A rejected close still retires the session after attempting provider and
|
|
37
|
+
* allocation release; destroyed guest state is never advertised as reusable.
|
|
38
|
+
*/
|
|
39
|
+
/** @internal */
|
|
40
|
+
export class DirectWasixSession {
|
|
41
|
+
identity;
|
|
42
|
+
#instance;
|
|
43
|
+
#storage;
|
|
44
|
+
#baseDirectory;
|
|
45
|
+
#instantiate;
|
|
46
|
+
#initialize;
|
|
47
|
+
#username;
|
|
48
|
+
#host;
|
|
49
|
+
#pgDump;
|
|
50
|
+
#pgDumpIdentity;
|
|
51
|
+
#closed = false;
|
|
52
|
+
#failed = false;
|
|
53
|
+
#closeAttempt;
|
|
54
|
+
#startupResponse = new Uint8Array();
|
|
55
|
+
constructor(instance, storage, baseDirectory, instantiate, initialize, username, database, host) {
|
|
56
|
+
this.#instance = instance;
|
|
57
|
+
this.#storage = storage;
|
|
58
|
+
this.#baseDirectory = baseDirectory;
|
|
59
|
+
this.#instantiate = instantiate;
|
|
60
|
+
this.#initialize = initialize;
|
|
61
|
+
this.#username = username;
|
|
62
|
+
this.#host = host;
|
|
63
|
+
this.identity = normalizeWasixDatabaseIdentity(username, database);
|
|
64
|
+
}
|
|
65
|
+
static async open(options, host, dependencies = defaultDependencies, environment = 'browser-main') {
|
|
66
|
+
if (environment === 'browser-main')
|
|
67
|
+
assertDirectExtensionCompatibility(options);
|
|
68
|
+
if (options.storage.kind === 'memory' && options.username !== 'postgres') {
|
|
69
|
+
throw newStorageRoleError(options.username);
|
|
70
|
+
}
|
|
71
|
+
const prepared = await dependencies.prepareRuntime(options);
|
|
72
|
+
const startupGUCs = normalizeWasixStartupGUCs(prepared.startupGUCs);
|
|
73
|
+
const eagerClusterSeed = options.storage.kind === 'memory' ? prepared.loadClusterSeed() : undefined;
|
|
74
|
+
const [, module] = await Promise.all([
|
|
75
|
+
initializeHost(host),
|
|
76
|
+
dependencies.compileModule(prepared.layout.module, prepared.moduleSha256),
|
|
77
|
+
eagerClusterSeed,
|
|
78
|
+
]);
|
|
79
|
+
// Acquire persistent ownership only after every non-owning preparation
|
|
80
|
+
// step succeeds, so a compilation rejection cannot strand its lock.
|
|
81
|
+
const storage = await dependencies.acquireStorage(options.storage, () => {
|
|
82
|
+
if (options.username !== 'postgres') {
|
|
83
|
+
throw newStorageRoleError(options.username);
|
|
84
|
+
}
|
|
85
|
+
return eagerClusterSeed ?? prepared.loadClusterSeed();
|
|
86
|
+
}, prepared.physicalIdentity);
|
|
87
|
+
let instance;
|
|
88
|
+
let baseDirectory;
|
|
89
|
+
let opened;
|
|
90
|
+
try {
|
|
91
|
+
const materialized = await materializeWasixMounts(host.Directory, prepared.layout, storage.mount, storage.createPgdataDirectory);
|
|
92
|
+
baseDirectory = materialized.baseDirectory;
|
|
93
|
+
const runtimeOptions = { ...options, startupGUCs };
|
|
94
|
+
const instantiate = () => instantiateDirectWithDeadline(host.instantiateOliphauntDirect(module, prepared.layout.module, {
|
|
95
|
+
program: '/bin/postgres',
|
|
96
|
+
args: wasixPostgresArgs(runtimeOptions),
|
|
97
|
+
cwd: '/',
|
|
98
|
+
env: wasixPostgresEnvironment(runtimeOptions, prepared.icuEnabled),
|
|
99
|
+
mount: materialized.mounts,
|
|
100
|
+
}));
|
|
101
|
+
const initialize = async (candidate, _storageState) => {
|
|
102
|
+
const response = candidate.startup(startupPacket(options.username, options.database));
|
|
103
|
+
assertSuccessfulStartupResponse(response);
|
|
104
|
+
await configureWasixDatabase(options, async (input) => candidate.execProtocolRaw(input));
|
|
105
|
+
return new Uint8Array(response);
|
|
106
|
+
};
|
|
107
|
+
instance = await instantiate();
|
|
108
|
+
const session = new DirectWasixSession(instance, storage, baseDirectory, instantiate, initialize, options.username, options.database, host);
|
|
109
|
+
opened = session;
|
|
110
|
+
session.#startupResponse = await initialize(instance, storage.state);
|
|
111
|
+
if (storage.state === 'new') {
|
|
112
|
+
await storage.sync(baseDirectory, 'full');
|
|
113
|
+
}
|
|
114
|
+
return session;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
let failure = directStartupFailure(error);
|
|
118
|
+
if (opened !== undefined) {
|
|
119
|
+
throw await opened.#closeAfterOpenFailure(failure);
|
|
120
|
+
}
|
|
121
|
+
if (instance !== undefined) {
|
|
122
|
+
try {
|
|
123
|
+
instance.close();
|
|
124
|
+
}
|
|
125
|
+
catch (closeError) {
|
|
126
|
+
failure = composeLifecycleFailure(failure, 'WASIX instance cleanup also failed', closeError);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
await storage.close(baseDirectory, 'failed');
|
|
131
|
+
}
|
|
132
|
+
catch (releaseError) {
|
|
133
|
+
failure = composeLifecycleFailure(failure, 'storage release also failed', releaseError);
|
|
134
|
+
}
|
|
135
|
+
if (instance !== undefined) {
|
|
136
|
+
try {
|
|
137
|
+
instance.free();
|
|
138
|
+
}
|
|
139
|
+
catch (freeError) {
|
|
140
|
+
failure = composeLifecycleFailure(failure, 'WASIX allocation release also failed', freeError);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
throw failure;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async exec(input, persistence = 'sync') {
|
|
147
|
+
this.#assertHealthy();
|
|
148
|
+
try {
|
|
149
|
+
const response = this.#currentInstance().execProtocolRaw(input);
|
|
150
|
+
if (persistence === 'sync') {
|
|
151
|
+
await this.#storage.sync(this.#baseDirectory, 'operation');
|
|
152
|
+
}
|
|
153
|
+
return response;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
this.#failed = true;
|
|
157
|
+
if (error instanceof WasixStorageError)
|
|
158
|
+
throw error;
|
|
159
|
+
return Promise.reject(new Error(`${describeError(error)}; this database can no longer be used and must be reopened`, { cause: error }));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async execStream(input, onChunk, persistence = 'sync') {
|
|
163
|
+
this.#assertHealthy();
|
|
164
|
+
try {
|
|
165
|
+
const status = this.#currentInstance().execProtocolStream(input, onChunk);
|
|
166
|
+
if (status !== PROTOCOL_STREAM_COMPLETE && status !== PROTOCOL_STREAM_CALLBACK_ABORTED) {
|
|
167
|
+
throw new Error(`WASIX host returned unknown protocol stream status ${status}`);
|
|
168
|
+
}
|
|
169
|
+
if (persistence === 'sync') {
|
|
170
|
+
await this.#storage.sync(this.#baseDirectory, 'operation');
|
|
171
|
+
}
|
|
172
|
+
return status === PROTOCOL_STREAM_CALLBACK_ABORTED ? 'callbackAborted' : 'complete';
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.#failed = true;
|
|
176
|
+
if (error instanceof WasixStorageError)
|
|
177
|
+
throw error;
|
|
178
|
+
throw new Error(`${describeError(error)}; this database can no longer be used and must be reopened`, { cause: error });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async runPgDump(options) {
|
|
182
|
+
this.#assertHealthy();
|
|
183
|
+
if (options.tool.name !== 'pg_dump') {
|
|
184
|
+
throw new TypeError('the same-realm tool path supports only pg_dump');
|
|
185
|
+
}
|
|
186
|
+
const cached = await this.#preparePgDump(options.tool);
|
|
187
|
+
const runTool = this.#host.runOliphauntToolDirect;
|
|
188
|
+
if (runTool === undefined) {
|
|
189
|
+
throw new Error('this WASIX host does not support same-realm pg_dump');
|
|
190
|
+
}
|
|
191
|
+
const mounts = await materializeWasixToolMounts(this.#host.Directory, cached.asset);
|
|
192
|
+
let result;
|
|
193
|
+
let failure;
|
|
194
|
+
let connection;
|
|
195
|
+
try {
|
|
196
|
+
result = await this.#runToolSession(async () => {
|
|
197
|
+
const activeConnection = new SynchronousPgDumpConnection({
|
|
198
|
+
startupResponse: this.#startupResponse,
|
|
199
|
+
startupIdentity: this.identity,
|
|
200
|
+
exec: (input, onChunk) => {
|
|
201
|
+
this.#currentInstance().execProtocolStream(input, onChunk);
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
connection = activeConnection;
|
|
205
|
+
const output = await runTool(cached.prepared, wasixToolRunOptions(cached.asset, options.args, mounts), (maximumBytes) => activeConnection.read(maximumBytes), (chunk) => activeConnection.write(chunk));
|
|
206
|
+
activeConnection.finish();
|
|
207
|
+
return {
|
|
208
|
+
exitCode: output.code,
|
|
209
|
+
stdout: output.stdoutBytes,
|
|
210
|
+
stderr: output.stderrBytes,
|
|
211
|
+
};
|
|
212
|
+
}, () => connection?.protocolStarted === true);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
failure = { primary: error };
|
|
216
|
+
}
|
|
217
|
+
if (failure !== undefined)
|
|
218
|
+
releaseWasixToolMounts(mounts, failure);
|
|
219
|
+
releaseWasixToolMounts(mounts);
|
|
220
|
+
if (result === undefined)
|
|
221
|
+
throw new Error('pg_dump completed without a process result');
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
async serve(connection, mode) {
|
|
225
|
+
this.#assertHealthy();
|
|
226
|
+
if (mode === 'tool') {
|
|
227
|
+
await this.#runToolSession(() => serveWasixProtocolConnection(connection, {
|
|
228
|
+
startupResponse: this.#startupResponse,
|
|
229
|
+
startupIdentity: this.identity,
|
|
230
|
+
execDuplex: (input, onRead, onWrite) => {
|
|
231
|
+
this.#currentInstance().execProtocolDuplex(input, onRead, onWrite);
|
|
232
|
+
},
|
|
233
|
+
publishIdle: async () => undefined,
|
|
234
|
+
rollback: async () => {
|
|
235
|
+
const response = this.#currentInstance().execProtocolRaw(simpleQuery('ROLLBACK'));
|
|
236
|
+
assertSuccessfulQueryResponse(response);
|
|
237
|
+
},
|
|
238
|
+
}));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
await this.#restartProtocolBackend();
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
this.#failed = true;
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
let primaryFailure;
|
|
249
|
+
try {
|
|
250
|
+
await serveWasixProtocolConnection(connection, {
|
|
251
|
+
startupResponse: this.#startupResponse,
|
|
252
|
+
startupIdentity: this.identity,
|
|
253
|
+
execDuplex: (input, onRead, onWrite) => {
|
|
254
|
+
this.#currentInstance().execProtocolDuplex(input, onRead, onWrite);
|
|
255
|
+
},
|
|
256
|
+
publishIdle: () => this.#storage.sync(this.#baseDirectory, 'operation'),
|
|
257
|
+
rollback: async () => {
|
|
258
|
+
const response = this.#currentInstance().execProtocolRaw(simpleQuery('ROLLBACK'));
|
|
259
|
+
assertSuccessfulQueryResponse(response);
|
|
260
|
+
await this.#storage.sync(this.#baseDirectory, 'operation');
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
primaryFailure = error;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
await this.#resetProtocolSession();
|
|
269
|
+
await this.#storage.sync(this.#baseDirectory, 'operation');
|
|
270
|
+
}
|
|
271
|
+
catch (cleanupError) {
|
|
272
|
+
this.#failed = true;
|
|
273
|
+
if (primaryFailure === undefined)
|
|
274
|
+
throw cleanupError;
|
|
275
|
+
throw composeLifecycleFailure(primaryFailure instanceof Error ? primaryFailure : new Error(describeError(primaryFailure)), 'WASIX server session cleanup also failed', cleanupError);
|
|
276
|
+
}
|
|
277
|
+
if (primaryFailure !== undefined) {
|
|
278
|
+
throw primaryFailure;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async #runToolSession(operation, outcomeUnknown = () => true) {
|
|
282
|
+
try {
|
|
283
|
+
await this.#resetProtocolSession();
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
this.#failed = true;
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
let result;
|
|
290
|
+
let primaryFailure;
|
|
291
|
+
try {
|
|
292
|
+
result = await operation();
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
primaryFailure = error;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
await this.#resetProtocolSession();
|
|
299
|
+
await this.#storage.sync(this.#baseDirectory, 'operation');
|
|
300
|
+
}
|
|
301
|
+
catch (cleanupError) {
|
|
302
|
+
this.#failed = true;
|
|
303
|
+
if (primaryFailure === undefined)
|
|
304
|
+
throw cleanupError;
|
|
305
|
+
throw composeLifecycleFailure(asError(primaryFailure), 'WASIX tool session cleanup also failed', cleanupError);
|
|
306
|
+
}
|
|
307
|
+
if (primaryFailure !== undefined) {
|
|
308
|
+
// A failure before either tool protocol callback is a known host/process
|
|
309
|
+
// failure. Once protocol activity begins, the database outcome may have
|
|
310
|
+
// escaped observation even though reset/publication itself succeeded.
|
|
311
|
+
if (outcomeUnknown())
|
|
312
|
+
this.#failed = true;
|
|
313
|
+
throw primaryFailure;
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
#preparePgDump(descriptor) {
|
|
318
|
+
validateWasixToolDescriptor(descriptor);
|
|
319
|
+
if (descriptor.name !== 'pg_dump') {
|
|
320
|
+
return Promise.reject(new TypeError('the same-realm tool path supports only pg_dump'));
|
|
321
|
+
}
|
|
322
|
+
const identity = wasixToolAssetIdentity(descriptor);
|
|
323
|
+
if (this.#pgDump !== undefined) {
|
|
324
|
+
if (this.#pgDumpIdentity !== identity) {
|
|
325
|
+
return Promise.reject(new Error('this database cannot replace its prepared pg_dump'));
|
|
326
|
+
}
|
|
327
|
+
return this.#pgDump;
|
|
328
|
+
}
|
|
329
|
+
const prepare = this.#host.prepareOliphauntTool;
|
|
330
|
+
if (prepare === undefined) {
|
|
331
|
+
return Promise.reject(new Error('this WASIX host does not support same-realm pg_dump'));
|
|
332
|
+
}
|
|
333
|
+
const pending = prepareWasixToolAsset(descriptor).then((asset) => ({
|
|
334
|
+
identity,
|
|
335
|
+
asset,
|
|
336
|
+
prepared: prepare(asset.module, asset.bytes),
|
|
337
|
+
}));
|
|
338
|
+
this.#pgDumpIdentity = identity;
|
|
339
|
+
this.#pgDump = pending;
|
|
340
|
+
void pending.catch(() => {
|
|
341
|
+
if (this.#pgDump === pending) {
|
|
342
|
+
this.#pgDump = undefined;
|
|
343
|
+
this.#pgDumpIdentity = undefined;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
return pending;
|
|
347
|
+
}
|
|
348
|
+
async #resetProtocolSession() {
|
|
349
|
+
for (const statement of ['ROLLBACK', 'DISCARD ALL']) {
|
|
350
|
+
const response = this.#currentInstance().execProtocolRaw(simpleQuery(statement));
|
|
351
|
+
assertSuccessfulQueryResponse(response);
|
|
352
|
+
}
|
|
353
|
+
await configureWasixRole(this.#username, async (input) => this.#currentInstance().execProtocolRaw(input));
|
|
354
|
+
}
|
|
355
|
+
async #restartProtocolBackend() {
|
|
356
|
+
const previous = this.#currentInstance();
|
|
357
|
+
this.#instance = undefined;
|
|
358
|
+
let failure;
|
|
359
|
+
try {
|
|
360
|
+
previous.close();
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
failure = new Error(`WASIX PostgreSQL backend restart close failed: ${describeError(error)}`, {
|
|
364
|
+
cause: error,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
previous.free();
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
failure =
|
|
372
|
+
failure === undefined
|
|
373
|
+
? new Error(`WASIX PostgreSQL backend restart release failed: ${describeError(error)}`, {
|
|
374
|
+
cause: error,
|
|
375
|
+
})
|
|
376
|
+
: composeLifecycleFailure(failure, 'backend allocation release also failed', error);
|
|
377
|
+
}
|
|
378
|
+
if (failure !== undefined)
|
|
379
|
+
throw failure;
|
|
380
|
+
let replacement;
|
|
381
|
+
try {
|
|
382
|
+
replacement = await this.#instantiate();
|
|
383
|
+
const startupResponse = await this.#initialize(replacement, 'existing');
|
|
384
|
+
this.#instance = replacement;
|
|
385
|
+
this.#startupResponse = startupResponse;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
failure = directStartupFailure(error);
|
|
389
|
+
if (replacement !== undefined) {
|
|
390
|
+
try {
|
|
391
|
+
replacement.close();
|
|
392
|
+
}
|
|
393
|
+
catch (closeError) {
|
|
394
|
+
failure = composeLifecycleFailure(failure, 'replacement WASIX backend cleanup also failed', closeError);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
replacement.free();
|
|
398
|
+
}
|
|
399
|
+
catch (freeError) {
|
|
400
|
+
failure = composeLifecycleFailure(failure, 'replacement WASIX backend allocation release also failed', freeError);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw failure;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async sync(boundary) {
|
|
407
|
+
this.#assertHealthy();
|
|
408
|
+
try {
|
|
409
|
+
await this.#storage.sync(this.#baseDirectory, boundary);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
this.#failed = true;
|
|
413
|
+
if (error instanceof WasixStorageError) {
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
throw new WasixStorageError(`WASIX PGDATA ${boundary} failed: ${describeError(error)}`, {
|
|
417
|
+
code: 'publication-failed',
|
|
418
|
+
commitState: 'unknown',
|
|
419
|
+
cause: error,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async backup() {
|
|
424
|
+
this.#assertHealthy();
|
|
425
|
+
try {
|
|
426
|
+
return await createPhysicalArchive((input) => this.#execBackupProtocol(input), this.#baseDirectory);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (error instanceof BackupModeExitUnconfirmedError)
|
|
430
|
+
this.#failed = true;
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async #execBackupProtocol(input) {
|
|
435
|
+
return this.#currentInstance().execProtocolRaw(input);
|
|
436
|
+
}
|
|
437
|
+
close() {
|
|
438
|
+
if (this.#closeAttempt !== undefined)
|
|
439
|
+
return this.#closeAttempt;
|
|
440
|
+
// Establish the admission cutoff before any guest/provider teardown can
|
|
441
|
+
// invoke userland code or yield.
|
|
442
|
+
this.#closed = true;
|
|
443
|
+
const attempt = createWasixDeferred();
|
|
444
|
+
this.#closeAttempt = attempt.promise;
|
|
445
|
+
void this.#closeInner().then(attempt.resolve, attempt.reject);
|
|
446
|
+
return attempt.promise;
|
|
447
|
+
}
|
|
448
|
+
async #closeInner() {
|
|
449
|
+
const pendingPgDump = this.#pgDump;
|
|
450
|
+
this.#pgDump = undefined;
|
|
451
|
+
this.#pgDumpIdentity = undefined;
|
|
452
|
+
let failure;
|
|
453
|
+
const instance = this.#instance;
|
|
454
|
+
this.#instance = undefined;
|
|
455
|
+
if (instance !== undefined) {
|
|
456
|
+
try {
|
|
457
|
+
instance.close();
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
failure = new Error(`WASIX PostgreSQL direct close failed: ${describeError(error)}`, {
|
|
461
|
+
cause: error,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
await this.#storage.close(this.#baseDirectory, failure === undefined && !this.#failed ? 'clean' : 'failed');
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
failure =
|
|
470
|
+
failure === undefined
|
|
471
|
+
? storageCloseFailure(error)
|
|
472
|
+
: composeLifecycleFailure(failure, 'storage release also failed', error);
|
|
473
|
+
}
|
|
474
|
+
if (instance !== undefined) {
|
|
475
|
+
try {
|
|
476
|
+
instance.free();
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
failure =
|
|
480
|
+
failure === undefined
|
|
481
|
+
? new Error(`WASIX allocation release failed: ${describeError(error)}`, {
|
|
482
|
+
cause: error,
|
|
483
|
+
})
|
|
484
|
+
: composeLifecycleFailure(failure, 'WASIX allocation release also failed', error);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const pgDump = await pendingPgDump?.catch(() => undefined);
|
|
488
|
+
if (pgDump !== undefined) {
|
|
489
|
+
try {
|
|
490
|
+
pgDump.prepared.free();
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
failure =
|
|
494
|
+
failure === undefined
|
|
495
|
+
? new Error(`prepared pg_dump release failed: ${describeError(error)}`, {
|
|
496
|
+
cause: error,
|
|
497
|
+
})
|
|
498
|
+
: composeLifecycleFailure(failure, 'prepared pg_dump release also failed', error);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (failure !== undefined) {
|
|
502
|
+
throw failure;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
#assertHealthy() {
|
|
506
|
+
if (this.#closed) {
|
|
507
|
+
throw new Error('Oliphaunt WASIX database is closed');
|
|
508
|
+
}
|
|
509
|
+
if (this.#failed) {
|
|
510
|
+
throw new Error('Oliphaunt WASIX database failed; close it and open a new one');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
#currentInstance() {
|
|
514
|
+
const instance = this.#instance;
|
|
515
|
+
if (instance === undefined) {
|
|
516
|
+
throw new Error('Oliphaunt WASIX database has no live PostgreSQL backend');
|
|
517
|
+
}
|
|
518
|
+
return instance;
|
|
519
|
+
}
|
|
520
|
+
async #closeAfterOpenFailure(failure) {
|
|
521
|
+
this.#closed = true;
|
|
522
|
+
this.#failed = true;
|
|
523
|
+
const instance = this.#instance;
|
|
524
|
+
this.#instance = undefined;
|
|
525
|
+
if (instance !== undefined) {
|
|
526
|
+
try {
|
|
527
|
+
instance.close();
|
|
528
|
+
}
|
|
529
|
+
catch (closeError) {
|
|
530
|
+
failure = composeLifecycleFailure(failure, 'WASIX instance cleanup also failed', closeError);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
await this.#storage.close(this.#baseDirectory, 'failed');
|
|
535
|
+
}
|
|
536
|
+
catch (releaseError) {
|
|
537
|
+
failure = composeLifecycleFailure(failure, 'storage release also failed', releaseError);
|
|
538
|
+
}
|
|
539
|
+
if (instance !== undefined) {
|
|
540
|
+
try {
|
|
541
|
+
instance.free();
|
|
542
|
+
}
|
|
543
|
+
catch (freeError) {
|
|
544
|
+
failure = composeLifecycleFailure(failure, 'WASIX allocation release also failed', freeError);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return failure;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function assertDirectExtensionCompatibility(options) {
|
|
551
|
+
const unsupported = Object.values(options.extensionCarriers)
|
|
552
|
+
.flatMap((carrier) => carrier.install.nativeModules
|
|
553
|
+
.filter((module) => module.size > CHROMIUM_SYNC_WASM_LIMIT_BYTES)
|
|
554
|
+
.map((module) => ({
|
|
555
|
+
extension: carrier.sqlName,
|
|
556
|
+
module,
|
|
557
|
+
})))
|
|
558
|
+
.sort((left, right) => left.extension.localeCompare(right.extension));
|
|
559
|
+
if (unsupported.length === 0) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const detail = unsupported
|
|
563
|
+
.map(({ extension, module }) => `${extension}:${module.path} (${module.size.toLocaleString('en-US')} bytes)`)
|
|
564
|
+
.join(', ');
|
|
565
|
+
throw new TypeError(`@oliphaunt/wasix-ts cannot load native extension modules larger than 8 MiB in a Chromium Window; use @oliphaunt/wasix-ts/worker for ${detail}`);
|
|
566
|
+
}
|
|
567
|
+
function directStartupFailure(error) {
|
|
568
|
+
const protocolResponse = directProtocolResponse(error);
|
|
569
|
+
if (protocolResponse !== undefined) {
|
|
570
|
+
try {
|
|
571
|
+
assertSuccessfulStartupResponse(protocolResponse);
|
|
572
|
+
}
|
|
573
|
+
catch (postgresError) {
|
|
574
|
+
if (postgresError instanceof Error) {
|
|
575
|
+
return postgresError;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (error instanceof WasixStorageError || error instanceof PostgresError) {
|
|
580
|
+
return error;
|
|
581
|
+
}
|
|
582
|
+
return new Error(`WASIX PostgreSQL direct startup failed: ${describeError(error)}`, {
|
|
583
|
+
cause: error,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
function newStorageRoleError(username) {
|
|
587
|
+
return new WasixStorageError(`PostgreSQL username ${JSON.stringify(username)} selects an existing role; new storage must first be opened as postgres`, { code: 'unavailable', commitState: 'unchanged' });
|
|
588
|
+
}
|
|
589
|
+
function directProtocolResponse(error) {
|
|
590
|
+
if (typeof error !== 'object' || error === null || !('protocolResponse' in error)) {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
const response = error.protocolResponse;
|
|
594
|
+
return response instanceof Uint8Array ? response : undefined;
|
|
595
|
+
}
|
|
596
|
+
function storageCloseFailure(error) {
|
|
597
|
+
if (error instanceof WasixStorageError) {
|
|
598
|
+
return error;
|
|
599
|
+
}
|
|
600
|
+
return new WasixStorageError(`WASIX PGDATA close failed: ${describeError(error)}`, {
|
|
601
|
+
code: 'publication-failed',
|
|
602
|
+
commitState: 'unknown',
|
|
603
|
+
cause: error,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
function asError(error) {
|
|
607
|
+
return error instanceof Error ? error : new Error(describeError(error));
|
|
608
|
+
}
|
|
609
|
+
/** @internal Cache verified runtime materialization without caching transient failures. */
|
|
610
|
+
export function prepareRuntimeCached(options, prepare = prepareWasixRuntime) {
|
|
611
|
+
const identity = preparedRuntimeIdentity(options);
|
|
612
|
+
let prepared = preparedRuntimes.get(identity);
|
|
613
|
+
if (prepared === undefined) {
|
|
614
|
+
const attempt = prepare(options);
|
|
615
|
+
prepared = attempt;
|
|
616
|
+
preparedRuntimes.set(identity, attempt);
|
|
617
|
+
void attempt.catch(() => {
|
|
618
|
+
if (preparedRuntimes.get(identity) === attempt) {
|
|
619
|
+
preparedRuntimes.delete(identity);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
while (preparedRuntimes.size > MAX_PREPARED_RUNTIMES) {
|
|
623
|
+
const oldest = preparedRuntimes.keys().next().value;
|
|
624
|
+
if (oldest === undefined)
|
|
625
|
+
break;
|
|
626
|
+
preparedRuntimes.delete(oldest);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return prepared;
|
|
630
|
+
}
|
|
631
|
+
function initializeHost(host) {
|
|
632
|
+
let initialized = initializedHosts.get(host);
|
|
633
|
+
if (initialized === undefined) {
|
|
634
|
+
initialized = host.init({}).then(() => undefined);
|
|
635
|
+
initializedHosts.set(host, initialized);
|
|
636
|
+
void initialized.catch(() => {
|
|
637
|
+
if (initializedHosts.get(host) === initialized) {
|
|
638
|
+
initializedHosts.delete(host);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return initialized;
|
|
643
|
+
}
|
|
644
|
+
/** Keep direct host startup observable to every JS event loop and bound genuine stalls. */
|
|
645
|
+
function instantiateDirectWithDeadline(operation) {
|
|
646
|
+
return new Promise((resolve, reject) => {
|
|
647
|
+
let expired = false;
|
|
648
|
+
const timer = setTimeout(() => {
|
|
649
|
+
expired = true;
|
|
650
|
+
reject(new Error(`Oliphaunt WASIX startup exceeded ${DIRECT_INSTANCE_DEADLINE_MS}ms`));
|
|
651
|
+
}, DIRECT_INSTANCE_DEADLINE_MS);
|
|
652
|
+
void operation.then((instance) => {
|
|
653
|
+
if (expired) {
|
|
654
|
+
try {
|
|
655
|
+
instance.close();
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// Startup has already failed; continue to release the late allocation.
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
instance.free();
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// The primary timeout remains the actionable failure.
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
clearTimeout(timer);
|
|
669
|
+
resolve(instance);
|
|
670
|
+
}, (error) => {
|
|
671
|
+
if (expired)
|
|
672
|
+
return;
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
reject(error);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function preparedRuntimeIdentity(options) {
|
|
679
|
+
const runtime = options.runtime;
|
|
680
|
+
return canonicalJson({
|
|
681
|
+
runtime: {
|
|
682
|
+
product: runtime.product,
|
|
683
|
+
version: runtime.version,
|
|
684
|
+
runtimeArchive: assetIdentity(runtime.runtimeArchive),
|
|
685
|
+
standardSeedArchive: assetIdentity(runtime.standardSeedArchive),
|
|
686
|
+
standardSeedManifest: {
|
|
687
|
+
sha256: runtime.standardSeedManifest.sha256,
|
|
688
|
+
size: runtime.standardSeedManifest.size,
|
|
689
|
+
},
|
|
690
|
+
manifest: {
|
|
691
|
+
sha256: runtime.manifest.sha256,
|
|
692
|
+
size: runtime.manifest.size,
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
icu: options.icu === undefined
|
|
696
|
+
? null
|
|
697
|
+
: {
|
|
698
|
+
product: options.icu.product,
|
|
699
|
+
version: options.icu.version,
|
|
700
|
+
compatibility: options.icu.compatibility,
|
|
701
|
+
dataArchive: assetIdentity(options.icu.dataArchive),
|
|
702
|
+
clusterSeedArchive: assetIdentity(options.icu.clusterSeedArchive),
|
|
703
|
+
clusterSeedManifest: {
|
|
704
|
+
sha256: options.icu.clusterSeedManifest.sha256,
|
|
705
|
+
size: options.icu.clusterSeedManifest.size,
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
extensions: options.extensions,
|
|
709
|
+
carriers: Object.values(options.extensionCarriers)
|
|
710
|
+
.sort((left, right) => left.sqlName.localeCompare(right.sqlName))
|
|
711
|
+
.map(({ source: _source, ...carrier }) => carrier),
|
|
712
|
+
startupGUCs: options.startupGUCs,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function assetIdentity(asset) {
|
|
716
|
+
return { archive: asset.archive, sha256: asset.sha256, size: asset.size };
|
|
717
|
+
}
|