@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,528 @@
|
|
|
1
|
+
import { closeWasixByteChannel, readWasixByteChannelSync, writeWasixByteChannelSync, } from './byte-channel.js';
|
|
2
|
+
const SSL_REQUEST = 80_877_103;
|
|
3
|
+
const GSSENC_REQUEST = 80_877_104;
|
|
4
|
+
const CANCEL_REQUEST = 80_877_102;
|
|
5
|
+
const PROTOCOL_3 = 196_608;
|
|
6
|
+
const MAX_FRONTEND_MESSAGE_BYTES = 128 * 1024 * 1024;
|
|
7
|
+
const POSTGRES_IDENTIFIER_BYTES = 63;
|
|
8
|
+
/** @internal Connect same-realm pg_dump to the already stepped PostgreSQL backend. */
|
|
9
|
+
export class SynchronousPgDumpConnection {
|
|
10
|
+
#executor;
|
|
11
|
+
#frontend = new FrontendFrameReader();
|
|
12
|
+
#backend = new ByteChunkQueue();
|
|
13
|
+
#protocolStarted = false;
|
|
14
|
+
#started = false;
|
|
15
|
+
#terminated = false;
|
|
16
|
+
constructor(executor) {
|
|
17
|
+
this.#executor = executor;
|
|
18
|
+
}
|
|
19
|
+
get protocolStarted() {
|
|
20
|
+
return this.#protocolStarted;
|
|
21
|
+
}
|
|
22
|
+
write(input) {
|
|
23
|
+
this.#protocolStarted = true;
|
|
24
|
+
if (this.#terminated) {
|
|
25
|
+
throw new Error('pg_dump wrote after terminating its PostgreSQL connection');
|
|
26
|
+
}
|
|
27
|
+
// The Wasmer host lends this callback a view into its guest memory only
|
|
28
|
+
// for the duration of the call. Own the bytes before the frame reader can
|
|
29
|
+
// retain a fragmented frontend message across writes.
|
|
30
|
+
this.#frontend.append(input.slice());
|
|
31
|
+
for (;;) {
|
|
32
|
+
const frame = this.#frontend.shift();
|
|
33
|
+
if (frame === undefined)
|
|
34
|
+
return;
|
|
35
|
+
switch (classifyFrontendFrame(frame)) {
|
|
36
|
+
case 'ssl':
|
|
37
|
+
this.#backend.push(Uint8Array.of('N'.charCodeAt(0)));
|
|
38
|
+
break;
|
|
39
|
+
case 'cancel':
|
|
40
|
+
throw new Error('pg_dump cancellation is not supported');
|
|
41
|
+
case 'terminate':
|
|
42
|
+
this.#terminated = true;
|
|
43
|
+
return;
|
|
44
|
+
case 'startup':
|
|
45
|
+
if (this.#started)
|
|
46
|
+
throw new Error('duplicate PostgreSQL startup packet');
|
|
47
|
+
assertStartupIdentity(frame, this.#executor.startupIdentity);
|
|
48
|
+
this.#started = true;
|
|
49
|
+
this.#backend.push(this.#executor.startupResponse);
|
|
50
|
+
break;
|
|
51
|
+
case 'protocol':
|
|
52
|
+
if (!this.#started)
|
|
53
|
+
throw new Error('PostgreSQL protocol message preceded startup');
|
|
54
|
+
this.#executor.exec(frame, (chunk) => this.#backend.push(chunk));
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
read(maximumBytes) {
|
|
60
|
+
this.#protocolStarted = true;
|
|
61
|
+
const output = this.#backend.read(maximumBytes);
|
|
62
|
+
if (output.length === 0 && !this.#terminated) {
|
|
63
|
+
throw new Error('pg_dump attempted to read before PostgreSQL produced a response');
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
finish() {
|
|
68
|
+
this.#frontend.finish();
|
|
69
|
+
if (!this.#backend.empty) {
|
|
70
|
+
throw new Error('pg_dump left unread PostgreSQL response bytes');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Owned backend chunks with O(1) reads and occasional bounded compaction. */
|
|
75
|
+
class ByteChunkQueue {
|
|
76
|
+
#chunks = [];
|
|
77
|
+
#head = 0;
|
|
78
|
+
#offset = 0;
|
|
79
|
+
get empty() {
|
|
80
|
+
return this.#head === this.#chunks.length;
|
|
81
|
+
}
|
|
82
|
+
push(chunk) {
|
|
83
|
+
if (chunk.length !== 0)
|
|
84
|
+
this.#chunks.push(chunk);
|
|
85
|
+
}
|
|
86
|
+
read(maximumBytes) {
|
|
87
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes <= 0) {
|
|
88
|
+
throw new TypeError('maximum backend byte count must be a positive integer');
|
|
89
|
+
}
|
|
90
|
+
const chunk = this.#chunks[this.#head];
|
|
91
|
+
if (chunk === undefined)
|
|
92
|
+
return new Uint8Array();
|
|
93
|
+
const end = Math.min(chunk.length, this.#offset + maximumBytes);
|
|
94
|
+
const output = chunk.subarray(this.#offset, end);
|
|
95
|
+
this.#offset = end;
|
|
96
|
+
if (end === chunk.length) {
|
|
97
|
+
this.#head += 1;
|
|
98
|
+
this.#offset = 0;
|
|
99
|
+
if (this.#head === this.#chunks.length) {
|
|
100
|
+
this.#chunks = [];
|
|
101
|
+
this.#head = 0;
|
|
102
|
+
}
|
|
103
|
+
else if (this.#head > 256 && this.#head * 2 >= this.#chunks.length) {
|
|
104
|
+
this.#chunks = this.#chunks.slice(this.#head);
|
|
105
|
+
this.#head = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return output;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** @internal Serve one complete frontend connection over bounded byte channels. */
|
|
112
|
+
export async function serveWasixProtocolConnection(connection, executor) {
|
|
113
|
+
const reader = new FrontendFrameReader();
|
|
114
|
+
let startupComplete = false;
|
|
115
|
+
let transactionStatus;
|
|
116
|
+
try {
|
|
117
|
+
for (;;) {
|
|
118
|
+
const frame = reader.shift();
|
|
119
|
+
if (frame === undefined) {
|
|
120
|
+
const input = readWasixByteChannelSync(connection.frontend);
|
|
121
|
+
if (input.length === 0) {
|
|
122
|
+
reader.finish();
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
reader.append(input);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const classification = classifyFrontendFrame(frame);
|
|
129
|
+
switch (classification) {
|
|
130
|
+
case 'ssl':
|
|
131
|
+
writeWasixByteChannelSync(connection.backend, Uint8Array.of('N'.charCodeAt(0)));
|
|
132
|
+
break;
|
|
133
|
+
case 'cancel':
|
|
134
|
+
case 'terminate':
|
|
135
|
+
return;
|
|
136
|
+
case 'startup':
|
|
137
|
+
if (startupComplete)
|
|
138
|
+
throw new Error('duplicate PostgreSQL startup packet');
|
|
139
|
+
if (executor.startupIdentity !== undefined) {
|
|
140
|
+
assertStartupIdentity(frame, executor.startupIdentity);
|
|
141
|
+
}
|
|
142
|
+
startupComplete = true;
|
|
143
|
+
writeWasixByteChannelSync(connection.backend, executor.startupResponse);
|
|
144
|
+
transactionStatus = 'I'.charCodeAt(0);
|
|
145
|
+
break;
|
|
146
|
+
case 'protocol': {
|
|
147
|
+
if (!startupComplete)
|
|
148
|
+
throw new Error('PostgreSQL protocol message preceded startup');
|
|
149
|
+
const response = new BackendResponseGate();
|
|
150
|
+
executor.execDuplex(frame, (maximumBytes) => reader.readFrameChunk(maximumBytes, () => readWasixByteChannelSync(connection.frontend)), (chunk) => response.push(chunk, (output) => writeWasixByteChannelSync(connection.backend, output)));
|
|
151
|
+
reader.finishPull();
|
|
152
|
+
const ready = response.finish();
|
|
153
|
+
if (ready !== undefined) {
|
|
154
|
+
transactionStatus = ready.status;
|
|
155
|
+
if (ready.status === 'I'.charCodeAt(0))
|
|
156
|
+
await executor.publishIdle();
|
|
157
|
+
writeWasixByteChannelSync(connection.backend, ready.frame);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
try {
|
|
166
|
+
if (transactionStatus !== undefined && transactionStatus !== 'I'.charCodeAt(0)) {
|
|
167
|
+
await executor.rollback();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
closeWasixByteChannel(connection.backend);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function classifyFrontendFrame(frame) {
|
|
176
|
+
if (frame.length === 0)
|
|
177
|
+
throw new Error('empty PostgreSQL frontend message');
|
|
178
|
+
if (frame[0] !== 0) {
|
|
179
|
+
return frame[0] === 'X'.charCodeAt(0) ? 'terminate' : 'protocol';
|
|
180
|
+
}
|
|
181
|
+
if (frame.length < 8)
|
|
182
|
+
throw new Error('PostgreSQL startup packet is too short');
|
|
183
|
+
const code = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getInt32(4);
|
|
184
|
+
if (code === SSL_REQUEST || code === GSSENC_REQUEST)
|
|
185
|
+
return 'ssl';
|
|
186
|
+
if (code === CANCEL_REQUEST)
|
|
187
|
+
return 'cancel';
|
|
188
|
+
if (code === PROTOCOL_3)
|
|
189
|
+
return 'startup';
|
|
190
|
+
throw new Error(`unsupported PostgreSQL startup packet code ${code}`);
|
|
191
|
+
}
|
|
192
|
+
/** @internal Parse the identity selected by a PostgreSQL v3 startup packet. */
|
|
193
|
+
export function parseStartupIdentity(frame) {
|
|
194
|
+
if (classifyFrontendFrame(frame) !== 'startup') {
|
|
195
|
+
throw new Error('PostgreSQL startup identity requires a protocol v3 startup packet');
|
|
196
|
+
}
|
|
197
|
+
const declaredLength = new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getInt32(0);
|
|
198
|
+
if (declaredLength !== frame.length) {
|
|
199
|
+
throw new Error(`PostgreSQL startup packet length ${declaredLength} does not match ${frame.length} bytes`);
|
|
200
|
+
}
|
|
201
|
+
const parameters = new Map();
|
|
202
|
+
let offset = 8;
|
|
203
|
+
for (;;) {
|
|
204
|
+
if (offset >= frame.length) {
|
|
205
|
+
throw new Error('PostgreSQL startup parameters are missing their final NUL byte');
|
|
206
|
+
}
|
|
207
|
+
const nameEnd = frame.indexOf(0, offset);
|
|
208
|
+
if (nameEnd < 0) {
|
|
209
|
+
throw new Error('PostgreSQL startup parameter name is missing its NUL terminator');
|
|
210
|
+
}
|
|
211
|
+
if (nameEnd === offset) {
|
|
212
|
+
if (nameEnd !== frame.length - 1) {
|
|
213
|
+
throw new Error('PostgreSQL startup packet contains bytes after its final NUL byte');
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
const valueOffset = nameEnd + 1;
|
|
218
|
+
const valueEnd = frame.indexOf(0, valueOffset);
|
|
219
|
+
if (valueEnd < 0) {
|
|
220
|
+
throw new Error('PostgreSQL startup parameter value is missing its NUL terminator');
|
|
221
|
+
}
|
|
222
|
+
const name = decodeStartupText(frame.subarray(offset, nameEnd), 'parameter name');
|
|
223
|
+
const value = decodeStartupText(frame.subarray(valueOffset, valueEnd), `parameter ${name}`);
|
|
224
|
+
// PostgreSQL's ProcessStartupPacket applies parameters in packet order,
|
|
225
|
+
// so a repeated key deliberately replaces its earlier value.
|
|
226
|
+
parameters.set(name, value);
|
|
227
|
+
offset = valueEnd + 1;
|
|
228
|
+
}
|
|
229
|
+
const username = parameters.get('user');
|
|
230
|
+
if (username === undefined || username.length === 0) {
|
|
231
|
+
throw new Error('PostgreSQL startup packet requires a non-empty user parameter');
|
|
232
|
+
}
|
|
233
|
+
const normalizedUsername = truncatePostgresIdentifier(username);
|
|
234
|
+
const database = parameters.get('database');
|
|
235
|
+
return {
|
|
236
|
+
username: normalizedUsername,
|
|
237
|
+
database: database === undefined || database.length === 0
|
|
238
|
+
? normalizedUsername
|
|
239
|
+
: truncatePostgresIdentifier(database),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function assertStartupIdentity(frame, expected) {
|
|
243
|
+
const actual = parseStartupIdentity(frame);
|
|
244
|
+
const expectedUsername = truncatePostgresIdentifier(expected.username);
|
|
245
|
+
const expectedDatabase = expected.database.length === 0
|
|
246
|
+
? expectedUsername
|
|
247
|
+
: truncatePostgresIdentifier(expected.database);
|
|
248
|
+
if (actual.username !== expectedUsername) {
|
|
249
|
+
throw new Error(`PostgreSQL startup user ${JSON.stringify(actual.username)} does not match endpoint user ${JSON.stringify(expectedUsername)}`);
|
|
250
|
+
}
|
|
251
|
+
if (actual.database !== expectedDatabase) {
|
|
252
|
+
throw new Error(`PostgreSQL startup database ${JSON.stringify(actual.database)} does not match endpoint database ${JSON.stringify(expectedDatabase)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function truncatePostgresIdentifier(value) {
|
|
256
|
+
const encoder = new TextEncoder();
|
|
257
|
+
let bytes = 0;
|
|
258
|
+
let result = '';
|
|
259
|
+
for (const character of value) {
|
|
260
|
+
const characterBytes = encoder.encode(character).length;
|
|
261
|
+
if (bytes + characterBytes > POSTGRES_IDENTIFIER_BYTES)
|
|
262
|
+
break;
|
|
263
|
+
result += character;
|
|
264
|
+
bytes += characterBytes;
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function decodeStartupText(bytes, label) {
|
|
269
|
+
try {
|
|
270
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
throw new Error(`PostgreSQL startup ${label} is not valid UTF-8`, { cause: error });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export class FrontendFrameReader {
|
|
277
|
+
#chunks = [];
|
|
278
|
+
#header = new Uint8Array(5);
|
|
279
|
+
#headIndex = 0;
|
|
280
|
+
#headOffset = 0;
|
|
281
|
+
#bufferedBytes = 0;
|
|
282
|
+
#frameLength;
|
|
283
|
+
#pullRemaining;
|
|
284
|
+
append(input) {
|
|
285
|
+
if (input.length === 0)
|
|
286
|
+
return;
|
|
287
|
+
this.#chunks.push(input);
|
|
288
|
+
this.#bufferedBytes += input.length;
|
|
289
|
+
}
|
|
290
|
+
shift() {
|
|
291
|
+
if (this.#pullRemaining !== undefined) {
|
|
292
|
+
throw new Error('PostgreSQL frontend pull frame is still active');
|
|
293
|
+
}
|
|
294
|
+
const length = this.#nextFrameLength();
|
|
295
|
+
if (length === undefined || this.#bufferedBytes < length)
|
|
296
|
+
return undefined;
|
|
297
|
+
this.#frameLength = undefined;
|
|
298
|
+
return this.#take(length);
|
|
299
|
+
}
|
|
300
|
+
/** Stream one frontend frame at a time to COPY's synchronous pull transport. */
|
|
301
|
+
readFrameChunk(maximumBytes, read) {
|
|
302
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes <= 0) {
|
|
303
|
+
throw new TypeError('maximum frontend byte count must be a positive integer');
|
|
304
|
+
}
|
|
305
|
+
while (this.#pullRemaining === undefined) {
|
|
306
|
+
const length = this.#nextFrameLength();
|
|
307
|
+
if (length !== undefined) {
|
|
308
|
+
this.#pullRemaining = length;
|
|
309
|
+
this.#frameLength = undefined;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
const input = read();
|
|
313
|
+
if (input.length === 0) {
|
|
314
|
+
this.finish();
|
|
315
|
+
return input;
|
|
316
|
+
}
|
|
317
|
+
this.append(input);
|
|
318
|
+
}
|
|
319
|
+
while (this.#bufferedBytes === 0) {
|
|
320
|
+
const input = read();
|
|
321
|
+
if (input.length === 0) {
|
|
322
|
+
this.finish();
|
|
323
|
+
return input;
|
|
324
|
+
}
|
|
325
|
+
this.append(input);
|
|
326
|
+
}
|
|
327
|
+
const remaining = this.#pullRemaining;
|
|
328
|
+
if (remaining === undefined)
|
|
329
|
+
return new Uint8Array();
|
|
330
|
+
const output = this.#take(Math.min(maximumBytes, remaining, this.#bufferedBytes), false);
|
|
331
|
+
const nextRemaining = remaining - output.length;
|
|
332
|
+
this.#pullRemaining = nextRemaining === 0 ? undefined : nextRemaining;
|
|
333
|
+
return output;
|
|
334
|
+
}
|
|
335
|
+
finishPull() {
|
|
336
|
+
if (this.#pullRemaining !== undefined) {
|
|
337
|
+
throw new Error('PostgreSQL COPY input stopped inside a frontend message');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
finish() {
|
|
341
|
+
this.finishPull();
|
|
342
|
+
if (this.#bufferedBytes !== 0) {
|
|
343
|
+
throw new Error('PostgreSQL frontend connection ended inside a message');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
#nextFrameLength() {
|
|
347
|
+
if (this.#frameLength !== undefined)
|
|
348
|
+
return this.#frameLength;
|
|
349
|
+
const required = this.#peekByte() === 0 ? 4 : 5;
|
|
350
|
+
if (this.#bufferedBytes < required)
|
|
351
|
+
return undefined;
|
|
352
|
+
this.#copyPrefix(this.#header, required);
|
|
353
|
+
const length = frontendFrameLengthFromHeader(this.#header.subarray(0, required));
|
|
354
|
+
if (length !== undefined)
|
|
355
|
+
this.#frameLength = length;
|
|
356
|
+
return length;
|
|
357
|
+
}
|
|
358
|
+
#peekByte() {
|
|
359
|
+
return this.#chunks[this.#headIndex]?.[this.#headOffset];
|
|
360
|
+
}
|
|
361
|
+
#copyPrefix(output, length) {
|
|
362
|
+
let outputOffset = 0;
|
|
363
|
+
let chunkIndex = this.#headIndex;
|
|
364
|
+
let chunkOffset = this.#headOffset;
|
|
365
|
+
while (outputOffset < length) {
|
|
366
|
+
const chunk = this.#chunks[chunkIndex];
|
|
367
|
+
if (chunk === undefined)
|
|
368
|
+
throw new Error('PostgreSQL frontend buffer is inconsistent');
|
|
369
|
+
const copied = Math.min(length - outputOffset, chunk.length - chunkOffset);
|
|
370
|
+
output.set(chunk.subarray(chunkOffset, chunkOffset + copied), outputOffset);
|
|
371
|
+
outputOffset += copied;
|
|
372
|
+
chunkIndex += 1;
|
|
373
|
+
chunkOffset = 0;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
#take(maximumBytes, coalesce = true) {
|
|
377
|
+
const head = this.#chunks[this.#headIndex];
|
|
378
|
+
if (head === undefined || maximumBytes <= 0 || maximumBytes > this.#bufferedBytes) {
|
|
379
|
+
throw new Error('PostgreSQL frontend buffer is inconsistent');
|
|
380
|
+
}
|
|
381
|
+
const available = head.length - this.#headOffset;
|
|
382
|
+
const length = coalesce ? maximumBytes : Math.min(maximumBytes, available);
|
|
383
|
+
if (length <= available) {
|
|
384
|
+
const output = head.subarray(this.#headOffset, this.#headOffset + length);
|
|
385
|
+
this.#consume(length);
|
|
386
|
+
return output;
|
|
387
|
+
}
|
|
388
|
+
const output = new Uint8Array(length);
|
|
389
|
+
let offset = 0;
|
|
390
|
+
while (offset < length) {
|
|
391
|
+
const chunk = this.#chunks[this.#headIndex];
|
|
392
|
+
if (chunk === undefined)
|
|
393
|
+
throw new Error('PostgreSQL frontend buffer is inconsistent');
|
|
394
|
+
const copied = Math.min(length - offset, chunk.length - this.#headOffset);
|
|
395
|
+
output.set(chunk.subarray(this.#headOffset, this.#headOffset + copied), offset);
|
|
396
|
+
this.#consume(copied);
|
|
397
|
+
offset += copied;
|
|
398
|
+
}
|
|
399
|
+
return output;
|
|
400
|
+
}
|
|
401
|
+
#consume(length) {
|
|
402
|
+
const head = this.#chunks[this.#headIndex];
|
|
403
|
+
if (head === undefined || length > head.length - this.#headOffset) {
|
|
404
|
+
throw new Error('PostgreSQL frontend buffer is inconsistent');
|
|
405
|
+
}
|
|
406
|
+
this.#headOffset += length;
|
|
407
|
+
this.#bufferedBytes -= length;
|
|
408
|
+
if (this.#headOffset === head.length) {
|
|
409
|
+
this.#headIndex += 1;
|
|
410
|
+
this.#headOffset = 0;
|
|
411
|
+
if (this.#headIndex === this.#chunks.length) {
|
|
412
|
+
this.#chunks.length = 0;
|
|
413
|
+
this.#headIndex = 0;
|
|
414
|
+
}
|
|
415
|
+
else if (this.#headIndex >= 1_024 && this.#headIndex * 2 >= this.#chunks.length) {
|
|
416
|
+
this.#chunks.splice(0, this.#headIndex);
|
|
417
|
+
this.#headIndex = 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function frontendFrameLengthFromHeader(input) {
|
|
423
|
+
if (input.length < 4)
|
|
424
|
+
return undefined;
|
|
425
|
+
const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
426
|
+
if (input[0] === 0) {
|
|
427
|
+
const length = view.getInt32(0);
|
|
428
|
+
validateFrontendLength(length, 8, 'startup/control packet');
|
|
429
|
+
return length;
|
|
430
|
+
}
|
|
431
|
+
if (input.length < 5)
|
|
432
|
+
return undefined;
|
|
433
|
+
const bodyLength = view.getInt32(1);
|
|
434
|
+
validateFrontendLength(bodyLength, 4, 'frontend message');
|
|
435
|
+
const length = bodyLength + 1;
|
|
436
|
+
if (length > MAX_FRONTEND_MESSAGE_BYTES) {
|
|
437
|
+
throw new Error(`PostgreSQL frontend message length ${length} exceeds limit`);
|
|
438
|
+
}
|
|
439
|
+
return length;
|
|
440
|
+
}
|
|
441
|
+
function validateFrontendLength(length, minimum, label) {
|
|
442
|
+
if (length < minimum)
|
|
443
|
+
throw new Error(`invalid PostgreSQL ${label} length ${length}`);
|
|
444
|
+
if (length > MAX_FRONTEND_MESSAGE_BYTES) {
|
|
445
|
+
throw new Error(`PostgreSQL ${label} length ${length} exceeds limit`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** @internal Hold only ReadyForQuery while streaming every preceding backend message. */
|
|
449
|
+
export class BackendResponseGate {
|
|
450
|
+
#header = new Uint8Array(5);
|
|
451
|
+
#headerLength = 0;
|
|
452
|
+
#tag = 0;
|
|
453
|
+
#bodyRemaining = 0;
|
|
454
|
+
#ready;
|
|
455
|
+
push(input, write) {
|
|
456
|
+
if (this.#ready !== undefined && this.#bodyRemaining === 0) {
|
|
457
|
+
throw new Error('PostgreSQL backend returned bytes after ReadyForQuery');
|
|
458
|
+
}
|
|
459
|
+
// One PostgreSQL host callback may contain many protocol messages. Keep
|
|
460
|
+
// their non-Ready bytes in one bounded write so the shared channel pays
|
|
461
|
+
// for one wakeup, while still flushing before this callback returns. The
|
|
462
|
+
// extra five bytes cover a header begun by the preceding callback.
|
|
463
|
+
const output = new Uint8Array(input.length + 5);
|
|
464
|
+
let outputLength = 0;
|
|
465
|
+
const append = (chunk) => {
|
|
466
|
+
output.set(chunk, outputLength);
|
|
467
|
+
outputLength += chunk.length;
|
|
468
|
+
};
|
|
469
|
+
let offset = 0;
|
|
470
|
+
while (offset < input.length) {
|
|
471
|
+
if (this.#bodyRemaining === 0) {
|
|
472
|
+
if (this.#ready !== undefined) {
|
|
473
|
+
throw new Error('PostgreSQL backend returned bytes after ReadyForQuery');
|
|
474
|
+
}
|
|
475
|
+
const copied = Math.min(5 - this.#headerLength, input.length - offset);
|
|
476
|
+
this.#header.set(input.subarray(offset, offset + copied), this.#headerLength);
|
|
477
|
+
this.#headerLength += copied;
|
|
478
|
+
offset += copied;
|
|
479
|
+
if (this.#headerLength < 5)
|
|
480
|
+
continue;
|
|
481
|
+
this.#tag = this.#header[0] ?? 0;
|
|
482
|
+
const length = new DataView(this.#header.buffer).getInt32(1);
|
|
483
|
+
if (length < 4)
|
|
484
|
+
throw new Error(`invalid PostgreSQL backend message length ${length}`);
|
|
485
|
+
this.#bodyRemaining = length - 4;
|
|
486
|
+
if (this.#tag === 'Z'.charCodeAt(0)) {
|
|
487
|
+
if (this.#bodyRemaining !== 1) {
|
|
488
|
+
throw new Error(`PostgreSQL ReadyForQuery body length ${this.#bodyRemaining}, expected 1`);
|
|
489
|
+
}
|
|
490
|
+
this.#ready = new Uint8Array(6);
|
|
491
|
+
this.#ready.set(this.#header);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
append(this.#header);
|
|
495
|
+
}
|
|
496
|
+
this.#headerLength = 0;
|
|
497
|
+
if (this.#bodyRemaining === 0)
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const copied = Math.min(this.#bodyRemaining, input.length - offset);
|
|
501
|
+
const body = input.subarray(offset, offset + copied);
|
|
502
|
+
if (this.#ready !== undefined) {
|
|
503
|
+
this.#ready.set(body, 6 - this.#bodyRemaining);
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
append(body);
|
|
507
|
+
}
|
|
508
|
+
this.#bodyRemaining -= copied;
|
|
509
|
+
offset += copied;
|
|
510
|
+
}
|
|
511
|
+
if (outputLength !== 0)
|
|
512
|
+
write(output.subarray(0, outputLength));
|
|
513
|
+
}
|
|
514
|
+
finish() {
|
|
515
|
+
if (this.#headerLength !== 0 || this.#bodyRemaining !== 0) {
|
|
516
|
+
throw new Error('PostgreSQL backend response ended inside a message');
|
|
517
|
+
}
|
|
518
|
+
if (this.#ready === undefined)
|
|
519
|
+
return undefined;
|
|
520
|
+
const status = this.#ready[5];
|
|
521
|
+
if (status !== 'I'.charCodeAt(0) &&
|
|
522
|
+
status !== 'T'.charCodeAt(0) &&
|
|
523
|
+
status !== 'E'.charCodeAt(0)) {
|
|
524
|
+
throw new Error(`PostgreSQL ReadyForQuery contained invalid transaction status ${status}`);
|
|
525
|
+
}
|
|
526
|
+
return { status, frame: this.#ready };
|
|
527
|
+
}
|
|
528
|
+
}
|
package/lib/pgwire.d.ts
ADDED
package/lib/pgwire.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { assertSuccessfulQueryResponse } from './query.js';
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
export function startupPacket(username, database) {
|
|
4
|
+
assertStartupValue('username', username);
|
|
5
|
+
assertStartupValue('database', database);
|
|
6
|
+
const body = [];
|
|
7
|
+
pushI32(body, 196_608);
|
|
8
|
+
pushCString(body, 'user');
|
|
9
|
+
pushCString(body, username);
|
|
10
|
+
pushCString(body, 'database');
|
|
11
|
+
pushCString(body, database);
|
|
12
|
+
pushCString(body, 'client_encoding');
|
|
13
|
+
pushCString(body, 'UTF8');
|
|
14
|
+
pushCString(body, 'DateStyle');
|
|
15
|
+
pushCString(body, 'ISO, MDY');
|
|
16
|
+
pushCString(body, 'TimeZone');
|
|
17
|
+
pushCString(body, 'UTC');
|
|
18
|
+
body.push(0);
|
|
19
|
+
const packet = [];
|
|
20
|
+
pushI32(packet, body.length + 4);
|
|
21
|
+
packet.push(...body);
|
|
22
|
+
return Uint8Array.from(packet);
|
|
23
|
+
}
|
|
24
|
+
/** Validate the first PostgreSQL startup exchange before settling the host loop. */
|
|
25
|
+
export function assertSuccessfulStartupResponse(response) {
|
|
26
|
+
let offset = 0;
|
|
27
|
+
let sawAuthenticationOk = false;
|
|
28
|
+
let sawReady = false;
|
|
29
|
+
while (offset < response.length) {
|
|
30
|
+
if (response.length - offset < 5) {
|
|
31
|
+
throw new Error('PostgreSQL startup response ended inside a backend message header');
|
|
32
|
+
}
|
|
33
|
+
const tag = response[offset] ?? 0;
|
|
34
|
+
const bodyLength = readI32(response, offset + 1);
|
|
35
|
+
if (bodyLength < 4) {
|
|
36
|
+
throw new Error(`invalid PostgreSQL startup message length ${bodyLength}`);
|
|
37
|
+
}
|
|
38
|
+
const messageLength = bodyLength + 1;
|
|
39
|
+
if (offset + messageLength > response.length) {
|
|
40
|
+
throw new Error('PostgreSQL startup response ended inside a backend message');
|
|
41
|
+
}
|
|
42
|
+
if (tag === 'E'.charCodeAt(0)) {
|
|
43
|
+
// Reuse the shared pgwire error decoder so startup and query failures
|
|
44
|
+
// expose the same PostgresError/SQLSTATE contract.
|
|
45
|
+
const ready = Uint8Array.of('Z'.charCodeAt(0), 0, 0, 0, 5, 'I'.charCodeAt(0));
|
|
46
|
+
const withReady = concatenate([response.slice(offset, offset + messageLength), ready], messageLength + ready.length);
|
|
47
|
+
// Imported lazily would obscure the synchronous error contract. Keep
|
|
48
|
+
// the parser dependency at module scope through this helper below.
|
|
49
|
+
throwStartupPostgresError(withReady);
|
|
50
|
+
}
|
|
51
|
+
if (tag === 'R'.charCodeAt(0)) {
|
|
52
|
+
if (bodyLength !== 8 || readI32(response, offset + 5) !== 0) {
|
|
53
|
+
throw new Error('Oliphaunt WASIX supports only PostgreSQL AuthenticationOk startup');
|
|
54
|
+
}
|
|
55
|
+
sawAuthenticationOk = true;
|
|
56
|
+
}
|
|
57
|
+
if (tag === 'Z'.charCodeAt(0)) {
|
|
58
|
+
if (bodyLength !== 5) {
|
|
59
|
+
throw new Error('invalid PostgreSQL ReadyForQuery startup message');
|
|
60
|
+
}
|
|
61
|
+
sawReady = true;
|
|
62
|
+
if (offset + messageLength !== response.length) {
|
|
63
|
+
throw new Error('PostgreSQL startup returned bytes after ReadyForQuery');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
offset += messageLength;
|
|
67
|
+
}
|
|
68
|
+
if (!sawAuthenticationOk || !sawReady) {
|
|
69
|
+
throw new Error('PostgreSQL startup response omitted AuthenticationOk or ReadyForQuery');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function throwStartupPostgresError(response) {
|
|
73
|
+
assertSuccessfulQueryResponse(response);
|
|
74
|
+
throw new Error('PostgreSQL startup ErrorResponse was not rejected');
|
|
75
|
+
}
|
|
76
|
+
function assertStartupValue(label, value) {
|
|
77
|
+
if (value.includes('\0')) {
|
|
78
|
+
throw new Error(`${label} must not contain NUL bytes`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function pushCString(output, value) {
|
|
82
|
+
output.push(...encoder.encode(value), 0);
|
|
83
|
+
}
|
|
84
|
+
function pushI32(output, value) {
|
|
85
|
+
output.push((value >>> 24) & 0xff);
|
|
86
|
+
output.push((value >>> 16) & 0xff);
|
|
87
|
+
output.push((value >>> 8) & 0xff);
|
|
88
|
+
output.push(value & 0xff);
|
|
89
|
+
}
|
|
90
|
+
function readI32(bytes, offset) {
|
|
91
|
+
return ((((bytes[offset] ?? 0) << 24) |
|
|
92
|
+
((bytes[offset + 1] ?? 0) << 16) |
|
|
93
|
+
((bytes[offset + 2] ?? 0) << 8) |
|
|
94
|
+
(bytes[offset + 3] ?? 0)) >>>
|
|
95
|
+
0);
|
|
96
|
+
}
|
|
97
|
+
function concatenate(chunks, totalLength) {
|
|
98
|
+
const result = new Uint8Array(totalLength);
|
|
99
|
+
let offset = 0;
|
|
100
|
+
for (const chunk of chunks) {
|
|
101
|
+
result.set(chunk, offset);
|
|
102
|
+
offset += chunk.length;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { StorageDirectory } from './storage-provider.js';
|
|
2
|
+
import { type StoredSnapshot } from './storage-snapshot.js';
|
|
3
|
+
type ExecProtocol = (input: Uint8Array) => Promise<Uint8Array>;
|
|
4
|
+
/** @internal Identifies the only backup failure that makes the session unsafe to reuse. */
|
|
5
|
+
export declare class BackupModeExitUnconfirmedError extends Error {
|
|
6
|
+
constructor(message: string, cause: unknown);
|
|
7
|
+
}
|
|
8
|
+
/** Create one PostgreSQL online physical backup without restarting its session. */
|
|
9
|
+
export declare function createPhysicalArchive(exec: ExecProtocol, directory: StorageDirectory): Promise<Uint8Array>;
|
|
10
|
+
/** @internal Read the bulk backup tree without reading transient or pre-stop WAL contents. */
|
|
11
|
+
export declare function snapshotPhysicalBackupBulk(directory: StorageDirectory): Promise<StoredSnapshot>;
|
|
12
|
+
/** @internal Prove the online backup contains every required start-to-stop WAL segment. */
|
|
13
|
+
export declare function validateBackupWalRange(snapshot: StoredSnapshot, startName: string, stopName: string, segmentSize: number): void;
|
|
14
|
+
/** @internal Enumerate PostgreSQL's inclusive start-to-stop WAL range. */
|
|
15
|
+
export declare function requiredBackupWalNames(startName: string, stopName: string, segmentSize: number): string[];
|
|
16
|
+
/** Validate one Oliphaunt physical tar and return its provider-neutral PGDATA snapshot. */
|
|
17
|
+
export declare function decodePhysicalArchive(bytes: Uint8Array): StoredSnapshot;
|
|
18
|
+
/** @internal Apply PostgreSQL's standard online-backup transient-state exclusions. */
|
|
19
|
+
export declare function withoutPostStopState(snapshot: StoredSnapshot): StoredSnapshot;
|
|
20
|
+
/** @internal Replace the bulk-scan control file with the final pre-stop read. */
|
|
21
|
+
export declare function refreshBackupPgControl(snapshot: StoredSnapshot, pgControl: Uint8Array): StoredSnapshot;
|
|
22
|
+
/** @internal Merge only the proven required WAL files into the bulk PGDATA scan. */
|
|
23
|
+
export declare function mergeBackupSnapshots(beforeStop: StoredSnapshot, walFiles: readonly Readonly<{
|
|
24
|
+
path: string;
|
|
25
|
+
bytes: Uint8Array;
|
|
26
|
+
}>[], backupLabel: string, tablespaceMap: string | null): StoredSnapshot;
|
|
27
|
+
/** @internal Strict ustar writer shared by backup and conformance tests. */
|
|
28
|
+
export declare function encodePhysicalArchive(snapshot: StoredSnapshot): Uint8Array;
|
|
29
|
+
export {};
|