@volter/world-runtime 3.0.48 → 3.0.49
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/dist/src/pglite-host.mjs +58 -629
- package/package.json +4 -4
- package/src/pglite-host.mjs +58 -629
package/dist/src/pglite-host.mjs
CHANGED
|
@@ -10,13 +10,11 @@
|
|
|
10
10
|
// server starts listening only once the backend is ready, so a port that
|
|
11
11
|
// answers is a port that serves.
|
|
12
12
|
//
|
|
13
|
-
// PGlite
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// the other's writes. A connection that dies mid-transaction is rolled back
|
|
19
|
-
// before the lock is released.
|
|
13
|
+
// PGlite owns one backend session. Admit one authenticated connection only,
|
|
14
|
+
// keep it until disconnect cleanup finishes, and refuse concurrent sessions
|
|
15
|
+
// with FATAL 53300. The native/container backing supplies independent sessions.
|
|
16
|
+
// The backend performs Sync, ROLLBACK and DISCARD ALL before another client
|
|
17
|
+
// enters; no isolation, advisory-lock or prepared-name behavior is simulated.
|
|
20
18
|
//
|
|
21
19
|
// Extensions: every contrib extension PGlite 0.5.8 ships (CONTRIB below) plus
|
|
22
20
|
// pgvector (`vector`, from @electric-sql/pglite-pgvector, pinned to the
|
|
@@ -40,43 +38,11 @@
|
|
|
40
38
|
// multiple databases require the native or container backing.
|
|
41
39
|
// - No COPY … FROM STDIN: refused with 0A000 (it aborts PGlite's backend);
|
|
42
40
|
// COPY … TO STDOUT works.
|
|
43
|
-
// - ONE
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// sends a session past idle_in_transaction_session_timeout (25P03), its
|
|
49
|
-
// transaction rolled back (writes made through it are lost, as on Postgres),
|
|
50
|
-
// and the next client runs. This DEVIATES from Postgres, whose timeout is off
|
|
51
|
-
// by default and where an idle open transaction holds only its row locks:
|
|
52
|
-
// here it would hold the whole database, so a transaction that waits on
|
|
53
|
-
// something else (an HTTP call inside it) for longer than the limit while
|
|
54
|
-
// another client queues is cut. A READ-ONLY batch another connection sends
|
|
55
|
-
// while the holder sits idle in its transaction does not wait: it runs as a
|
|
56
|
-
// guest in the holder's session, inside a savepoint (see enter()), and sees
|
|
57
|
-
// the holder's uncommitted rows — Postgres would show it only committed ones.
|
|
58
|
-
// While a guest's batch runs, the holder waits for it (its idle clock
|
|
59
|
-
// stopped); a guest batch still open after GUEST_TURN_MS while anyone waits
|
|
60
|
-
// (a cursor that has not sent its Sync) is cancelled (57014), its savepoint
|
|
61
|
-
// undone;
|
|
62
|
-
// a batch that begins with a read and goes on to write is refused (0A000).
|
|
63
|
-
// A connection that closes while it waits
|
|
64
|
-
// for the lock gives it up unrun (rallly walk 2: pg-pool evicted an idle
|
|
65
|
-
// client — its Terminate queued behind another connection's transaction,
|
|
66
|
-
// ran once that ended, left no ReadyForQuery, and held the lock forever). All connections share the one
|
|
67
|
-
// backend session, so session state leaks between them and outlives the
|
|
68
|
-
// connection that set it: SET (search_path, timezone…), temp tables and
|
|
69
|
-
// SQL PREPAREd statements are visible to every other connection (a
|
|
70
|
-
// statement prepared over the protocol is the connection's own: a name
|
|
71
|
-
// another live connection holds is renamed for it, and its own SQL
|
|
72
|
-
// EXECUTE/DEALLOCATE of that name is renamed with it; scopeNames — except
|
|
73
|
-
// in CREATE TABLE … AS EXECUTE, a comment between the verb and the name,
|
|
74
|
-
// a Query holding a backslash, and a DEALLOCATE sent through Parse, which
|
|
75
|
-
// reach the name as the backend holds it); session
|
|
76
|
-
// advisory locks do NOT exclude (a second connection's
|
|
77
|
-
// pg_try_advisory_lock on a held key returns true); a NOTIFY is delivered
|
|
78
|
-
// to the connection that sent it, not to the one that ran LISTEN.
|
|
79
|
-
// CREATE ROLE succeeds but connections never run as that role.
|
|
41
|
+
// - ONE wire session, including idle connections. A second authenticated
|
|
42
|
+
// connection fails FATAL 53300. Concurrent pools and distinct backend
|
|
43
|
+
// identities require the native or container backing. Session settings,
|
|
44
|
+
// temp objects, prepared statements, transactions and locks are the backend's
|
|
45
|
+
// own and are reset before its session is reused.
|
|
80
46
|
//
|
|
81
47
|
// Database timestamps use the World clock: PGlite reads Date.now() for now(),
|
|
82
48
|
// CURRENT_TIMESTAMP, clock_timestamp() and transaction starts. Refresh the file
|
|
@@ -186,462 +152,14 @@ dbReady.catch((error) => {
|
|
|
186
152
|
process.exit(1);
|
|
187
153
|
});
|
|
188
154
|
|
|
189
|
-
// ----
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
/** Each connection's socket, whether a message of it is running, and since
|
|
198
|
-
* when it has been idle. */
|
|
199
|
-
const sessions = new Map();
|
|
200
|
-
// A GUEST batch: while the holder sits idle inside its transaction (its last
|
|
201
|
-
// ReadyForQuery 'T', no message of its own running, none half-sent), another
|
|
202
|
-
// connection's READ-ONLY batch runs in the holder's session, inside a savepoint
|
|
203
|
-
// released after it (rolled back to on an error, so the holder's transaction is
|
|
204
|
-
// never aborted by it), and is answered ReadyForQuery 'I' as its own session
|
|
205
|
-
// would be. An application that reads on a second pooled connection while its
|
|
206
|
-
// first holds a transaction open (twenty's sign-up: a TypeORM transaction that
|
|
207
|
-
// awaits a repository read on another connection) otherwise waits for a lock
|
|
208
|
-
// the transaction never gives up. The read sees the holder's uncommitted rows —
|
|
209
|
-
// a deviation from Postgres's isolation, stated in the limits above. Writes,
|
|
210
|
-
// and a guest's own BEGIN, wait for the lock as before.
|
|
211
|
-
let guestBatch = null;
|
|
212
|
-
let guestStartedAt = 0;
|
|
213
|
-
// How long a guest batch may hold its turn while the holder or a writer waits (a cursor that never sends its Sync
|
|
214
|
-
// would otherwise hold every connection up): past it the guest's batch is refused and the waiters run.
|
|
215
|
-
const GUEST_TURN_MS = 2000;
|
|
216
|
-
function wakeAll() {
|
|
217
|
-
for (const wake of waiters.splice(0)) wake();
|
|
218
|
-
}
|
|
219
|
-
/** How `token` may run its next message: 'own' (it holds the lock, and its turn is claimed here, before any other
|
|
220
|
-
* waiter wakes to look) or 'guest'. A holder waiting for its turn is `wanting`: no new guest starts ahead of it. */
|
|
221
|
-
async function enter(token, session, data) {
|
|
222
|
-
for (;;) {
|
|
223
|
-
if (guestBatch === token) return 'guest';
|
|
224
|
-
if (guestBatch === null) {
|
|
225
|
-
if (holder === null || holder === token) {
|
|
226
|
-
holder = token;
|
|
227
|
-
session.busy = true;
|
|
228
|
-
session.wanting = false;
|
|
229
|
-
return 'own';
|
|
230
|
-
}
|
|
231
|
-
const host = sessions.get(holder);
|
|
232
|
-
if (host && !host.busy && !host.wanting && !host.midBatch && host.status === 'T'
|
|
233
|
-
&& session.status !== 'T' && session.status !== 'E' && guestMessageAllowed(session, data)) {
|
|
234
|
-
guestBatch = token;
|
|
235
|
-
guestStartedAt = machineNow();
|
|
236
|
-
return 'guest';
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (holder === token) session.wanting = true;
|
|
240
|
-
await new Promise((resolve) => waiters.push(resolve));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
function release(token) {
|
|
244
|
-
if (holder !== token) return;
|
|
245
|
-
holder = null;
|
|
246
|
-
wakeAll();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** A statement's text with its string literals, quoted identifiers and comments blanked, or null when it cannot be
|
|
250
|
-
* read safely (a dollar-quoted body, an unterminated literal): the caller then treats it as not read-only. */
|
|
251
|
-
function statementSkeleton(text) {
|
|
252
|
-
// dollar quoting and backslash-escape strings (E'…', where \' does not close) are not read here: not read-only
|
|
253
|
-
// (a backslash anywhere, too: with standard_conforming_strings off, which one connection can set for the shared
|
|
254
|
-
// session, a plain '…' takes backslash escapes)
|
|
255
|
-
if (/\$[A-Za-z_0-9]*\$/.test(text) || text.includes('\\')) return null;
|
|
256
|
-
let out = '';
|
|
257
|
-
for (let i = 0; i < text.length;) {
|
|
258
|
-
const c = text[i];
|
|
259
|
-
if (c === "'" || c === '"') {
|
|
260
|
-
const end = text.indexOf(c, i + 1);
|
|
261
|
-
if (end < 0) return null;
|
|
262
|
-
let j = end;
|
|
263
|
-
while (text[j + 1] === c) { const next = text.indexOf(c, j + 2); if (next < 0) return null; j = next; }
|
|
264
|
-
out += ' ';
|
|
265
|
-
i = j + 1;
|
|
266
|
-
} else if (c === '-' && text[i + 1] === '-') {
|
|
267
|
-
const end = text.indexOf('\n', i);
|
|
268
|
-
i = end < 0 ? text.length : end;
|
|
269
|
-
out += ' ';
|
|
270
|
-
} else if (c === '/' && text[i + 1] === '*') {
|
|
271
|
-
const end = text.indexOf('*/', i + 2);
|
|
272
|
-
if (end < 0) return null;
|
|
273
|
-
i = end + 2;
|
|
274
|
-
out += ' ';
|
|
275
|
-
} else { out += c; i += 1; }
|
|
276
|
-
}
|
|
277
|
-
return out.trim();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Whether a statement only reads: one SELECT (or WITH/SHOW/VALUES/TABLE/EXPLAIN without ANALYZE) that writes,
|
|
281
|
-
* locks or advances nothing, judged on its text with literals and comments removed. A false "no" only waits. */
|
|
282
|
-
function readOnlySql(text) {
|
|
283
|
-
const sql = statementSkeleton(text);
|
|
284
|
-
if (sql === null) return false;
|
|
285
|
-
if (sql.replace(/;\s*$/, '').includes(';')) return false; // one statement only
|
|
286
|
-
if (!/^(select|with|show|values|table|explain)\b/i.test(sql)) return false;
|
|
287
|
-
return !/\b(insert|update|delete|merge|into|copy|analyze|call|do|set_config|nextval|setval|pg_(try_)?advisory\w*|pg_notify|lo_\w+|for\s+(update|share|no\s+key\s+update|key\s+share))\b/i.test(sql);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const text = (data, from) => data.subarray(from, data.indexOf(0, from)).toString('utf8');
|
|
291
|
-
const asBuffer = (m) => Buffer.from(m.buffer, m.byteOffset, m.byteLength);
|
|
292
|
-
|
|
293
|
-
/** Prepared statements' protocol names each connection gives, kept apart where they would collide.
|
|
294
|
-
* Every connection shares the one backend session, and a client names its statements per connection: Prisma's engine
|
|
295
|
-
* starts each process at `s0`, so a second process's first Parse met "prepared statement "s0" already exists" (Postiz
|
|
296
|
-
* walk postiz-2: sign-up, and the post workflow's first read, which left no timer to publish the post). A name no other
|
|
297
|
-
* live connection holds goes to the backend as the client gave it, so SQL on it works as on Postgres; one another
|
|
298
|
-
* live connection holds goes as a short name of the host's own (`volter_s<N>`), and the connection's own simple-Query
|
|
299
|
-
* DEALLOCATE or EXECUTE of it is rewritten to match (sqlNames). Names are compared on their first 63 bytes, as Postgres compares them
|
|
300
|
-
* (NAMEDATALEN), and handled as bytes, never decoded. A closed connection's statements are closed on the next turn
|
|
301
|
-
* that begins outside any transaction (flushClosed). */
|
|
302
|
-
const NAMEDATALEN = 63;
|
|
303
|
-
/** backend statement name (latin1 of its bytes, first 63) -> the session holding it */
|
|
304
|
-
const heldStatements = new Map();
|
|
305
|
-
/** backend statement names of closed connections, closed at the next clean turn */
|
|
306
|
-
const closedStatements = [];
|
|
307
|
-
let renamed = 0;
|
|
308
|
-
/** A name no statement has: what another connection's statement name is, to a connection that never prepared it. */
|
|
309
|
-
const UNPREPARED = Buffer.from('volter_unprepared', 'latin1');
|
|
310
|
-
const nameKey = (bytes) => bytes.subarray(0, NAMEDATALEN).toString('latin1');
|
|
311
|
-
|
|
312
|
-
/** The name at `from` (its bytes, to the NUL) and where the field after it starts. */
|
|
313
|
-
function cstring(data, from) {
|
|
314
|
-
const end = data.indexOf(0, from);
|
|
315
|
-
return { bytes: data.subarray(from, end), next: end + 1 };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** The backend's name for a statement this connection names `bytes`; `claim` (a Parse on the connection's turn) takes
|
|
319
|
-
* one for a new statement, anything else only looks its name up. */
|
|
320
|
-
function statementName(session, bytes, claim) {
|
|
321
|
-
if (bytes.length === 0) return bytes;
|
|
322
|
-
const own = session.names.get(bytes.toString('latin1'));
|
|
323
|
-
if (own) return own;
|
|
324
|
-
if (!claim) {
|
|
325
|
-
// a statement it never prepared: the backend answers as Postgres does, never with another live connection's
|
|
326
|
-
// statement of that name (it would not exist in this connection's own session)
|
|
327
|
-
const holder = heldStatements.get(nameKey(bytes));
|
|
328
|
-
return holder && holder !== session && !holder.closed ? UNPREPARED : bytes;
|
|
329
|
-
}
|
|
330
|
-
const key = nameKey(bytes);
|
|
331
|
-
const holder = heldStatements.get(key);
|
|
332
|
-
const backend = holder && holder !== session ? Buffer.from(`volter_s${(renamed += 1)}`, 'latin1') : bytes;
|
|
333
|
-
heldStatements.set(nameKey(backend), session);
|
|
334
|
-
session.names.set(bytes.toString('latin1'), backend);
|
|
335
|
-
return backend;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** The message with its statement names as the backend knows them. Portals keep the client's names: a named portal
|
|
339
|
-
* lives only inside its transaction, and one transaction runs at a time. */
|
|
340
|
-
function scopeNames(session, message, claim) {
|
|
341
|
-
const data = asBuffer(message);
|
|
342
|
-
const kind = data[0];
|
|
343
|
-
let parts;
|
|
344
|
-
if (kind === 0x50 /* P */) {
|
|
345
|
-
const name = cstring(data, 5);
|
|
346
|
-
parts = [statementName(session, name.bytes, claim), Buffer.from([0]), data.subarray(name.next)];
|
|
347
|
-
} else if (kind === 0x42 /* B */) {
|
|
348
|
-
const portal = cstring(data, 5);
|
|
349
|
-
const statement = cstring(data, portal.next);
|
|
350
|
-
parts = [data.subarray(5, portal.next), statementName(session, statement.bytes, false), Buffer.from([0]), data.subarray(statement.next)];
|
|
351
|
-
} else if ((kind === 0x44 /* D */ || kind === 0x43 /* C */) && data[5] === 0x53 /* S */) {
|
|
352
|
-
const name = cstring(data, 6);
|
|
353
|
-
const backend = statementName(session, name.bytes, false);
|
|
354
|
-
parts = [data.subarray(5, 6), backend, Buffer.from([0]), data.subarray(name.next)];
|
|
355
|
-
} else if (kind === 0x51 /* Q */ && claim) {
|
|
356
|
-
return sqlNames(session, message);
|
|
357
|
-
} else {
|
|
358
|
-
return message;
|
|
359
|
-
}
|
|
360
|
-
const body = Buffer.concat(parts);
|
|
361
|
-
if (body.length === data.length - 5 && body.equals(data.subarray(5))) return message;
|
|
362
|
-
const out = Buffer.alloc(5 + body.length);
|
|
363
|
-
out[0] = kind;
|
|
364
|
-
out.writeInt32BE(4 + body.length, 1);
|
|
365
|
-
body.copy(out, 5);
|
|
366
|
-
return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** A statement of a simple Query that DEALLOCATEs or EXECUTEs one statement, at the statement's start: the verb
|
|
370
|
-
* (EXPLAIN's options before EXECUTE included) and the name, quoted or not. */
|
|
371
|
-
const STATEMENT_NAMED = /^((?:EXPLAIN\s+(?:\([^)]*\)\s*)?(?:ANALYZE\s+)?(?:VERBOSE\s+)?)?(?:DEALLOCATE\s+(?:PREPARE\s+)?|EXECUTE\s+))(?:"((?:[^"]|"")+)"|([A-Za-z_][A-Za-z0-9_$]*))(?=[\s(;]|$)/i;
|
|
372
|
-
/** a quoted name as it is; an unquoted one folds to lower case, as Postgres folds it */
|
|
373
|
-
const folded = (quoted, name) => (quoted !== undefined ? quoted.replace(/""/g, '"') : name.toLowerCase());
|
|
374
|
-
const EMPTIES_ALL = /^(?:DEALLOCATE\s+(?:PREPARE\s+)?ALL|DISCARD\s+ALL)\s*;?\s*$/i;
|
|
375
|
-
|
|
376
|
-
/** Where each statement of a Query begins, outside string literals, quoted identifiers, dollar-quoted bodies and
|
|
377
|
-
* comments (a statement's leading comments skipped), so a rewrite never touches a literal's text. */
|
|
378
|
-
function statementStarts(sql) {
|
|
379
|
-
const starts = [];
|
|
380
|
-
let atStart = true;
|
|
381
|
-
for (let i = 0; i < sql.length;) {
|
|
382
|
-
const c = sql[i];
|
|
383
|
-
if (/\s/.test(c)) { i += 1; continue; }
|
|
384
|
-
if (c === '-' && sql[i + 1] === '-') { const end = sql.indexOf('\n', i); i = end < 0 ? sql.length : end + 1; continue; }
|
|
385
|
-
if (c === '/' && sql[i + 1] === '*') {
|
|
386
|
-
let depth = 1; let j = i + 2; // Postgres's block comments nest
|
|
387
|
-
while (j < sql.length && depth > 0) { if (sql.startsWith('/*', j)) { depth += 1; j += 2; } else if (sql.startsWith('*/', j)) { depth -= 1; j += 2; } else j += 1; }
|
|
388
|
-
i = j; continue;
|
|
389
|
-
}
|
|
390
|
-
if (atStart) { starts.push(i); atStart = false; }
|
|
391
|
-
if (c === ';') { atStart = true; i += 1; continue; }
|
|
392
|
-
if (c === "'" || c === '"') {
|
|
393
|
-
// E'…' takes backslash escapes; a plain '…' and "…" end at their quote, a doubled one inside
|
|
394
|
-
const escapes = c === "'" && /[Ee]/.test(sql[i - 1] ?? '') && !/[A-Za-z0-9_]/.test(sql[i - 2] ?? '');
|
|
395
|
-
let j = i + 1;
|
|
396
|
-
while (j < sql.length) {
|
|
397
|
-
if (escapes && sql[j] === '\\') { j += 2; continue; }
|
|
398
|
-
if (sql[j] === c) { if (sql[j + 1] === c) { j += 2; continue; } break; }
|
|
399
|
-
j += 1;
|
|
400
|
-
}
|
|
401
|
-
i = j + 1; continue;
|
|
402
|
-
}
|
|
403
|
-
// a `$` inside an identifier (`a$b$c`) opens nothing, as Postgres's lexer reads it
|
|
404
|
-
const dollar = c === '$' && !/[A-Za-z0-9_$]/.test(sql[i - 1] ?? '') ? /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql.slice(i)) : null;
|
|
405
|
-
if (dollar) { const end = sql.indexOf(dollar[0], i + dollar[0].length); i = end < 0 ? sql.length : end + dollar[0].length; continue; }
|
|
406
|
-
i += 1;
|
|
407
|
-
}
|
|
408
|
-
return starts;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** Each statement of the Query that DEALLOCATEs or EXECUTEs one name: where it is, its verb and the name. */
|
|
412
|
-
function namedStatements(sql) {
|
|
413
|
-
const out = [];
|
|
414
|
-
for (const at of statementStarts(sql)) {
|
|
415
|
-
const m = STATEMENT_NAMED.exec(sql.slice(at));
|
|
416
|
-
if (m) out.push({ at, length: m[0].length, verb: m[1], name: folded(m[2], m[3]) });
|
|
417
|
-
}
|
|
418
|
-
return out;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/** A simple Query's DEALLOCATE or EXECUTE of a statement this connection prepared under a name of the host's own
|
|
422
|
-
* (another connection held the client's), or of a name another live connection holds: the statement it means, never
|
|
423
|
-
* the other connection's. Only statements' own text is rewritten (statementStarts); CREATE TABLE … AS EXECUTE and
|
|
424
|
-
* a DEALLOCATE sent through Parse are not. */
|
|
425
|
-
function sqlNames(session, message) {
|
|
426
|
-
const data = asBuffer(message);
|
|
427
|
-
const sql = data.subarray(5, data.indexOf(0, 5)).toString('latin1');
|
|
428
|
-
// a backslash can end a plain '…' literal (standard_conforming_strings off, which one connection can set for the
|
|
429
|
-
// shared session): such a Query is not read, as statementSkeleton does not read one
|
|
430
|
-
if (sql.includes('\\')) return message;
|
|
431
|
-
let text = sql;
|
|
432
|
-
for (const n of namedStatements(sql).reverse()) {
|
|
433
|
-
const backend = statementName(session, Buffer.from(n.name, 'latin1'), false).toString('latin1');
|
|
434
|
-
if (backend !== n.name) text = `${text.slice(0, n.at)}${n.verb}"${backend}"${text.slice(n.at + n.length)}`;
|
|
435
|
-
}
|
|
436
|
-
if (text === sql) return message;
|
|
437
|
-
const body = Buffer.concat([Buffer.from(text, 'latin1'), Buffer.from([0])]);
|
|
438
|
-
const out = Buffer.alloc(5 + body.length);
|
|
439
|
-
out[0] = 0x51;
|
|
440
|
-
out.writeInt32BE(4 + body.length, 1);
|
|
441
|
-
body.copy(out, 5);
|
|
442
|
-
return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** What a client message closes: a statement (Close 'S', or a Query that is one DEALLOCATE), by the client's name,
|
|
446
|
-
* or ALL (a Query that is one DEALLOCATE ALL or DISCARD ALL, emptying the one session's statements, every
|
|
447
|
-
* connection's, as they did before scoping). */
|
|
448
|
-
const ALL = Symbol('all');
|
|
449
|
-
function closing(message) {
|
|
450
|
-
const data = asBuffer(message);
|
|
451
|
-
if (data[0] === 0x43 && data[5] === 0x53) { const name = cstring(data, 6).bytes; return name.length ? name : undefined; }
|
|
452
|
-
if (data[0] === 0x51) {
|
|
453
|
-
// one statement alone in its Query (one inside a longer Query settles when the connection closes)
|
|
454
|
-
const sql = data.subarray(5, data.indexOf(0, 5)).toString('latin1');
|
|
455
|
-
const starts = statementStarts(sql);
|
|
456
|
-
if (starts.length !== 1 && !(starts.length === 2 && /^;\s*$/.test(sql.slice(starts[1])))) return undefined;
|
|
457
|
-
if (EMPTIES_ALL.test(sql.slice(starts[0]))) return ALL;
|
|
458
|
-
const named = namedStatements(sql);
|
|
459
|
-
if (named.length === 1 && /^DEALLOCATE/i.test(named[0].verb)) return Buffer.from(named[0].name, 'latin1');
|
|
460
|
-
}
|
|
461
|
-
return undefined;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** Free what a message closed once the backend has answered it: CloseComplete ('3') or the DEALLOCATE's
|
|
465
|
-
* CommandComplete ('C') first; a Close Postgres skipped (an earlier error in its batch) frees nothing. */
|
|
466
|
-
function settleClose(session, name, response) {
|
|
467
|
-
if (!name || !response || !response.length || (response[0] !== 0x33 && response[0] !== 0x43)) return;
|
|
468
|
-
if (name === ALL) {
|
|
469
|
-
heldStatements.clear();
|
|
470
|
-
for (const other of sessions.values()) other.names.clear();
|
|
471
|
-
} else forget(session, name);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/** A statement the connection closed (Close 'S'): its backend name is free again. */
|
|
475
|
-
function forget(session, bytes) {
|
|
476
|
-
const backend = session.names.get(bytes.toString('latin1'));
|
|
477
|
-
if (!backend) return;
|
|
478
|
-
session.names.delete(bytes.toString('latin1'));
|
|
479
|
-
if (heldStatements.get(nameKey(backend)) === session) heldStatements.delete(nameKey(backend));
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** A closed connection's statements, to close on the next clean turn (a leak otherwise: each connection's statements
|
|
483
|
-
* outlived it in the one session, 300 short connections x 20 statements aborted the backend). */
|
|
484
|
-
function releaseStatements(session) {
|
|
485
|
-
for (const backend of session.names.values()) if (heldStatements.get(nameKey(backend)) === session) closedStatements.push(backend);
|
|
486
|
-
session.names.clear();
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
/** Close closed connections' statements with the protocol's Close (a SQL DEALLOCATE would replace the unnamed
|
|
490
|
-
* statement a batch may still Bind), at the start of a turn outside any transaction. */
|
|
491
|
-
async function flushClosed(db) {
|
|
492
|
-
while (closedStatements.length) {
|
|
493
|
-
const backend = closedStatements.shift();
|
|
494
|
-
const body = Buffer.concat([Buffer.from('S', 'latin1'), backend, Buffer.from([0])]);
|
|
495
|
-
const close = Buffer.alloc(5 + body.length);
|
|
496
|
-
close[0] = 0x43;
|
|
497
|
-
close.writeInt32BE(4 + body.length, 1);
|
|
498
|
-
body.copy(close, 5);
|
|
499
|
-
try {
|
|
500
|
-
await db.execProtocolRaw(new Uint8Array(close));
|
|
501
|
-
await db.execProtocolRaw(new Uint8Array([0x53, 0, 0, 0, 4])); // Sync
|
|
502
|
-
} catch (error) { process.stderr.write(`pglite-host: statement cleanup failed: ${errorMessage(error)}\n`); }
|
|
503
|
-
if (heldStatements.get(nameKey(backend))?.closed) heldStatements.delete(nameKey(backend));
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** Record, per connection, which named statements it prepared read-only (a Parse of its own or a guest's); a
|
|
508
|
-
* PREPARE/DEALLOCATE/DISCARD sent as SQL forgets them all. */
|
|
509
|
-
function noteStatement(session, message) {
|
|
510
|
-
const data = asBuffer(message);
|
|
511
|
-
if (data[0] === 0x51 /* Q */ && /\b(prepare|deallocate|discard)\b/i.test(text(data, 5))) session.statements.clear();
|
|
512
|
-
else if (data[0] === 0x50 /* P */) {
|
|
513
|
-
const name = text(data, 5);
|
|
514
|
-
session.statements.set(name, readOnlySql(text(data, 5 + Buffer.byteLength(name) + 1)));
|
|
515
|
-
} else if (data[0] === 0x43 /* C */ && data[5] === 0x53 /* S */) {
|
|
516
|
-
session.statements.delete(text(data, 6));
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/** Whether a guest may run this message: a Query or Parse of read-only SQL, a Bind of a statement this connection
|
|
521
|
-
* prepared read-only (the unnamed one included), and the batch's Describe/Execute/Sync/Flush/Close. */
|
|
522
|
-
function guestMessageAllowed(session, message) {
|
|
523
|
-
const data = asBuffer(message);
|
|
524
|
-
switch (data[0]) {
|
|
525
|
-
case 0x51: return readOnlySql(text(data, 5)); // Q
|
|
526
|
-
case 0x50: { const name = text(data, 5); return readOnlySql(text(data, 5 + Buffer.byteLength(name) + 1)); } // P
|
|
527
|
-
case 0x42: { // B: portal name, then statement name; the unnamed one only as this batch parsed it
|
|
528
|
-
const portal = text(data, 5);
|
|
529
|
-
const statement = text(data, 5 + Buffer.byteLength(portal) + 1);
|
|
530
|
-
if (statement === '') return session.guestOpen && session.guestUnnamed === true;
|
|
531
|
-
return session.statements.get(statement) === true;
|
|
532
|
-
}
|
|
533
|
-
case 0x44: case 0x45: case 0x53: case 0x48: case 0x43: return session.guestOpen; // D E S H C, inside its batch
|
|
534
|
-
default: return false;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/** An ErrorResponse (and nothing else): the guest batch refused mid-way. */
|
|
539
|
-
function errorResponse(code, message) {
|
|
540
|
-
const fields = ['SERROR', 'VERROR', `C${code}`, `M${message}`];
|
|
541
|
-
const body = Buffer.concat([...fields.map((f) => Buffer.from(`${f}\0`)), Buffer.from([0])]);
|
|
542
|
-
const head = Buffer.alloc(5);
|
|
543
|
-
head[0] = 0x45;
|
|
544
|
-
head.writeInt32BE(body.length + 4, 1);
|
|
545
|
-
return Buffer.concat([head, body]);
|
|
546
|
-
}
|
|
547
|
-
const READY_IDLE = Buffer.from([0x5a, 0, 0, 0, 5, 0x49]);
|
|
548
|
-
|
|
549
|
-
/** A statement of the host's own around a guest; its failure (the holder's transaction already gone) is not fatal. */
|
|
550
|
-
async function quiet(db, sql) {
|
|
551
|
-
try { await db.query(sql); } catch (error) {
|
|
552
|
-
if (error?.code !== '3B001' && error?.code !== '25P01') throw error;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/** `response` with its last ReadyForQuery reporting `status`. */
|
|
557
|
-
function withReadyStatus(response, status) {
|
|
558
|
-
const out = Buffer.from(response);
|
|
559
|
-
let offset = 0;
|
|
560
|
-
let at = -1;
|
|
561
|
-
while (offset + 5 <= out.length) {
|
|
562
|
-
const length = out.readInt32BE(offset + 1);
|
|
563
|
-
if (length < 4) break;
|
|
564
|
-
if (out[offset] === 0x5a && offset + 5 < out.length) at = offset + 5;
|
|
565
|
-
offset += 1 + length;
|
|
566
|
-
}
|
|
567
|
-
if (at >= 0) out[at] = status.charCodeAt(0);
|
|
568
|
-
return out;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/** Postgres's FATAL for a session past idle_in_transaction_session_timeout
|
|
572
|
-
* (postgres.c: ERRCODE_IDLE_IN_TRANSACTION_SESSION_TIMEOUT, 25P03). */
|
|
573
|
-
function idleInTransactionFatal() {
|
|
574
|
-
const fields = ['SFATAL', 'VFATAL', 'C25P03', 'Mterminating connection due to idle-in-transaction timeout'];
|
|
575
|
-
const body = Buffer.concat([...fields.map((f) => Buffer.from(`${f}\0`)), Buffer.from([0])]);
|
|
576
|
-
const head = Buffer.alloc(5);
|
|
577
|
-
head[0] = 0x45; // 'E'
|
|
578
|
-
head.writeInt32BE(body.length + 4, 1);
|
|
579
|
-
return Buffer.concat([head, body]);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// the holder idle in its transaction past the limit while others wait: end it
|
|
583
|
-
const idleWatch = setInterval(() => {
|
|
584
|
-
// a guest holding its turn past GUEST_TURN_MS while others wait: its batch is refused, its savepoint undone
|
|
585
|
-
if (guestBatch !== null && waiters.length > 0 && machineNow() - guestStartedAt > GUEST_TURN_MS) {
|
|
586
|
-
const guest = sessions.get(guestBatch);
|
|
587
|
-
const expired = guestBatch;
|
|
588
|
-
if (guest && guest.guestRefused && !guest.guestRunning) {
|
|
589
|
-
// a refused batch that never sends its Sync: its savepoint is already undone; its turn ends
|
|
590
|
-
guest.guestRefused = false;
|
|
591
|
-
guest.guestExpired = true;
|
|
592
|
-
guest.guestErrorSent = true;
|
|
593
|
-
const host = sessions.get(holder);
|
|
594
|
-
if (host) host.idleSince += machineNow() - guestStartedAt;
|
|
595
|
-
guestBatch = null;
|
|
596
|
-
wakeAll();
|
|
597
|
-
} else if (guest && guest.guestOpen && !guest.guestRunning) {
|
|
598
|
-
guest.guestOpen = false;
|
|
599
|
-
guest.guestUnnamed = false;
|
|
600
|
-
guest.guestExpired = true;
|
|
601
|
-
void dbReady.then(async (db) => {
|
|
602
|
-
await quiet(db, 'ROLLBACK TO SAVEPOINT volter_guest');
|
|
603
|
-
await quiet(db, 'RELEASE SAVEPOINT volter_guest');
|
|
604
|
-
}).then(() => {
|
|
605
|
-
if (guestBatch !== expired) return;
|
|
606
|
-
const host = sessions.get(holder);
|
|
607
|
-
if (host) host.idleSince += machineNow() - guestStartedAt;
|
|
608
|
-
guestBatch = null;
|
|
609
|
-
wakeAll();
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (holder === null || waiters.length === 0) return;
|
|
614
|
-
const session = sessions.get(holder);
|
|
615
|
-
// armed at ReadyForQuery inside a transaction, as Postgres arms it — never mid-pipeline
|
|
616
|
-
// a holder waiting for its turn (behind a guest) is sending, not idle; a guest's turn is not the holder's idleness
|
|
617
|
-
if (!session || session.busy || session.wanting || guestBatch !== null || session.terminated || (session.status !== 'T' && session.status !== 'E')) return;
|
|
618
|
-
if (machineNow() - session.idleSince < IDLE_IN_TRANSACTION_MS) return;
|
|
619
|
-
session.terminated = true;
|
|
620
|
-
process.stderr.write('pglite-host: terminated a connection idle in its transaction while others waited\n');
|
|
621
|
-
// the socket's close handler rolls the transaction back and releases the lock
|
|
622
|
-
session.socket.end(idleInTransactionFatal());
|
|
623
|
-
setTimeout(() => session.socket.destroy(), 500).unref();
|
|
624
|
-
}, 250);
|
|
625
|
-
idleWatch.unref();
|
|
626
|
-
|
|
627
|
-
/** The transaction status of the LAST ReadyForQuery ('Z') message in a raw
|
|
628
|
-
* protocol response, or null when the response carries none (mid-pipeline:
|
|
629
|
-
* the sender keeps the lock). 'I' = idle, 'T' = in transaction, 'E' = failed
|
|
630
|
-
* transaction. */
|
|
631
|
-
function lastReadyStatus(response) {
|
|
632
|
-
let status = null;
|
|
633
|
-
let offset = 0;
|
|
634
|
-
while (offset + 5 <= response.length) {
|
|
635
|
-
const type = response[offset];
|
|
636
|
-
const length = (response[offset + 1] << 24) | (response[offset + 2] << 16)
|
|
637
|
-
| (response[offset + 3] << 8) | response[offset + 4];
|
|
638
|
-
if (length < 4) break; // malformed; stop scanning rather than loop
|
|
639
|
-
if (type === 0x5a /* 'Z' */ && offset + 5 < response.length) {
|
|
640
|
-
status = String.fromCharCode(response[offset + 5]);
|
|
641
|
-
}
|
|
642
|
-
offset += 1 + length;
|
|
643
|
-
}
|
|
644
|
-
return status;
|
|
155
|
+
// ---- exclusive backend session ---------------------------------------------
|
|
156
|
+
let activeSession = null;
|
|
157
|
+
const SYNC = new Uint8Array([0x53, 0, 0, 0, 4]);
|
|
158
|
+
const SESSION_GAP = 'World PGlite supports one PostgreSQL session; select the native or container backing for concurrent connections';
|
|
159
|
+
function concurrentSession() {
|
|
160
|
+
return BackendError.create({ severity: 'FATAL', code: '53300', message: SESSION_GAP,
|
|
161
|
+
detail: 'PGlite has one PostgreSQL backend session; separate wire connections cannot share its transactions or session state faithfully.',
|
|
162
|
+
hint: 'Select VOLTER_WORLD_INFRA_BACKING=native with PostgreSQL installed, or use the container backing.' });
|
|
645
163
|
}
|
|
646
164
|
|
|
647
165
|
// ---- CREATE DATABASE refusal -----------------------------------------------
|
|
@@ -736,7 +254,7 @@ function refuseStatements(message, own) {
|
|
|
736
254
|
// then answers the Sync with a second one. The client counts two and every
|
|
737
255
|
// later result shifts by one (pg then crashes on a null handler). So the
|
|
738
256
|
// ReadyForQuery that follows an error from an extended-protocol message is
|
|
739
|
-
// dropped; the Sync's own one ends the batch
|
|
257
|
+
// dropped; the Sync's own one ends the batch. Messages
|
|
740
258
|
// that end a simple cycle — Query, Sync, FunctionCall, CopyDone, CopyFail —
|
|
741
259
|
// keep theirs: Postgres always answers them with one.
|
|
742
260
|
// Measured on PGlite 0.5.8 by feeding execProtocolRaw directly: Parse of
|
|
@@ -766,37 +284,28 @@ function withoutEarlyReady(message, response) {
|
|
|
766
284
|
return response;
|
|
767
285
|
}
|
|
768
286
|
|
|
769
|
-
const server = net.createServer(
|
|
770
|
-
const
|
|
771
|
-
let holding = false;
|
|
772
|
-
const session = { socket, busy: false, idleSince: machineNow(), terminated: false, closed: false, status: null, midBatch: false,
|
|
773
|
-
wanting: false, guestOpen: false, guestRefused: false, guestUnnamed: false, guestExpired: false, guestErrorSent: false, guestRunning: false, statements: new Map(),
|
|
774
|
-
names: new Map() };
|
|
775
|
-
sessions.set(token, session);
|
|
287
|
+
const server = net.createServer((socket) => {
|
|
288
|
+
const session = { closed: false, pending: Promise.resolve() };
|
|
776
289
|
socket.on('close', () => {
|
|
777
290
|
session.closed = true;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
.
|
|
792
|
-
|
|
793
|
-
.then(() => {
|
|
794
|
-
holding = false;
|
|
795
|
-
release(token);
|
|
796
|
-
});
|
|
291
|
+
if (activeSession !== session) return;
|
|
292
|
+
// Admission stays reserved through pending messages and confirmed reset.
|
|
293
|
+
// Sync also clears a disconnected extended-protocol batch's error state.
|
|
294
|
+
void session.pending.then(async () => {
|
|
295
|
+
const db = await dbReady;
|
|
296
|
+
readWorldClock();
|
|
297
|
+
await db.execProtocolRaw(SYNC);
|
|
298
|
+
await db.exec('ROLLBACK');
|
|
299
|
+
await db.exec('DISCARD ALL');
|
|
300
|
+
await db.exec(`SET SESSION AUTHORIZATION ${quoteIdentifier(databaseUser)}`);
|
|
301
|
+
activeSession = null;
|
|
302
|
+
}).catch(error => {
|
|
303
|
+
process.stderr.write(`pglite-host: session cleanup failed: ${errorMessage(error)}\n`);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
|
797
306
|
});
|
|
798
307
|
socket.on('error', () => { /* close handler owns cleanup */ });
|
|
799
|
-
|
|
308
|
+
void fromNodeSocket(socket, {
|
|
800
309
|
serverVersion: '18.3 (PGlite)',
|
|
801
310
|
auth: { method: 'password', getClearTextPassword: () => databasePassword,
|
|
802
311
|
validateCredentials: ({ username, password }) => username === databaseUser && password === databasePassword },
|
|
@@ -804,115 +313,35 @@ const server = net.createServer(async (socket) => {
|
|
|
804
313
|
const requested = clientParams?.database || clientParams?.user;
|
|
805
314
|
if (requested !== databaseName) throw BackendError.create({ severity: 'FATAL', code: '3D000', message: `database "${requested}" does not exist` });
|
|
806
315
|
},
|
|
807
|
-
|
|
316
|
+
onAuthenticated() {
|
|
317
|
+
if (activeSession !== null) throw concurrentSession();
|
|
318
|
+
if (!session.closed) activeSession = session;
|
|
319
|
+
},
|
|
320
|
+
onMessage(data, { isAuthenticated }) {
|
|
808
321
|
if (!isAuthenticated) return undefined; // gateway owns startup/auth traffic
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const db = await dbReady;
|
|
815
|
-
readWorldClock();
|
|
816
|
-
// a guest whose turn ran out: told once, the rest of its batch dropped until its Sync
|
|
817
|
-
if (session.guestExpired) {
|
|
818
|
-
if (data[0] === 0x53) { session.guestExpired = false; session.guestErrorSent = false; session.status = 'I'; return READY_IDLE; }
|
|
819
|
-
if (session.guestErrorSent) return new Uint8Array(0);
|
|
820
|
-
session.guestErrorSent = true;
|
|
821
|
-
return errorResponse('57014', 'the containerless Postgres (one session) cancelled this read: it ran beside another connection\'s open transaction longer than its turn allows');
|
|
822
|
-
}
|
|
823
|
-
const role = await enter(token, session, data);
|
|
824
|
-
if (role === 'guest') {
|
|
322
|
+
if (activeSession !== session) throw concurrentSession();
|
|
323
|
+
// Gateway owns Terminate; sending it to the shared backend would end the
|
|
324
|
+
// backend itself. Disconnect cleanup resets its session instead.
|
|
325
|
+
if (data[0] === 0x58) return undefined;
|
|
326
|
+
const pending = session.pending.then(async () => {
|
|
825
327
|
if (session.closed) return undefined;
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
const endGuest = () => {
|
|
829
|
-
session.guestOpen = false;
|
|
830
|
-
session.guestRefused = false;
|
|
831
|
-
session.status = 'I';
|
|
832
|
-
guestBatch = null;
|
|
833
|
-
session.guestUnnamed = false;
|
|
834
|
-
// the holder's idle clock stops while a guest runs, and goes on from where it was
|
|
835
|
-
const host = sessions.get(holder);
|
|
836
|
-
if (host) host.idleSince += machineNow() - guestStartedAt;
|
|
837
|
-
wakeAll();
|
|
838
|
-
};
|
|
839
|
-
// a batch refused mid-way: its remaining messages are dropped until its Sync, as Postgres skips after an error
|
|
840
|
-
if (session.guestRefused) {
|
|
841
|
-
if (sync) { endGuest(); return READY_IDLE; }
|
|
842
|
-
return new Uint8Array(0);
|
|
843
|
-
}
|
|
844
|
-
if (session.guestOpen && !guestMessageAllowed(session, data)) {
|
|
845
|
-
session.guestRunning = true; // the expiry waits until this refusal is done
|
|
846
|
-
await quiet(db, 'ROLLBACK TO SAVEPOINT volter_guest');
|
|
847
|
-
await quiet(db, 'RELEASE SAVEPOINT volter_guest');
|
|
848
|
-
session.guestOpen = false;
|
|
849
|
-
session.guestRefused = true;
|
|
850
|
-
session.guestRunning = false;
|
|
851
|
-
return errorResponse('0A000', 'the containerless Postgres (one session) runs only reads beside another connection\'s open transaction; this batch began with a read and went on to write');
|
|
852
|
-
}
|
|
853
|
-
if (!session.guestOpen) { await quiet(db, 'SAVEPOINT volter_guest'); session.guestOpen = true; }
|
|
854
|
-
data = scopeNames(session, client, true);
|
|
855
|
-
noteStatement(session, data);
|
|
856
|
-
if (data[0] === 0x50 && text(asBuffer(data), 5) === '') session.guestUnnamed = true;
|
|
857
|
-
let raw;
|
|
858
|
-
session.guestRunning = true;
|
|
328
|
+
const db = await dbReady;
|
|
329
|
+
readWorldClock();
|
|
859
330
|
try {
|
|
860
|
-
raw = await db.execProtocolRaw(refuseStatements(data,
|
|
331
|
+
const raw = await db.execProtocolRaw(refuseStatements(data, databaseName));
|
|
332
|
+
return withoutEarlyReady(data, raw);
|
|
861
333
|
} catch (error) {
|
|
334
|
+
// SQL errors arrive as protocol bytes. A throw is a backend failure.
|
|
862
335
|
process.stderr.write(`pglite-host: backend aborted: ${errorMessage(error)}\n`);
|
|
863
336
|
process.exit(1);
|
|
864
337
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (status === null) { session.guestRunning = false; return response; } // mid-batch: the guest keeps its turn until its Sync
|
|
869
|
-
// the savepoint is gone if the holder's transaction ended meanwhile (its socket closed): quiet, not fatal
|
|
870
|
-
if (status === 'E') await quiet(db, 'ROLLBACK TO SAVEPOINT volter_guest');
|
|
871
|
-
await quiet(db, 'RELEASE SAVEPOINT volter_guest');
|
|
872
|
-
session.guestRunning = false; // the batch has ended: nothing left for the expiry to cancel
|
|
873
|
-
endGuest();
|
|
874
|
-
return withReadyStatus(response, 'I');
|
|
875
|
-
}
|
|
876
|
-
// closed (or cut) while it waited: give the lock up without running the
|
|
877
|
-
// message — a Terminate run now would leave no ReadyForQuery to release it
|
|
878
|
-
if (session.closed || session.terminated) {
|
|
879
|
-
session.busy = false;
|
|
880
|
-
if (!holding) release(token);
|
|
881
|
-
return undefined;
|
|
882
|
-
}
|
|
883
|
-
holding = true;
|
|
884
|
-
session.busy = true;
|
|
885
|
-
// a turn that begins outside any transaction first closes what closed connections left
|
|
886
|
-
if (closedStatements.length && !session.midBatch && session.status !== 'T' && session.status !== 'E') await flushClosed(await dbReady);
|
|
887
|
-
data = scopeNames(session, client, true);
|
|
888
|
-
noteStatement(session, data);
|
|
889
|
-
// libpq's default: no database parameter means the user's name
|
|
890
|
-
const own = clientParams?.database || clientParams?.user;
|
|
891
|
-
let raw;
|
|
892
|
-
try {
|
|
893
|
-
raw = await db.execProtocolRaw(refuseStatements(data, own));
|
|
894
|
-
} catch (error) {
|
|
895
|
-
// SQL errors arrive as ErrorResponse bytes; a throw means the WASM
|
|
896
|
-
// backend itself aborted and every later call would hang. Exit so the
|
|
897
|
-
// World sees the service down (status fails) instead of a wedged port.
|
|
898
|
-
process.stderr.write(`pglite-host: backend aborted: ${errorMessage(error)}\n`);
|
|
899
|
-
process.exit(1);
|
|
900
|
-
}
|
|
901
|
-
session.busy = false;
|
|
902
|
-
session.idleSince = machineNow();
|
|
903
|
-
const response = withoutEarlyReady(data, raw);
|
|
904
|
-
settleClose(session, closes, response);
|
|
905
|
-
const status = lastReadyStatus(response);
|
|
906
|
-
session.midBatch = status === null;
|
|
907
|
-
if (status !== null) session.status = status;
|
|
908
|
-
if (status === 'I') {
|
|
909
|
-
holding = false;
|
|
910
|
-
release(token);
|
|
911
|
-
} else if (status === 'T') {
|
|
912
|
-
wakeAll(); // idle in its transaction: a waiting reader may now run as a guest
|
|
913
|
-
}
|
|
914
|
-
return response;
|
|
338
|
+
});
|
|
339
|
+
session.pending = pending;
|
|
340
|
+
return pending;
|
|
915
341
|
},
|
|
342
|
+
}).catch(error => {
|
|
343
|
+
process.stderr.write(`pglite-host: wire connection failed: ${errorMessage(error)}\n`);
|
|
344
|
+
socket.destroy();
|
|
916
345
|
});
|
|
917
346
|
});
|
|
918
347
|
|