@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.
Files changed (169) hide show
  1. package/ARCHITECTURE.md +655 -0
  2. package/CHANGELOG.md +33 -0
  3. package/LICENSE +21 -0
  4. package/README.md +404 -0
  5. package/THIRD_PARTY_NOTICES.md +20 -0
  6. package/lib/archive.d.ts +21 -0
  7. package/lib/archive.js +336 -0
  8. package/lib/asset-source.d.ts +4 -0
  9. package/lib/asset-source.js +15 -0
  10. package/lib/byte-channel.d.ts +27 -0
  11. package/lib/byte-channel.js +170 -0
  12. package/lib/client-common.d.ts +6 -0
  13. package/lib/client-common.js +36 -0
  14. package/lib/client.d.ts +7 -0
  15. package/lib/client.js +24 -0
  16. package/lib/database-root.d.ts +19 -0
  17. package/lib/database-root.js +139 -0
  18. package/lib/database.d.ts +135 -0
  19. package/lib/database.js +1039 -0
  20. package/lib/descriptor-validation.d.ts +10 -0
  21. package/lib/descriptor-validation.js +75 -0
  22. package/lib/direct-client-common.d.ts +50 -0
  23. package/lib/direct-client-common.js +717 -0
  24. package/lib/direct-client.d.ts +4 -0
  25. package/lib/direct-client.js +13 -0
  26. package/lib/direct.node.d.ts +2 -0
  27. package/lib/direct.node.js +2 -0
  28. package/lib/errors.d.ts +25 -0
  29. package/lib/errors.js +34 -0
  30. package/lib/extension-descriptor.d.ts +14 -0
  31. package/lib/extension-descriptor.js +419 -0
  32. package/lib/extensions.d.ts +68 -0
  33. package/lib/extensions.js +769 -0
  34. package/lib/host/LICENSE +21 -0
  35. package/lib/host/index.d.mts +123 -0
  36. package/lib/host/index.mjs +11 -0
  37. package/lib/host/provenance.json +16 -0
  38. package/lib/host/wasmer_js_bg.wasm +0 -0
  39. package/lib/host/worker.mjs +11 -0
  40. package/lib/host-runtime.d.ts +5 -0
  41. package/lib/host-runtime.js +13 -0
  42. package/lib/icu-descriptor.d.ts +3 -0
  43. package/lib/icu-descriptor.js +92 -0
  44. package/lib/index.bun.d.ts +2 -0
  45. package/lib/index.bun.js +2 -0
  46. package/lib/index.d.ts +2 -0
  47. package/lib/index.deno.d.ts +2 -0
  48. package/lib/index.deno.js +2 -0
  49. package/lib/index.js +2 -0
  50. package/lib/index.node.d.ts +2 -0
  51. package/lib/index.node.js +2 -0
  52. package/lib/internal-common.d.ts +12 -0
  53. package/lib/internal-common.js +274 -0
  54. package/lib/internal.d.ts +5 -0
  55. package/lib/internal.js +42 -0
  56. package/lib/internal.node.d.ts +5 -0
  57. package/lib/internal.node.js +8 -0
  58. package/lib/native-addon.d.ts +111 -0
  59. package/lib/native-addon.js +223 -0
  60. package/lib/native-server.d.ts +21 -0
  61. package/lib/native-server.js +109 -0
  62. package/lib/native-session.d.ts +60 -0
  63. package/lib/native-session.js +565 -0
  64. package/lib/node-actor.d.ts +7 -0
  65. package/lib/node-actor.js +10 -0
  66. package/lib/node-client-common.d.ts +9 -0
  67. package/lib/node-client-common.js +35 -0
  68. package/lib/node-client.d.ts +4 -0
  69. package/lib/node-client.js +13 -0
  70. package/lib/node-direct.d.ts +7 -0
  71. package/lib/node-direct.js +10 -0
  72. package/lib/node-worker-options.d.ts +5 -0
  73. package/lib/node-worker-options.js +65 -0
  74. package/lib/node-worker-port.d.ts +4 -0
  75. package/lib/node-worker-port.js +116 -0
  76. package/lib/node-worker.d.ts +1 -0
  77. package/lib/node-worker.js +38 -0
  78. package/lib/pgwire-connection.d.ts +60 -0
  79. package/lib/pgwire-connection.js +528 -0
  80. package/lib/pgwire.d.ts +3 -0
  81. package/lib/pgwire.js +105 -0
  82. package/lib/physical-archive.d.ts +29 -0
  83. package/lib/physical-archive.js +527 -0
  84. package/lib/protocol.d.ts +1 -0
  85. package/lib/protocol.js +1 -0
  86. package/lib/public.d.ts +4 -0
  87. package/lib/public.js +3 -0
  88. package/lib/query.d.ts +1 -0
  89. package/lib/query.js +1 -0
  90. package/lib/rpc.d.ts +203 -0
  91. package/lib/rpc.js +84 -0
  92. package/lib/runtime-descriptor.d.ts +3 -0
  93. package/lib/runtime-descriptor.js +79 -0
  94. package/lib/server.node.d.ts +1 -0
  95. package/lib/server.node.js +1 -0
  96. package/lib/startup-config.d.ts +2 -0
  97. package/lib/startup-config.js +19 -0
  98. package/lib/storage/bun.d.ts +6 -0
  99. package/lib/storage/bun.js +6 -0
  100. package/lib/storage/deno.d.ts +7 -0
  101. package/lib/storage/deno.js +7 -0
  102. package/lib/storage/incremental-storage.d.ts +25 -0
  103. package/lib/storage/incremental-storage.js +154 -0
  104. package/lib/storage/indexed-db-provider.d.ts +40 -0
  105. package/lib/storage/indexed-db-provider.js +259 -0
  106. package/lib/storage/indexed-db.d.ts +9 -0
  107. package/lib/storage/indexed-db.js +11 -0
  108. package/lib/storage/node.d.ts +10 -0
  109. package/lib/storage/node.js +13 -0
  110. package/lib/storage/opfs-pool.d.ts +31 -0
  111. package/lib/storage/opfs-pool.js +1271 -0
  112. package/lib/storage/opfs-provider.d.ts +4 -0
  113. package/lib/storage/opfs-provider.js +257 -0
  114. package/lib/storage/opfs.d.ts +8 -0
  115. package/lib/storage/opfs.js +10 -0
  116. package/lib/storage/restore-cleanup.d.ts +4 -0
  117. package/lib/storage/restore-cleanup.js +22 -0
  118. package/lib/storage/web-lock.d.ts +2 -0
  119. package/lib/storage/web-lock.js +64 -0
  120. package/lib/storage-provider.d.ts +47 -0
  121. package/lib/storage-provider.js +141 -0
  122. package/lib/storage-snapshot.d.ts +44 -0
  123. package/lib/storage-snapshot.js +274 -0
  124. package/lib/storage.d.ts +46 -0
  125. package/lib/storage.js +83 -0
  126. package/lib/tool-runtime.d.ts +43 -0
  127. package/lib/tool-runtime.js +93 -0
  128. package/lib/tool-worker-common.d.ts +48 -0
  129. package/lib/tool-worker-common.js +97 -0
  130. package/lib/tool-worker.d.ts +1 -0
  131. package/lib/tool-worker.js +10 -0
  132. package/lib/types.d.ts +230 -0
  133. package/lib/types.js +1 -0
  134. package/lib/wasix-runtime.d.ts +24 -0
  135. package/lib/wasix-runtime.js +186 -0
  136. package/lib/worker-client.d.ts +4 -0
  137. package/lib/worker-client.js +45 -0
  138. package/lib/worker-dispatch.d.ts +11 -0
  139. package/lib/worker-dispatch.js +174 -0
  140. package/lib/worker-entry.bun.d.ts +2 -0
  141. package/lib/worker-entry.bun.js +2 -0
  142. package/lib/worker-entry.d.ts +2 -0
  143. package/lib/worker-entry.deno.d.ts +2 -0
  144. package/lib/worker-entry.deno.js +2 -0
  145. package/lib/worker-entry.js +2 -0
  146. package/lib/worker-entry.node.d.ts +2 -0
  147. package/lib/worker-entry.node.js +2 -0
  148. package/lib/worker-node-client.d.ts +4 -0
  149. package/lib/worker-node-client.js +55 -0
  150. package/lib/worker-rpc.d.ts +36 -0
  151. package/lib/worker-rpc.js +422 -0
  152. package/lib/worker-transfer.d.ts +6 -0
  153. package/lib/worker-transfer.js +11 -0
  154. package/lib/worker.d.ts +1 -0
  155. package/lib/worker.js +21 -0
  156. package/lib/zstd.d.ts +4 -0
  157. package/lib/zstd.js +12 -0
  158. package/node_modules/@oliphaunt/js-core/README.md +7 -0
  159. package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.d.ts +1 -0
  160. package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.js +22 -0
  161. package/node_modules/@oliphaunt/js-core/dist/commonjs/query.d.ts +255 -0
  162. package/node_modules/@oliphaunt/js-core/dist/commonjs/query.js +2068 -0
  163. package/node_modules/@oliphaunt/js-core/dist/module/package.json +3 -0
  164. package/node_modules/@oliphaunt/js-core/dist/module/protocol.d.ts +1 -0
  165. package/node_modules/@oliphaunt/js-core/dist/module/protocol.js +19 -0
  166. package/node_modules/@oliphaunt/js-core/dist/module/query.d.ts +255 -0
  167. package/node_modules/@oliphaunt/js-core/dist/module/query.js +2039 -0
  168. package/node_modules/@oliphaunt/js-core/package.json +21 -0
  169. package/package.json +122 -0
@@ -0,0 +1,527 @@
1
+ import { PHYSICAL_FORMAT as WASIX_PHYSICAL_FORMAT, POSTGRES_MAJOR as WASIX_POSTGRES_MAJOR, } from '@oliphaunt/liboliphaunt-wasix';
2
+ import { extractTar } from './archive.js';
3
+ import { WasixStorageError } from './errors.js';
4
+ import { simpleQuery } from './protocol.js';
5
+ import { PostgresError, parseSimpleQueryRawResponse } from './query.js';
6
+ import { snapshotStorageDirectory, validateStoredSnapshot, } from './storage-snapshot.js';
7
+ const TAR_BLOCK_BYTES = 512;
8
+ const ARCHIVE_MANIFEST_PATH = '.oliphaunt/backup-manifest.properties';
9
+ const ARCHIVE_MANIFEST = 'archiveLayout=oliphaunt-physical-archive-v1\n' +
10
+ 'product=oliphaunt\n' +
11
+ 'engineFamily=wasix\n' +
12
+ `physicalFormat=${WASIX_PHYSICAL_FORMAT}\n` +
13
+ `postgresMajor=${WASIX_POSTGRES_MAJOR}\n`;
14
+ const encoder = new TextEncoder();
15
+ /** @internal Identifies the only backup failure that makes the session unsafe to reuse. */
16
+ export class BackupModeExitUnconfirmedError extends Error {
17
+ constructor(message, cause) {
18
+ super(message, { cause });
19
+ this.name = 'BackupModeExitUnconfirmedError';
20
+ }
21
+ }
22
+ /** Create one PostgreSQL online physical backup without restarting its session. */
23
+ export async function createPhysicalArchive(exec, directory) {
24
+ let state = 'not-entered';
25
+ try {
26
+ let startResponse;
27
+ try {
28
+ startResponse = await exec(simpleQuery("SELECT pg_walfile_name(pg_backup_start(label => 'oliphaunt physical backup', fast => true)), pg_size_bytes(current_setting('wal_segment_size'))::text"));
29
+ }
30
+ catch (error) {
31
+ state = 'exit-unconfirmed';
32
+ throw error;
33
+ }
34
+ let start;
35
+ try {
36
+ start = parseSimpleQueryRawResponse(startResponse);
37
+ state = 'exit-required';
38
+ }
39
+ catch (error) {
40
+ if (error instanceof PostgresError)
41
+ throw error;
42
+ state = 'exit-required';
43
+ if (!responseConfirmsCommandCompletion(startResponse)) {
44
+ throw new Error('pg_backup_start did not return a successful PostgreSQL command tag', {
45
+ cause: error,
46
+ });
47
+ }
48
+ throw error;
49
+ }
50
+ if (start.commandTag !== 'SELECT 1') {
51
+ throw new Error('pg_backup_start did not return a successful PostgreSQL command tag');
52
+ }
53
+ if (start.rows.length !== 1 || start.rows[0]?.values.length !== 2) {
54
+ throw new Error('pg_backup_start returned an unexpected result');
55
+ }
56
+ const startWal = start.rows[0].text(0);
57
+ const walSegmentSizeText = start.rows[0].text(1);
58
+ if (startWal === null || walSegmentSizeText === null) {
59
+ throw new Error('pg_backup_start returned an unexpected result');
60
+ }
61
+ const walSegmentSize = Number(walSegmentSizeText);
62
+ const beforeStop = refreshBackupPgControl(await snapshotPhysicalBackupBulk(directory), await directory.readFile('global/pg_control'));
63
+ const stop = await stopPhysicalBackup(exec);
64
+ state = stop.state;
65
+ if (stop.state === 'exit-unconfirmed') {
66
+ throw stop.error;
67
+ }
68
+ if (stop.files === undefined)
69
+ throw stop.validationError;
70
+ const { stopWal, backupLabel, tablespaceMap } = stop.files;
71
+ const walNames = requiredBackupWalNames(startWal, stopWal, walSegmentSize);
72
+ const walFiles = await Promise.all(walNames.map(async (name) => {
73
+ const bytes = await directory.readFile(`pg_wal/${name}`);
74
+ if (bytes.length !== walSegmentSize) {
75
+ throw new Error(`physical backup WAL segment ${name} has the wrong size`);
76
+ }
77
+ return { path: `pg_wal/${name}`, bytes };
78
+ }));
79
+ const snapshot = mergeBackupSnapshots(beforeStop, walFiles, backupLabel, tablespaceMap);
80
+ return encodePhysicalArchive(snapshot);
81
+ }
82
+ catch (primaryError) {
83
+ if (state === 'exit-required' || state === 'exit-unconfirmed') {
84
+ const cleanup = await stopPhysicalBackup(exec);
85
+ state = cleanup.state;
86
+ if (cleanup.state === 'exit-unconfirmed') {
87
+ throw backupModeExitUnconfirmed(primaryError, cleanup.error);
88
+ }
89
+ if (cleanup.validationError !== undefined) {
90
+ throw backupCleanupFailure(primaryError, cleanup.validationError);
91
+ }
92
+ }
93
+ throw primaryError;
94
+ }
95
+ }
96
+ async function stopPhysicalBackup(exec) {
97
+ let response;
98
+ try {
99
+ response = await exec(simpleQuery('SELECT pg_walfile_name(lsn), labelfile, spcmapfile FROM pg_backup_stop(wait_for_archive => false)'));
100
+ }
101
+ catch (error) {
102
+ return { state: 'exit-unconfirmed', error };
103
+ }
104
+ const exitConfirmed = responseConfirmsCommandCompletion(response);
105
+ let result;
106
+ try {
107
+ result = parseSimpleQueryRawResponse(response);
108
+ }
109
+ catch (error) {
110
+ if (exitConfirmed && !(error instanceof PostgresError)) {
111
+ return { state: 'exited', validationError: error };
112
+ }
113
+ if (!exitConfirmed && !(error instanceof PostgresError)) {
114
+ return {
115
+ state: 'exit-unconfirmed',
116
+ error: new Error('pg_backup_stop did not return a successful PostgreSQL command completion', { cause: error }),
117
+ };
118
+ }
119
+ return { state: 'exit-unconfirmed', error };
120
+ }
121
+ if (!exitConfirmed) {
122
+ return {
123
+ state: 'exit-unconfirmed',
124
+ error: new Error('pg_backup_stop did not return a successful PostgreSQL command completion'),
125
+ };
126
+ }
127
+ if (result.commandTag !== 'SELECT 1') {
128
+ return {
129
+ state: 'exited',
130
+ validationError: new Error('pg_backup_stop returned an unexpected PostgreSQL command tag'),
131
+ };
132
+ }
133
+ try {
134
+ const row = result.rows[0];
135
+ if (row === undefined || row.values.length !== 3) {
136
+ throw new Error('pg_backup_stop returned an unexpected result');
137
+ }
138
+ const stopWal = row.text(0);
139
+ const backupLabel = row.text(1);
140
+ const tablespaceMap = row.text(2);
141
+ if (stopWal === null)
142
+ throw new Error('pg_backup_stop returned an unexpected result');
143
+ if (backupLabel === null || backupLabel.length === 0) {
144
+ throw new Error('pg_backup_stop returned an empty backup label');
145
+ }
146
+ return { state: 'exited', files: { stopWal, backupLabel, tablespaceMap } };
147
+ }
148
+ catch (validationError) {
149
+ // A fully parsed response without ErrorResponse proves pg_backup_stop ran.
150
+ return { state: 'exited', validationError };
151
+ }
152
+ }
153
+ function responseConfirmsCommandCompletion(response) {
154
+ let offset = 0;
155
+ let sawCommandComplete = false;
156
+ while (offset < response.length) {
157
+ if (response.length - offset < 5)
158
+ return false;
159
+ const length = new DataView(response.buffer, response.byteOffset + offset + 1, 4).getUint32(0);
160
+ if (length < 4 || length + 1 > response.length - offset)
161
+ return false;
162
+ const tag = response[offset];
163
+ if (tag === 0x45)
164
+ return false;
165
+ if (tag === 0x43)
166
+ sawCommandComplete = true;
167
+ const next = offset + length + 1;
168
+ if (tag === 0x5a) {
169
+ const status = response[offset + 5];
170
+ return (sawCommandComplete &&
171
+ length === 5 &&
172
+ next === response.length &&
173
+ (status === 0x49 || status === 0x54 || status === 0x45));
174
+ }
175
+ offset = next;
176
+ }
177
+ return false;
178
+ }
179
+ function backupCleanupFailure(primary, cleanup) {
180
+ return new AggregateError([primary, cleanup], `physical backup failed: ${backupFailureMessage(primary)}; PostgreSQL left backup mode but cleanup validation also failed: ${backupFailureMessage(cleanup)}`);
181
+ }
182
+ function backupModeExitUnconfirmed(primary, cleanup) {
183
+ return new BackupModeExitUnconfirmedError(`physical backup failed: ${backupFailureMessage(primary)}; PostgreSQL could not confirm leaving backup mode cleanly: ${backupFailureMessage(cleanup)}`, new AggregateError([primary, cleanup], 'physical backup and cleanup both failed'));
184
+ }
185
+ function backupFailureMessage(error) {
186
+ return error instanceof Error ? error.message : String(error);
187
+ }
188
+ /** @internal Read the bulk backup tree without reading transient or pre-stop WAL contents. */
189
+ export async function snapshotPhysicalBackupBulk(directory) {
190
+ return withoutPostStopState(await snapshotStorageDirectory(directory, {
191
+ skipFile: isTransientBackupEntry,
192
+ skipDirectoryContents: (path) => path === 'pg_wal' || TRANSIENT_STATE_DIRECTORIES.has(path),
193
+ }));
194
+ }
195
+ /** @internal Prove the online backup contains every required start-to-stop WAL segment. */
196
+ export function validateBackupWalRange(snapshot, startName, stopName, segmentSize) {
197
+ const names = requiredBackupWalNames(startName, stopName, segmentSize);
198
+ const files = new Map(snapshot.files.map(({ path, bytes }) => [path, bytes.length]));
199
+ for (const name of names) {
200
+ const length = files.get(`pg_wal/${name}`);
201
+ if (length === undefined)
202
+ throw new Error(`physical backup is missing WAL segment ${name}`);
203
+ if (length !== segmentSize)
204
+ throw new Error(`physical backup WAL segment ${name} has the wrong size`);
205
+ }
206
+ }
207
+ /** @internal Enumerate PostgreSQL's inclusive start-to-stop WAL range. */
208
+ export function requiredBackupWalNames(startName, stopName, segmentSize) {
209
+ if (!Number.isSafeInteger(segmentSize) ||
210
+ segmentSize < 1024 * 1024 ||
211
+ segmentSize > 1024 * 1024 * 1024 ||
212
+ (segmentSize & (segmentSize - 1)) !== 0) {
213
+ throw new Error('PostgreSQL returned an invalid WAL segment size');
214
+ }
215
+ const start = parseWalName(startName, segmentSize);
216
+ const stop = parseWalName(stopName, segmentSize);
217
+ if (start.timeline !== stop.timeline) {
218
+ throw new Error('physical backup WAL range crosses timelines');
219
+ }
220
+ if (start.segment > stop.segment) {
221
+ throw new Error('physical backup WAL range is reversed');
222
+ }
223
+ const segmentsPerLog = 0x1_0000_0000 / segmentSize;
224
+ const names = [];
225
+ for (let segment = start.segment; segment <= stop.segment; segment += 1) {
226
+ const log = Math.floor(segment / segmentsPerLog);
227
+ const index = segment % segmentsPerLog;
228
+ const name = `${start.timeline.toString(16).padStart(8, '0')}${log
229
+ .toString(16)
230
+ .padStart(8, '0')}${index.toString(16).padStart(8, '0')}`.toUpperCase();
231
+ names.push(name);
232
+ }
233
+ return names;
234
+ }
235
+ function parseWalName(name, segmentSize) {
236
+ if (!/^[0-9A-F]{24}$/u.test(name)) {
237
+ throw new Error(`PostgreSQL returned an invalid WAL filename ${JSON.stringify(name)}`);
238
+ }
239
+ const timeline = Number.parseInt(name.slice(0, 8), 16);
240
+ const log = Number.parseInt(name.slice(8, 16), 16);
241
+ const index = Number.parseInt(name.slice(16), 16);
242
+ const segmentsPerLog = 0x1_0000_0000 / segmentSize;
243
+ if (index >= segmentsPerLog) {
244
+ throw new Error(`PostgreSQL returned an invalid WAL filename ${JSON.stringify(name)}`);
245
+ }
246
+ return { timeline, segment: log * segmentsPerLog + index };
247
+ }
248
+ /** Validate one Oliphaunt physical tar and return its provider-neutral PGDATA snapshot. */
249
+ export function decodePhysicalArchive(bytes) {
250
+ try {
251
+ validateTarFraming(bytes);
252
+ const archive = extractTar(bytes);
253
+ const manifest = archive.files.get(ARCHIVE_MANIFEST_PATH);
254
+ if (manifest === undefined || new TextDecoder().decode(manifest) !== ARCHIVE_MANIFEST) {
255
+ throw corruptArchive('has a missing or incompatible backup manifest');
256
+ }
257
+ const directories = [];
258
+ const files = [];
259
+ for (const path of archive.directories) {
260
+ if (path === 'pgdata')
261
+ continue;
262
+ if (path === '.oliphaunt')
263
+ continue;
264
+ if (!path.startsWith('pgdata/')) {
265
+ throw corruptArchive(`contains unexpected directory ${JSON.stringify(path)}`);
266
+ }
267
+ directories.push(path.slice('pgdata/'.length));
268
+ }
269
+ for (const [path, contents] of archive.files) {
270
+ if (path === ARCHIVE_MANIFEST_PATH)
271
+ continue;
272
+ if (!path.startsWith('pgdata/')) {
273
+ throw corruptArchive(`contains unexpected file ${JSON.stringify(path)}`);
274
+ }
275
+ const relative = path.slice('pgdata/'.length);
276
+ if (relative === '.oliphaunt.json') {
277
+ throw corruptArchive('contains destination-owned database-root metadata');
278
+ }
279
+ files.push({ path: relative, bytes: contents.slice() });
280
+ }
281
+ const backupLabel = files.find((file) => file.path === 'backup_label');
282
+ if (backupLabel === undefined || backupLabel.bytes.length === 0) {
283
+ throw corruptArchive('has a missing or empty pgdata/backup_label');
284
+ }
285
+ if (!directories.includes('base')) {
286
+ throw corruptArchive('is missing pgdata/base');
287
+ }
288
+ return validateStoredSnapshot({ schema: 'oliphaunt-wasix-directory-snapshot-v1', directories, files }, String(WASIX_POSTGRES_MAJOR), {
289
+ label: 'physical archive',
290
+ corrupt: (detail, cause) => corruptArchive(detail, cause),
291
+ });
292
+ }
293
+ catch (error) {
294
+ if (error instanceof WasixStorageError)
295
+ throw error;
296
+ throw corruptArchive('is malformed', error);
297
+ }
298
+ }
299
+ /** @internal Apply PostgreSQL's standard online-backup transient-state exclusions. */
300
+ export function withoutPostStopState(snapshot) {
301
+ return {
302
+ ...snapshot,
303
+ directories: snapshot.directories.filter((path) => !pathWithin(path, 'pg_wal') && !isTransientBackupEntry(path)),
304
+ files: snapshot.files.filter(({ path }) => !pathWithin(path, 'pg_wal') &&
305
+ path !== 'backup_label' &&
306
+ path !== 'tablespace_map' &&
307
+ !isTransientBackupEntry(path)),
308
+ };
309
+ }
310
+ /** @internal Replace the bulk-scan control file with the final pre-stop read. */
311
+ export function refreshBackupPgControl(snapshot, pgControl) {
312
+ if (pgControl.length === 0)
313
+ throw new Error('final PGDATA global/pg_control is empty');
314
+ const files = snapshot.files.filter(({ path }) => path !== 'global/pg_control');
315
+ files.push({ path: 'global/pg_control', bytes: pgControl.slice() });
316
+ files.sort(compareFile);
317
+ return { ...snapshot, files };
318
+ }
319
+ function isTransientBackupEntry(path) {
320
+ if ([
321
+ 'postmaster.pid',
322
+ 'postmaster.opts',
323
+ 'postgresql.auto.conf.tmp',
324
+ 'current_logfiles.tmp',
325
+ 'backup_label',
326
+ 'tablespace_map',
327
+ 'backup_manifest',
328
+ ].includes(path)) {
329
+ return true;
330
+ }
331
+ const parts = path.split('/');
332
+ const name = parts.at(-1) ?? '';
333
+ if (name === '.DS_Store' || name.startsWith('pg_internal.init') || name.startsWith('pgsql_tmp')) {
334
+ return true;
335
+ }
336
+ return parts.length > 1 && TRANSIENT_STATE_DIRECTORIES.has(parts[0] ?? '');
337
+ }
338
+ const TRANSIENT_STATE_DIRECTORIES = new Set([
339
+ 'pg_dynshmem',
340
+ 'pg_notify',
341
+ 'pg_replslot',
342
+ 'pg_serial',
343
+ 'pg_snapshots',
344
+ 'pg_stat_tmp',
345
+ 'pg_subtrans',
346
+ ]);
347
+ /** @internal Merge only the proven required WAL files into the bulk PGDATA scan. */
348
+ export function mergeBackupSnapshots(beforeStop, walFiles, backupLabel, tablespaceMap) {
349
+ const directories = new Set(beforeStop.directories.filter((path) => !pathWithin(path, 'pg_wal')));
350
+ const files = new Map(beforeStop.files
351
+ .filter((file) => !pathWithin(file.path, 'pg_wal'))
352
+ .map((file) => [file.path, file.bytes]));
353
+ directories.add('pg_wal');
354
+ for (const file of walFiles)
355
+ files.set(file.path, file.bytes);
356
+ files.set('backup_label', encoder.encode(backupLabel));
357
+ if (tablespaceMap !== null && tablespaceMap.length > 0) {
358
+ files.set('tablespace_map', encoder.encode(tablespaceMap));
359
+ }
360
+ return {
361
+ schema: 'oliphaunt-wasix-directory-snapshot-v1',
362
+ directories: [...directories].sort(compareDirectoryDepth),
363
+ files: [...files].map(([path, contents]) => ({ path, bytes: contents })).sort(compareFile),
364
+ };
365
+ }
366
+ /** @internal Strict ustar writer shared by backup and conformance tests. */
367
+ export function encodePhysicalArchive(snapshot) {
368
+ const chunks = [];
369
+ appendTarEntry(chunks, 'pgdata/', new Uint8Array(), '5');
370
+ for (const path of snapshot.directories) {
371
+ appendTarEntry(chunks, `pgdata/${path}/`, new Uint8Array(), '5');
372
+ }
373
+ for (const { path, bytes } of snapshot.files) {
374
+ appendTarEntry(chunks, `pgdata/${path}`, bytes, '0');
375
+ }
376
+ appendTarEntry(chunks, ARCHIVE_MANIFEST_PATH, encoder.encode(ARCHIVE_MANIFEST), '0');
377
+ chunks.push(new Uint8Array(TAR_BLOCK_BYTES * 2));
378
+ return concatenate(chunks);
379
+ }
380
+ function appendTarEntry(chunks, path, contents, type) {
381
+ if (splitUstarPath(path) === undefined) {
382
+ throw new Error(`physical archive path cannot be represented by ustar: ${path}`);
383
+ }
384
+ appendRawTarEntry(chunks, path, contents, type);
385
+ }
386
+ function appendRawTarEntry(chunks, path, contents, type) {
387
+ const header = new Uint8Array(TAR_BLOCK_BYTES);
388
+ const split = splitUstarPath(path);
389
+ if (split === undefined)
390
+ throw new Error(`tar path cannot be represented: ${path}`);
391
+ writeAscii(header, 0, 100, split.name);
392
+ writeOctal(header, 100, 8, type === '5' ? 0o700 : 0o600);
393
+ writeOctal(header, 108, 8, 0);
394
+ writeOctal(header, 116, 8, 0);
395
+ writeOctal(header, 124, 12, contents.length);
396
+ writeOctal(header, 136, 12, 0);
397
+ header.fill(0x20, 148, 156);
398
+ header[156] = type.charCodeAt(0);
399
+ writeAscii(header, 257, 6, 'ustar\0');
400
+ writeAscii(header, 263, 2, '00');
401
+ writeAscii(header, 345, 155, split.prefix);
402
+ const checksum = header.reduce((sum, byte) => sum + byte, 0);
403
+ writeAscii(header, 148, 8, `${checksum.toString(8).padStart(6, '0')}\0 `);
404
+ chunks.push(header, contents.slice());
405
+ const padding = (TAR_BLOCK_BYTES - (contents.length % TAR_BLOCK_BYTES)) % TAR_BLOCK_BYTES;
406
+ if (padding > 0)
407
+ chunks.push(new Uint8Array(padding));
408
+ }
409
+ function splitUstarPath(path) {
410
+ if (encoder.encode(path).length <= 100)
411
+ return { name: path, prefix: '' };
412
+ for (let slash = path.lastIndexOf('/'); slash > 0; slash = path.lastIndexOf('/', slash - 1)) {
413
+ const prefix = path.slice(0, slash);
414
+ const name = path.slice(slash + 1);
415
+ if (encoder.encode(prefix).length <= 155 && encoder.encode(name).length <= 100) {
416
+ return { name, prefix };
417
+ }
418
+ }
419
+ return undefined;
420
+ }
421
+ function validateTarFraming(bytes) {
422
+ if (bytes.length < TAR_BLOCK_BYTES * 2 || bytes.length % TAR_BLOCK_BYTES !== 0) {
423
+ throw corruptArchive('has invalid tar framing');
424
+ }
425
+ let offset = 0;
426
+ while (offset + TAR_BLOCK_BYTES <= bytes.length) {
427
+ const header = bytes.subarray(offset, offset + TAR_BLOCK_BYTES);
428
+ if (header.every((byte) => byte === 0)) {
429
+ if (bytes.length - offset < TAR_BLOCK_BYTES * 2 ||
430
+ bytes.subarray(offset).some((byte) => byte !== 0)) {
431
+ throw corruptArchive('has trailing data after its tar terminator');
432
+ }
433
+ return;
434
+ }
435
+ const expected = parseTarOctal(header.subarray(148, 156), 'checksum');
436
+ let actual = 0;
437
+ for (let index = 0; index < header.length; index += 1) {
438
+ actual += index >= 148 && index < 156 ? 0x20 : (header[index] ?? 0);
439
+ }
440
+ if (expected !== actual)
441
+ throw corruptArchive('contains an invalid tar checksum');
442
+ const mode = parseTarOctal(header.subarray(100, 108), 'mode');
443
+ parseTarOctal(header.subarray(108, 116), 'uid', true);
444
+ parseTarOctal(header.subarray(116, 124), 'gid', true);
445
+ const size = parseTarOctal(header.subarray(124, 136), 'size');
446
+ parseTarOctal(header.subarray(136, 148), 'mtime', true);
447
+ if (mode > 0o777) {
448
+ throw corruptArchive('contains unsupported tar permission bits');
449
+ }
450
+ const magic = decodeTarText(header.subarray(257, 263));
451
+ const version = decodeTarText(header.subarray(263, 265));
452
+ if (!((magic === 'ustar\0' && version === '00') || (magic === 'ustar ' && version === ' \0'))) {
453
+ throw corruptArchive('contains an unsupported tar header');
454
+ }
455
+ const type = String.fromCharCode(header[156] ?? 0).replace('\0', '') || '0';
456
+ if (type !== '0' && type !== '5') {
457
+ throw corruptArchive(`contains unsupported tar entry type ${JSON.stringify(type)}`);
458
+ }
459
+ if (decodeTarText(header.subarray(157, 257)).replace(/\0.*$/s, '').length > 0) {
460
+ throw corruptArchive('contains a tar link target');
461
+ }
462
+ if (type === '5' && size !== 0) {
463
+ throw corruptArchive('contains a directory entry with data');
464
+ }
465
+ offset += TAR_BLOCK_BYTES + Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES;
466
+ if (offset > bytes.length)
467
+ throw corruptArchive('ends in the middle of an entry');
468
+ }
469
+ throw corruptArchive('is missing a tar terminator');
470
+ }
471
+ function decodeTarText(bytes) {
472
+ try {
473
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
474
+ }
475
+ catch (error) {
476
+ throw corruptArchive('contains non-UTF-8 tar metadata', error);
477
+ }
478
+ }
479
+ function parseTarOctal(bytes, label, allowEmpty = false) {
480
+ const text = new TextDecoder().decode(bytes).replace(/^[\0 ]+|[\0 ]+$/g, '');
481
+ if (text.length === 0 && allowEmpty)
482
+ return 0;
483
+ if (!/^[0-7]+$/.test(text))
484
+ throw corruptArchive(`contains an invalid tar ${label}`);
485
+ const value = Number.parseInt(text, 8);
486
+ if (!Number.isSafeInteger(value))
487
+ throw corruptArchive(`contains an oversized tar ${label}`);
488
+ return value;
489
+ }
490
+ function writeOctal(target, offset, width, value) {
491
+ const text = `${value.toString(8).padStart(width - 1, '0')}\0`;
492
+ if (text.length !== width)
493
+ throw new Error('tar numeric field overflow');
494
+ writeAscii(target, offset, width, text);
495
+ }
496
+ function writeAscii(target, offset, width, value) {
497
+ const bytes = encoder.encode(value);
498
+ if (bytes.length > width)
499
+ throw new Error(`tar field is too long: ${value}`);
500
+ target.set(bytes, offset);
501
+ }
502
+ function concatenate(chunks) {
503
+ const length = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
504
+ const output = new Uint8Array(length);
505
+ let offset = 0;
506
+ for (const chunk of chunks) {
507
+ output.set(chunk, offset);
508
+ offset += chunk.length;
509
+ }
510
+ return output;
511
+ }
512
+ function pathWithin(path, root) {
513
+ return path === root || path.startsWith(`${root}/`);
514
+ }
515
+ function compareDirectoryDepth(left, right) {
516
+ return left.split('/').length - right.split('/').length || left.localeCompare(right);
517
+ }
518
+ function compareFile(left, right) {
519
+ return left.path.localeCompare(right.path);
520
+ }
521
+ function corruptArchive(detail, cause) {
522
+ return new WasixStorageError(`physical archive ${detail}`, {
523
+ code: 'corrupt',
524
+ commitState: 'unchanged',
525
+ ...(cause === undefined ? {} : { cause }),
526
+ });
527
+ }
@@ -0,0 +1 @@
1
+ export * from '@oliphaunt/js-core/protocol';
@@ -0,0 +1 @@
1
+ export * from '@oliphaunt/js-core/protocol';
@@ -0,0 +1,4 @@
1
+ export { type WasixStorageCommitState, WasixStorageError, type WasixStorageErrorCode, type WasixStoragePhase, } from './errors.js';
2
+ export { array, binary, json, postgresOids, text, typedNull, type BinaryQueryParameter, type CommandResult, type DescribeResult, type EncodedQueryParameter, type ExecResult, type InferQueryRow, type NullQueryParameter, type ParameterOptions, PostgresError, type PostgresErrorField, type PostgresNotice, type QueryArrayRow, type QueryBinaryInput, type QueryDecoderMap, type QueryField, type QueryFormat, type QueryObjectRow, type QueryOptions, type QueryParam, type QueryParameterEncoder, type QueryResult, type QueryRowMode, type QueryValue, type QueryValueDecoder, type RawQueryResult, type RawQueryRow, type TextQueryParameter, type TransactionStatus, } from './query.js';
3
+ export { memory, type PersistentWasixStorage, type WasixStorage, } from './storage.js';
4
+ export type { BinaryInput, OliphauntClient, OliphauntDatabase, OliphauntTransaction, OpenConfig, WasixAssetSource, WasixExtensionDescriptor, } from './types.js';
package/lib/public.js ADDED
@@ -0,0 +1,3 @@
1
+ export { WasixStorageError, } from './errors.js';
2
+ export { array, binary, json, postgresOids, text, typedNull, PostgresError, } from './query.js';
3
+ export { memory, } from './storage.js';
package/lib/query.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from '@oliphaunt/js-core/query';
package/lib/query.js ADDED
@@ -0,0 +1 @@
1
+ export * from '@oliphaunt/js-core/query';