@amritk/nish-x86_64-linux 0.22.1 → 0.23.1

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/bin/nish CHANGED
Binary file
package/docs/AI.md CHANGED
@@ -2584,6 +2584,38 @@ most 100 pairs. (No block, as for `nish/text`; `tests/link/std_url` is its
2584
2584
  program, and the user guide's [standard-library
2585
2585
  page](guide/using-nish/standard-library.md#nishneturl) its walk-through.)
2586
2586
 
2587
+ **Pass a list to a PostgreSQL statement** — there is no driver that expands
2588
+ `IN (...)` and no `pg`'s array serialiser: in `nish/net/postgres`, write
2589
+ `= any($1::int8[])` or `unnest($1::text[])` and bind the list as one
2590
+ parameter. `paramIntArray(values, start, count)` binds an `i64[]` window;
2591
+ `arrayStart()`, `arrayElement(bytes, start, length)` or `arrayNull()` per
2592
+ element, then `arrayEnd()`, builds a `text[]` straight into the Bind from
2593
+ windows of a buffer. An element refused for room answers `false` and leaves
2594
+ the array open: end it and send the rest in the next statement. Every other
2595
+ `param...` call and `bindEnd()` panic until `arrayEnd()`. (No block, as for
2596
+ `nish/text`; `tests/postgres/live` runs it against a server, and the user
2597
+ guide's [PostgreSQL page](guide/using-nish/postgres.md#array-parameters) its
2598
+ walk-through.)
2599
+
2600
+ **Batch a server's keyed reads into one PostgreSQL query** — there is no
2601
+ DataLoader and no `setTimeout` to collect a tick's keys: in `nish/net/postgres`,
2602
+ a `PgBatcher` built from a `PgBatchConfig` (`name`, `select` reading the key as
2603
+ `q.key`, `keyType` `PG_BATCH_INT` or `PG_BATCH_TEXT`, caps) queues requests with
2604
+ `addInt(key, tag)` or `addText(bytes, start, length, tag)` — `false` when its
2605
+ queue is full, or for a text key too long for `maxBytes`, with no room left in
2606
+ `queueBytes`, with a NUL or not UTF-8 — and `take(conn)`
2607
+ binds the waiting ones, each key once, as one execution of its `sql` over
2608
+ `unnest($1) with ordinality`, answering a run handle or -1. Hand that
2609
+ execution's `PG_ROW` to `row(run, conn)`, `PG_COMPLETE` to `end(run)`, an error
2610
+ or `PG_LOST` to `fail(run)` (on a spent handle these do nothing and count in
2611
+ `stray`), and after each call `next(run)` until
2612
+ `PG_BATCH_NONE`: `PG_BATCH_ROW` (the connection's row answers request `tag`),
2613
+ then one of `PG_BATCH_DONE`, `PG_BATCH_MISSING` or `PG_BATCH_FAILED` per
2614
+ request. It reads no clock: when to take is the loop's choice. (No block;
2615
+ `tests/postgres/live`'s `batch` scenario runs it against a server, and the user
2616
+ guide's [PostgreSQL page](guide/using-nish/postgres.md#batching-keyed-reads)
2617
+ its walk-through.)
2618
+
2587
2619
  **Draw to the whole terminal** (a game, a dashboard) — there is no curses and
2588
2620
  no `process.stdout.on("resize")`: `runTerminal(options, state, update, render)`
2589
2621
  from `nish/term-io` opens the terminal, probes what it can draw, steps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.22.1",
3
+ "version": "0.23.1",
4
4
  "description": "The nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
package/std/README.md CHANGED
@@ -202,7 +202,7 @@ reads one, and it still takes its randomness as an argument.
202
202
  | [`net/http3-server.ts`](./net/http3-server.ts) | `Http3Server`: HTTP/3 over QUIC on one UDP socket for a program that owns its loop — a pool of `QuicConnection` slots with an `Http3Connection` each, a slot's built on the first Initial accepted into it, slots 0 up, and reused, while the connection-ID index, the timer wheel and the rings are sized from the cap at start-up; a `QuicListener` in front; datagrams routed by a hashed index of every slot's connection IDs; `receive(now, key)`, `tick(now)`, `ready()` for the next slot with news, `flush(now)` sending paced GSO flights, `timeout(now)` from a timer wheel. A handshake that did not choose ALPN `h3` is closed with no_application_protocol (RFC 9114 §3.1); a slot done after GOAWAY is closed and freed. Every slot's final responses carry the server's one `HttpDate` (`date`), refreshed at `connection(slot)` and `ready()` and formatted at most once a second. `stop()` releases every connection and the listener and wipes the configuration's stateless reset and Retry token keys, which are the server's once handed in, and its last answer; the keys are zeroed in place, so a `QuicServerConfig` serves one server (QUIC-5). Native only, as `nish:net` is | A Nish client across loopback (`tests/link/net_http3_server`): requests, a paced 300 KB response, GOAWAY and the slot freed, ALPN, a full pool, the arena over connections through one slot while the pinned date moves a second on for each, one date formatted for two slots; every key read back as zero after `stop()` (`tests/link/net_quic_server_wipes`); aioquic and `curl --http3` against its `serve` mode |
203
203
  | [`net/webtransport.ts`](./net/webtransport.ts) | `WebTransport`, the server side of WebTransport over HTTP/3 as draft-ietf-webtrans-http3-02 has it and Chrome and `wtransport` 0.7 negotiate it, a layer over one `Http3Connection` whose `Http3Config.webtransportSessions` turns it on (extended CONNECT, SETTINGS_H3_DATAGRAM and the draft's two WebTransport settings): `next()` answers `WT_SESSION` (the program `accept`s or `refuse`s it), `WT_DATAGRAM`, `WT_STREAM`, `WT_STREAM_DATA`, `WT_STREAM_END`, `WT_STREAM_RESET`, `WT_STREAM_STOPPED`, `WT_WRITABLE`, `WT_DRAIN` and `WT_CLOSED`, and passes a plain request's `H3_*` events through; `sendDatagram` (the quarter stream ID, then the payload), `openStream`, `write`, `resetStream`, `stopSending`, `close` (CLOSE_WEBTRANSPORT_SESSION) and `drain`. Streams that arrive before their session wait, bounded; every cap is fixed at start-up, and a warm session allocates nothing per datagram or stream (`docs/security/webtransport.md`) | RFC 9220, RFC 9297 and draft-02: a Nish client over QUIC — sessions, datagrams at the size limits, streams both ways, close and drain from either side, every refusal — and one exchange recorded from a `wtransport` 0.7 client replayed byte for byte (`tests/link/net_webtransport`) |
204
204
  | [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
205
- | [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramNull`, `bindEnd`, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`) |
205
+ | [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first, to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
206
206
 
207
207
  **How a carrier drives `TlsServer`.** The server speaks in handshake
208
208
  *messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
@@ -1334,18 +1334,24 @@ export class Http1Connection {
1334
1334
  }
1335
1335
 
1336
1336
  /**
1337
- * The soonest `expired` can turn true, however the connection moves from
1338
- * here: `lastActive` only grows, and a head clock not yet started starts
1339
- * no earlier than it.
1337
+ * The soonest `expired` can turn true at or after `now` (`monotonicNanos`),
1338
+ * however the connection moves from here: `lastActive` only grows, and a
1339
+ * head clock not yet started starts no earlier than `now`, unless an
1340
+ * exchange is open: `nextRequest` starts a pipelined head's clock at
1341
+ * `lastActive` when it ends. Counting a quiet keep-alive's clock from
1342
+ * `lastActive` put it in the past once it was quiet for longer than
1343
+ * `headTimeout`, so each `expire` walked every slot.
1340
1344
  */
1341
- expiresNoSooner(): i64 {
1342
- const idle: i64 = toI64(this.config.idleTimeout) * toI64(1000000)
1343
- const head: i64 = toI64(this.config.headTimeout) * toI64(1000000)
1344
- const soonest: i64 = this.lastActive + (head < idle ? head : idle)
1345
- if (this.headTimed && this.headSince + head < soonest) {
1346
- return this.headSince + head
1345
+ expiresNoSooner(now: i64): i64 {
1346
+ const idle: i64 = this.lastActive + toI64(this.config.idleTimeout) * toI64(1000000)
1347
+ let start: i64 = now
1348
+ if (this.headTimed) {
1349
+ start = this.headSince
1350
+ } else if (this.requestOpen) {
1351
+ start = this.lastActive
1347
1352
  }
1348
- return soonest
1353
+ const head: i64 = start + toI64(this.config.headTimeout) * toI64(1000000)
1354
+ return head < idle ? head : idle
1349
1355
  }
1350
1356
  }
1351
1357
 
@@ -1679,8 +1685,8 @@ export class Http1Server {
1679
1685
  if (conn.expired(now)) {
1680
1686
  this.close(k)
1681
1687
  n = n + 1
1682
- } else if (conn.expiresNoSooner() < soonest) {
1683
- soonest = conn.expiresNoSooner()
1688
+ } else if (conn.expiresNoSooner(now) < soonest) {
1689
+ soonest = conn.expiresNoSooner(now)
1684
1690
  }
1685
1691
  }
1686
1692
  }
@@ -1982,8 +1988,8 @@ export class Http1TlsServer {
1982
1988
  if (conn.expired(now)) {
1983
1989
  this.tls.close(k)
1984
1990
  n = n + 1
1985
- } else if (conn.expiresNoSooner() < soonest) {
1986
- soonest = conn.expiresNoSooner()
1991
+ } else if (conn.expiresNoSooner(now) < soonest) {
1992
+ soonest = conn.expiresNoSooner(now)
1987
1993
  }
1988
1994
  }
1989
1995
  }
@@ -63,7 +63,12 @@
63
63
  * length in two reused arrays, so a row costs no allocation. `int`, `bool`,
64
64
  * `isNull` and `PgText.timestamp` decode a column in place; `PgArrayWalker`
65
65
  * walks a one-dimensional `text[]` in place. `columnText(c)` is the one that
66
- * allocates, a string.
66
+ * allocates, a string. An array parameter is a text array literal too:
67
+ * `paramIntArray` writes an `int8[]` from an `i64[]` window, and `arrayStart`,
68
+ * `arrayElement`, `arrayInt`, `arrayNull` and `arrayEnd` build an array one
69
+ * element at a time into the Bind, so a list needs no string built first. A
70
+ * program whose element was refused for room can drop the whole bind,
71
+ * unsent, with `bindCancel`.
67
72
  *
68
73
  * **Authentication.** SCRAM-SHA-256 (RFC 5802, RFC 7677) or none (`trust`).
69
74
  * The password arrives as a `Secret<u8[]>`; the connection keeps a copy in a
@@ -104,6 +109,21 @@
104
109
  * is a whole carrier: one `pollWait` loop holding the pool's sockets beside an
105
110
  * HTTP listener.
106
111
  *
112
+ * **Batching.** `PgBatcher` turns many reads of one row shape by key — a user
113
+ * by id, an article by slug — into one execution of a statement over
114
+ * `unnest($1) with ordinality`. `addInt` or `addText` queues a request's key
115
+ * and tag; `take(conn)` binds the oldest waiting, each distinct key once, up
116
+ * to `maxRequests` and `maxBytes`, and answers a run's handle; `row`, `end`
117
+ * and `fail` take that execution's events (a handle already answered makes
118
+ * them a no-op counted in `stray`, so a server's late event cannot panic),
119
+ * routing each row by its ordinal (checked first: a server's ordinal is
120
+ * refused before it routes anything), and `next(run)` answers
121
+ * `PG_BATCH_ROW`, then `PG_BATCH_DONE`, `PG_BATCH_MISSING` or
122
+ * `PG_BATCH_FAILED`, for each request. It reads no clock, and the batcher
123
+ * itself allocates only in its constructor (the connection's output buffer a
124
+ * take writes into may still grow, up to its `maxOutput`); when to take is
125
+ * the program's.
126
+ *
107
127
  * Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
108
128
  * and RFC 5802 and RFC 7677; not ported from another implementation.
109
129
  */
@@ -349,6 +369,17 @@ const pgBytesOf = (text: string): u8[] => {
349
369
  return out
350
370
  }
351
371
 
372
+ /** How many bytes `value` takes in decimal, its sign included. */
373
+ const pgDecimalLength = (value: i64): i32 => {
374
+ let n: i32 = value < 0 ? 2 : 1
375
+ let v: i64 = value < 0 ? value : -value
376
+ while (v <= -10) {
377
+ v = v / 10
378
+ n += 1
379
+ }
380
+ return n
381
+ }
382
+
352
383
  /** Whether `text` holds a NUL byte, which a protocol string cannot. */
353
384
  const pgHasNul = (text: string): boolean => {
354
385
  const n: i32 = toI32(text.length)
@@ -360,6 +391,61 @@ const pgHasNul = (text: string): boolean => {
360
391
  return false
361
392
  }
362
393
 
394
+ /**
395
+ * Whether `b[from, end)` is UTF-8 (RFC 3629: shortest forms, no surrogates,
396
+ * nothing past U+10FFFF) with no NUL: what a `text` value can hold, so that
397
+ * the server accepts it.
398
+ */
399
+ const pgIsText = (b: u8[], from: i32, end: i32): boolean => {
400
+ let i: i32 = from
401
+ while (i >= 0 && i < end && i < toI32(b.length)) {
402
+ const c: i32 = toI32(b[i])
403
+ if (c < 0x80) {
404
+ if (c === 0) {
405
+ return false
406
+ }
407
+ i += 1
408
+ continue
409
+ }
410
+ // The bytes after the lead, and the range of the first: the rest are 0x80-0xBF.
411
+ let more: i32 = 0
412
+ let low: i32 = 0x80
413
+ let high: i32 = 0xbf
414
+ if (c >= 0xc2 && c <= 0xdf) {
415
+ more = 1
416
+ } else if (c >= 0xe0 && c <= 0xef) {
417
+ more = 2
418
+ if (c === 0xe0) {
419
+ low = 0xa0
420
+ } else if (c === 0xed) {
421
+ high = 0x9f
422
+ }
423
+ } else if (c >= 0xf0 && c <= 0xf4) {
424
+ more = 3
425
+ if (c === 0xf0) {
426
+ low = 0x90
427
+ } else if (c === 0xf4) {
428
+ high = 0x8f
429
+ }
430
+ } else {
431
+ return false
432
+ }
433
+ if (more > end - i - 1) {
434
+ return false
435
+ }
436
+ for (let j: i32 = 1; j <= more && i + j < toI32(b.length); j++) {
437
+ const d: i32 = toI32(b[i + j])
438
+ if (d < low || d > high) {
439
+ return false
440
+ }
441
+ low = 0x80
442
+ high = 0xbf
443
+ }
444
+ i += more + 1
445
+ }
446
+ return true
447
+ }
448
+
363
449
  /** The first `byte` in `b[from, end)`, or `end`. */
364
450
  const pgFind = (b: u8[], from: i32, end: i32, byte: i32): i32 => {
365
451
  if (from < 0 || end > toI32(b.length)) {
@@ -775,9 +861,14 @@ export class PgConnection {
775
861
  private groupOpen: boolean = false
776
862
  /** Whether a statement in the group `PG_GROUP_DONE` just finished failed. */
777
863
  groupFailed: boolean = false
864
+ // Whether the open bind opened its group, which `bindCancel` closes again.
865
+ private bindOpened: boolean = false
778
866
  private bindAt: i32 = -1
779
867
  private paramCountAt: i32 = 0
780
868
  private paramCount: i32 = 0
869
+ // The open array parameter's length word, -1 when none is open, and its elements so far.
870
+ private arrayAt: i32 = -1
871
+ private arrayCount: i32 = 0
781
872
  private clientNonce: string = ""
782
873
  private serverSignature: u8[]
783
874
 
@@ -1516,6 +1607,9 @@ export class PgConnection {
1516
1607
  this.bindAt -= shift
1517
1608
  this.paramCountAt -= shift
1518
1609
  }
1610
+ if (this.arrayAt >= 0) {
1611
+ this.arrayAt -= shift
1612
+ }
1519
1613
  }
1520
1614
 
1521
1615
  /** Makes the output buffer at least `want` bytes long. */
@@ -1646,9 +1740,11 @@ export class PgConnection {
1646
1740
  * order, then `bindEnd()`. False as `prepare` is.
1647
1741
  */
1648
1742
  bindStart(name: string): boolean {
1743
+ const opens: boolean = !this.groupOpen
1649
1744
  if (pgHasNul(name) || toI32(name.length) > PG_MAX_CAP || !this.canOpen(11 + toI32(name.length))) {
1650
1745
  return false
1651
1746
  }
1747
+ this.bindOpened = opens
1652
1748
  this.bindAt = this.startMessage(PG_FE_BIND)
1653
1749
  this.putByte(PG_ZERO)
1654
1750
  this.putCString(name)
@@ -1665,6 +1761,7 @@ export class PgConnection {
1665
1761
  * bind has 65535 parameters already, or there is no room under `maxOutput`.
1666
1762
  */
1667
1763
  private startParam(size: i32): i32 {
1764
+ this.noArrayOpen()
1668
1765
  if (this.bindAt < 0 || this.paramCount === 65535 || size > PG_MAX_CAP || !this.room(4 + size)) {
1669
1766
  return -1
1670
1767
  }
@@ -1678,6 +1775,12 @@ export class PgConnection {
1678
1775
  setI32BE(this.out, at, this.outEnd - at - 4)
1679
1776
  }
1680
1777
 
1778
+ private noArrayOpen(): void {
1779
+ if (this.arrayAt >= 0) {
1780
+ panic("nish/net/postgres: a parameter while an array is open")
1781
+ }
1782
+ }
1783
+
1681
1784
  // Every `param...` call answers false, and writes nothing, when no bind is
1682
1785
  // open, the bind has 65535 parameters already, or the parameter would pass
1683
1786
  // `maxOutput`; a program that checked `bindStart()` meets only the last.
@@ -1695,6 +1798,7 @@ export class PgConnection {
1695
1798
 
1696
1799
  /** A text parameter from `bytes[start, start + length)`, UTF-8; false too when the window is not inside `bytes`. */
1697
1800
  paramBytes(bytes: u8[], start: i32, length: i32): boolean {
1801
+ this.noArrayOpen()
1698
1802
  if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
1699
1803
  return false
1700
1804
  }
@@ -1724,6 +1828,13 @@ export class PgConnection {
1724
1828
  if (at < 0) {
1725
1829
  return false
1726
1830
  }
1831
+ this.putDecimal(value)
1832
+ this.endParam(at)
1833
+ return true
1834
+ }
1835
+
1836
+ /** `value` in decimal, `pgDecimalLength(value)` bytes. */
1837
+ private putDecimal(value: i64): void {
1727
1838
  this.reserve(this.outEnd + 20)
1728
1839
  // The digits are written from the least significant, then reversed; the
1729
1840
  // value is made negative first so that the most negative i64 has its digits.
@@ -1750,6 +1861,39 @@ export class PgConnection {
1750
1861
  lo++
1751
1862
  hi--
1752
1863
  }
1864
+ }
1865
+
1866
+ /**
1867
+ * An `int8[]` parameter from `values[start, start + count)`, in decimal;
1868
+ * panics when the range is not inside `values`.
1869
+ */
1870
+ paramIntArray(values: i64[], start: i32, count: i32): boolean {
1871
+ if (start < 0 || count < 0 || count > toI32(values.length) - start) {
1872
+ panic("nish/net/postgres: paramIntArray() outside its array")
1873
+ }
1874
+ // Each loop is bounded by `values.length` as well as by the window's end,
1875
+ // which the guard above keeps inside it: that names the bound the compiler
1876
+ // proves `values[i]` against, where the guard alone does not reach it.
1877
+ const end: i32 = start + count
1878
+ // Its exact size first, so that room is asked for once and a refusal writes nothing.
1879
+ let size: i64 = count > 0 ? toI64(count + 1) : toI64(2)
1880
+ for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
1881
+ size += toI64(pgDecimalLength(values[i]))
1882
+ }
1883
+ const at: i32 = this.startParam(size > toI64(PG_MAX_CAP) ? PG_MAX_CAP + 1 : toI32(size))
1884
+ if (at < 0) {
1885
+ return false
1886
+ }
1887
+ this.putByte(PG_LBRACE)
1888
+ for (let i: i32 = start; i < end && i < toI32(values.length); i++) {
1889
+ // Read before the comma's call, which could drop the length fact.
1890
+ const value: i64 = values[i]
1891
+ if (i > start) {
1892
+ this.putByte(PG_COMMA)
1893
+ }
1894
+ this.putDecimal(value)
1895
+ }
1896
+ this.putByte(PG_RBRACE)
1753
1897
  this.endParam(at)
1754
1898
  return true
1755
1899
  }
@@ -1795,6 +1939,124 @@ export class PgConnection {
1795
1939
  return true
1796
1940
  }
1797
1941
 
1942
+ // An array parameter built element by element: `arrayStart()`, then
1943
+ // `arrayElement`, `arrayInt` or `arrayNull` for each element, then `arrayEnd()`. An
1944
+ // element refused for room writes nothing and leaves the array open, so a
1945
+ // program that batches rows ends the array with the elements it has and
1946
+ // sends the rest in the next statement. While an array is open, every other
1947
+ // `param...` call and `bindEnd()` panic, as do an element or `arrayEnd()`
1948
+ // with none open and a second `arrayStart()`; `sync()`, `prepare`,
1949
+ // `closeStatement` and `bindStart` answer false, the bind being open.
1950
+
1951
+ /** Opens an array parameter, `{}` so far; false as the `param...` calls are. */
1952
+ arrayStart(): boolean {
1953
+ if (this.arrayAt >= 0) {
1954
+ panic("nish/net/postgres: arrayStart() while an array is open")
1955
+ }
1956
+ // Room for `{}`: the `}` is owed, and every element keeps room for it.
1957
+ const at: i32 = this.startParam(toI32(2))
1958
+ if (at < 0) {
1959
+ return false
1960
+ }
1961
+ this.putByte(PG_LBRACE)
1962
+ this.arrayAt = at
1963
+ this.arrayCount = 0
1964
+ return true
1965
+ }
1966
+
1967
+ private arrayOpen(): void {
1968
+ if (this.arrayAt < 0) {
1969
+ panic("nish/net/postgres: an array element with no arrayStart()")
1970
+ }
1971
+ }
1972
+
1973
+ /** Whether the open array has room for an element of `size` bytes, its comma, and the `}` it owes. */
1974
+ private elementRoom(size: i64): boolean {
1975
+ const total: i64 = size + toI64(this.arrayCount > 0 ? 2 : 1)
1976
+ return total <= toI64(PG_MAX_CAP) && this.room(toI32(total))
1977
+ }
1978
+
1979
+ /**
1980
+ * A text element from `bytes[start, start + length)`, UTF-8, quoted with
1981
+ * `"` and `\` escaped; false, and nothing written, when there is no room.
1982
+ * Panics when no array is open or the window is not inside `bytes`.
1983
+ */
1984
+ arrayElement(bytes: u8[], start: i32, length: i32): boolean {
1985
+ this.arrayOpen()
1986
+ if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
1987
+ panic("nish/net/postgres: arrayElement() outside its source")
1988
+ }
1989
+ // Both loops are bounded by `bytes.length` too, as `paramIntArray`'s are.
1990
+ const end: i32 = start + length
1991
+ let escapes: i32 = 0
1992
+ for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
1993
+ const c: i32 = toI32(bytes[i])
1994
+ if (c === PG_QUOTE || c === PG_BACKSLASH) {
1995
+ escapes += 1
1996
+ }
1997
+ }
1998
+ if (!this.elementRoom(toI64(length) + toI64(escapes) + 2)) {
1999
+ return false
2000
+ }
2001
+ this.elementComma()
2002
+ this.putByte(PG_QUOTE)
2003
+ this.reserve(this.outEnd + length + escapes + 1)
2004
+ if (escapes === 0) {
2005
+ copyBytes(this.out, this.outEnd, bytes, start, length)
2006
+ this.outEnd += length
2007
+ } else {
2008
+ for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
2009
+ const c: i32 = toI32(bytes[i])
2010
+ if (c === PG_QUOTE || c === PG_BACKSLASH) {
2011
+ this.putByte(PG_BACKSLASH)
2012
+ }
2013
+ this.putByte(c)
2014
+ }
2015
+ }
2016
+ this.putByte(PG_QUOTE)
2017
+ return true
2018
+ }
2019
+
2020
+ /** A NULL element; false, and nothing written, when there is no room. Panics when no array is open. */
2021
+ arrayNull(): boolean {
2022
+ this.arrayOpen()
2023
+ if (!this.elementRoom(toI64(4))) {
2024
+ return false
2025
+ }
2026
+ this.elementComma()
2027
+ this.putString("NULL")
2028
+ return true
2029
+ }
2030
+
2031
+ private elementComma(): void {
2032
+ if (this.arrayCount > 0) {
2033
+ this.putByte(PG_COMMA)
2034
+ }
2035
+ this.arrayCount += 1
2036
+ }
2037
+
2038
+ /** An integer element, in decimal; false, and nothing written, when there is no room. Panics when no array is open. */
2039
+ arrayInt(value: i64): boolean {
2040
+ this.arrayOpen()
2041
+ if (!this.elementRoom(toI64(pgDecimalLength(value)))) {
2042
+ return false
2043
+ }
2044
+ this.elementComma()
2045
+ this.putDecimal(value)
2046
+ return true
2047
+ }
2048
+
2049
+ /** Ends the open array parameter; never refused for room, which its elements kept. Panics when no array is open. */
2050
+ arrayEnd(): void {
2051
+ if (this.arrayAt < 0) {
2052
+ panic("nish/net/postgres: arrayEnd() with no arrayStart()")
2053
+ }
2054
+ this.tailRoom(toI32(1))
2055
+ this.putByte(PG_RBRACE)
2056
+ this.endParam(this.arrayAt)
2057
+ this.arrayAt = -1
2058
+ }
2059
+
1798
2060
  /**
1799
2061
  * Ends the Bind (every result column in text) and queues its Execute, for
1800
2062
  * every row. False, and nothing written, when no bind is open; never for
@@ -1804,6 +2066,9 @@ export class PgConnection {
1804
2066
  if (this.bindAt < 0) {
1805
2067
  return false
1806
2068
  }
2069
+ if (this.arrayAt >= 0) {
2070
+ panic("nish/net/postgres: bindEnd() while an array is open")
2071
+ }
1807
2072
  this.tailRoom(PG_TAIL)
1808
2073
  const at: i32 = this.bindAt
1809
2074
  setU16BE(this.out, this.paramCountAt, toU16(this.paramCount))
@@ -1817,6 +2082,30 @@ export class PgConnection {
1817
2082
  return true
1818
2083
  }
1819
2084
 
2085
+ /**
2086
+ * Drops the open bind, and the array open in it, unsent, so that a program
2087
+ * whose parameter was refused for room can send the group without it; a
2088
+ * group the bind opened is closed again. False when no bind is open.
2089
+ */
2090
+ bindCancel(): boolean {
2091
+ if (this.bindAt < 0) {
2092
+ return false
2093
+ }
2094
+ this.dropBind()
2095
+ if (this.bindOpened) {
2096
+ this.groupOpen = false
2097
+ this.inFlight -= 1
2098
+ }
2099
+ return true
2100
+ }
2101
+
2102
+ // Drops the open bind's bytes, and the array open in it; the group is left as it is.
2103
+ private dropBind(): void {
2104
+ this.outEnd = this.bindAt
2105
+ this.bindAt = -1
2106
+ this.arrayAt = -1
2107
+ }
2108
+
1820
2109
  /** Sync: ends the open group, which the server answers with `PG_GROUP_DONE`. False when no group is open or a bind is; never for room. */
1821
2110
  sync(): boolean {
1822
2111
  if (!this.groupOpen || this.bindAt >= 0) {
@@ -1832,8 +2121,7 @@ export class PgConnection {
1832
2121
  /** Terminate: tells the server the connection is ending; close the socket once it is sent. A bind still open is dropped unsent. */
1833
2122
  terminate(): void {
1834
2123
  if (this.bindAt >= 0) {
1835
- this.outEnd = this.bindAt
1836
- this.bindAt = -1
2124
+ this.dropBind()
1837
2125
  }
1838
2126
  this.tailRoom(toI32(5))
1839
2127
  const at: i32 = this.startMessage(PG_FE_TERMINATE)
@@ -2292,3 +2580,580 @@ export class PgPool {
2292
2580
  }
2293
2581
  }
2294
2582
  }
2583
+
2584
+ // --- Batching ------------------------------------------------------------------
2585
+
2586
+ /** `PgBatchConfig.keyType`: the keys are `int8`, given to `PgBatcher.addInt`. */
2587
+ export const PG_BATCH_INT: i32 = 0
2588
+ /** `PgBatchConfig.keyType`: the keys are `text`, given to `PgBatcher.addText`. */
2589
+ export const PG_BATCH_TEXT: i32 = 1
2590
+
2591
+ /** `PgBatcher.next(run)`: no event until the next row, `end` or `fail`. */
2592
+ export const PG_BATCH_NONE: i32 = 0
2593
+ /** `PgBatcher.next(run)`: the connection's current row answers the request `tag`. */
2594
+ export const PG_BATCH_ROW: i32 = 1
2595
+ /** `PgBatcher.next(run)`: the request `tag` has had all its rows, at least one. */
2596
+ export const PG_BATCH_DONE: i32 = 2
2597
+ /** `PgBatcher.next(run)`: no row answered the request `tag`'s key. */
2598
+ export const PG_BATCH_MISSING: i32 = 3
2599
+ /** `PgBatcher.next(run)`: the request `tag`'s execution failed, and its rows so far may be incomplete. */
2600
+ export const PG_BATCH_FAILED: i32 = 4
2601
+
2602
+ // `take` probes at most maxRequests^2 / 2 slots, so the cap keeps that near 5 * 10^5;
2603
+ // a probe whose hash matches in full compares the key's bytes, so the bytes
2604
+ // compared are at most that times the longest key, which `maxBytes` bounds.
2605
+ const PG_BATCH_MAX_REQUESTS: i32 = 1024
2606
+ const PG_BATCH_MAX_QUEUE: i32 = 1048576
2607
+ const PG_BATCH_MAX_RUNS: i32 = 1024
2608
+ // The takes a run's handles count before they wrap: handles stay under 2^30.
2609
+ const PG_BATCH_USES: i32 = 1048576
2610
+ // The array's `{}` and the longest int8 element, `-9223372036854775808`.
2611
+ const PG_BATCH_MIN_BYTES: i32 = 22
2612
+
2613
+ /** What a `PgBatcher` sends: one statement shape, its key, and its caps. */
2614
+ export class PgBatchConfig {
2615
+ /** The prepared statement's name, which `PgBatcher.sql` is prepared under. */
2616
+ name: string = ""
2617
+ /** The query for one key, `q.key`: `select id, title from articles where slug = q.key`. */
2618
+ select: string = ""
2619
+ /** Columns of `t`, the query's rows, ordering the rows of one key: `t.created_at desc`; "" for none. */
2620
+ orderBy: string = ""
2621
+ /** `PG_BATCH_INT` or `PG_BATCH_TEXT`. */
2622
+ keyType: i32 = 0
2623
+ /** The most requests one execution answers. */
2624
+ maxRequests: i32 = 32
2625
+ /** The most bytes the key array parameter takes, `{}` included. */
2626
+ maxBytes: i32 = 32768
2627
+ /** The most requests waiting. */
2628
+ queueSize: i32 = 1024
2629
+ /** The most bytes of text keys waiting. */
2630
+ queueBytes: i32 = 65536
2631
+ /** The most executions in flight at once. */
2632
+ runs: i32 = 16
2633
+ }
2634
+
2635
+ /** One execution's requests, grouped by key, and how far its rows have been routed. */
2636
+ class PgBatchRun {
2637
+ /** Key `k`'s requests' tags are `tags[first[k], first[k + 1])`, in the order they were added. */
2638
+ readonly tags: i32[]
2639
+ readonly first: i32[]
2640
+ keys: i32 = 0
2641
+ /** The key rows are being read for, whether it has had one, and the next of its requests to answer. */
2642
+ cur: i32 = 0
2643
+ curRows: boolean = false
2644
+ at: i32 = 0
2645
+ /** The key whose `PG_BATCH_ROW` events are owed for the connection's current row; -1 for none. */
2646
+ target: i32 = -1
2647
+ /** The handle `take` gave for the run's execution, and how many takes it has had, mod `PG_BATCH_USES`. */
2648
+ handle: i32 = -1
2649
+ uses: i32 = 0
2650
+ open: boolean = false
2651
+ ended: boolean = false
2652
+ failed: boolean = false
2653
+
2654
+ constructor(maxRequests: i32) {
2655
+ this.tags = new Array<i32>(maxRequests)
2656
+ this.first = new Array<i32>(maxRequests + 1)
2657
+ }
2658
+
2659
+ /** Opens the run for `keys` keys, none of whose rows have been read. */
2660
+ reset(keys: i32): void {
2661
+ this.open = true
2662
+ this.keys = keys
2663
+ this.cur = 0
2664
+ this.curRows = false
2665
+ this.at = 0
2666
+ this.target = -1
2667
+ this.ended = false
2668
+ this.failed = false
2669
+ }
2670
+ }
2671
+
2672
+ /**
2673
+ * Batches keyed reads of one statement shape, sans-IO like `PgConnection`:
2674
+ * it reads no clock and makes no socket. Each request is a key and the
2675
+ * caller's `tag`, queued by `addInt` or `addText`. When a connection is free,
2676
+ * `take(conn)` binds up to `maxRequests` of them, and up to `maxBytes` of
2677
+ * keys, as one execution of `sql`, each distinct key once, and answers the
2678
+ * execution's run handle. Each `PG_ROW` of that execution goes to
2679
+ * `row(run, conn)`, its `PG_COMPLETE` to `end(run)`, and an error or a lost
2680
+ * connection to `fail(run)`; after each, `next(run)` answers events until
2681
+ * `PG_BATCH_NONE`, `tag` naming the request each is about. Every row goes to
2682
+ * every request for its key, and every request gets one last event:
2683
+ * `PG_BATCH_DONE`, `PG_BATCH_MISSING` or `PG_BATCH_FAILED`.
2684
+ *
2685
+ * Nothing outlives its execution: a run is free again once the events
2686
+ * `end` or `fail` owe are answered, and its handle is then spent. What the
2687
+ * server or the network can send after that — a second `PG_COMPLETE`, a row
2688
+ * or an error after one, a lost connection — goes to a spent handle, and
2689
+ * `row`, `end` and `fail` count it in `stray` and do nothing, even once
2690
+ * `take` has given the run out again under a new handle. The ordinal each
2691
+ * row carries comes from the server and is checked before it routes
2692
+ * anything. What panics is only the program's own misuse: a handle out of
2693
+ * the range `take` can give (-1 or any negative, or `runs` * 2^20 or more),
2694
+ * or `row`, `end` or `fail` before `next(run)` has answered `PG_BATCH_NONE`
2695
+ * for the event before. A handle in that range that `take` never gave is
2696
+ * spent like any other: counted in `stray`, not a panic.
2697
+ */
2698
+ export class PgBatcher {
2699
+ /** The prepared statement's name. */
2700
+ readonly name: string
2701
+ /** The statement to prepare under `name` (`PgPool.register` or `PgConnection.prepare`, no parameter types); each row's last column is the ordinal it routes by. */
2702
+ readonly sql: string
2703
+ // The queue, a ring: each request's tag, its key's hash and encoded size,
2704
+ // and the key: an int8, or a window of `store`, where text keys sit in the
2705
+ // order they were added, from the oldest's start to `storeEnd`.
2706
+ private readonly qTags: i32[]
2707
+ private readonly qHashes: i32[]
2708
+ private readonly qSizes: i32[]
2709
+ private readonly qInts: i64[]
2710
+ private readonly qStarts: i32[]
2711
+ private readonly qLengths: i32[]
2712
+ private readonly store: u8[]
2713
+ // `take`'s open-addressed table of the keys it has bound, each a key number
2714
+ // plus one, 0 for an empty slot; and for each key, its slot and its first request.
2715
+ private readonly table: i32[]
2716
+ private readonly keySlots: i32[]
2717
+ private readonly keyEntries: i32[]
2718
+ private readonly keyCounts: i32[]
2719
+ private readonly takenKeys: i32[]
2720
+ private readonly runs: PgBatchRun[]
2721
+ /** The request the last `next(run)` answered about; -1 for none. */
2722
+ tag: i32 = -1
2723
+ /** The number of runs failed by a bad ordinal (missing, NULL, not an integer, or naming no key still open), stopping at 2^31 - 1: each counts its first refused row, and the rows after it, dropped with the failed run, are not counted. */
2724
+ refused: i32 = 0
2725
+ /** The number of `row`, `end` and `fail` calls on a spent handle, each ignored, stopping at 2^31 - 1. */
2726
+ stray: i32 = 0
2727
+ private readonly keyType: i32
2728
+ private readonly maxRequests: i32
2729
+ private readonly maxBytes: i32
2730
+ private storeEnd: i32 = 0
2731
+ private head: i32 = 0
2732
+ private count: i32 = 0
2733
+
2734
+ /** A batcher for `config`; panics when a name or query is empty or holds a NUL, or a cap is out of range. */
2735
+ constructor(config: PgBatchConfig) {
2736
+ const keyType: i32 = config.keyType
2737
+ if (keyType !== PG_BATCH_INT && keyType !== PG_BATCH_TEXT) {
2738
+ panic("nish/net/postgres: a PgBatchConfig keyType out of range")
2739
+ }
2740
+ if (
2741
+ config.name.length === 0 ||
2742
+ config.select.length === 0 ||
2743
+ pgHasNul(config.name) ||
2744
+ pgHasNul(config.select) ||
2745
+ pgHasNul(config.orderBy)
2746
+ ) {
2747
+ panic("nish/net/postgres: a PgBatchConfig name or query empty or with a NUL")
2748
+ }
2749
+ const maxRequests: i32 = config.maxRequests
2750
+ if (
2751
+ maxRequests < 1 ||
2752
+ maxRequests > PG_BATCH_MAX_REQUESTS ||
2753
+ config.maxBytes < PG_BATCH_MIN_BYTES ||
2754
+ config.maxBytes > PG_MAX_CAP ||
2755
+ config.queueSize < 1 ||
2756
+ config.queueSize > PG_BATCH_MAX_QUEUE ||
2757
+ config.queueBytes < 0 ||
2758
+ config.queueBytes > PG_MAX_CAP ||
2759
+ config.runs < 1 ||
2760
+ config.runs > PG_BATCH_MAX_RUNS
2761
+ ) {
2762
+ panic("nish/net/postgres: a PgBatchConfig cap out of range")
2763
+ }
2764
+ this.keyType = keyType
2765
+ this.maxRequests = maxRequests
2766
+ this.maxBytes = config.maxBytes
2767
+ this.name = config.name
2768
+ const type: string = keyType === PG_BATCH_INT ? "int8" : "text"
2769
+ const order: string = config.orderBy.length > 0 ? `, ${config.orderBy}` : ""
2770
+ this.sql = `select t.*, q.ord from unnest($1::${type}[]) with ordinality as q(key, ord) cross join lateral (${config.select}) as t order by q.ord${order}`
2771
+ const queueSize: i32 = config.queueSize
2772
+ this.qTags = new Array<i32>(queueSize)
2773
+ this.qHashes = new Array<i32>(queueSize)
2774
+ this.qSizes = new Array<i32>(queueSize)
2775
+ const text: boolean = keyType === PG_BATCH_TEXT
2776
+ const intSize: i32 = text ? 0 : queueSize
2777
+ const textSize: i32 = text ? queueSize : 0
2778
+ const storeSize: i32 = text ? config.queueBytes : 0
2779
+ this.qInts = new Array<i64>(intSize)
2780
+ this.qStarts = new Array<i32>(textSize)
2781
+ this.qLengths = new Array<i32>(textSize)
2782
+ this.store = new Array<u8>(storeSize)
2783
+ // At least twice the keys a take can hold, a power of two for the mask.
2784
+ let tableSize: i32 = 2
2785
+ while (tableSize < 2 * maxRequests) {
2786
+ tableSize *= 2
2787
+ }
2788
+ this.table = new Array<i32>(tableSize)
2789
+ this.keySlots = new Array<i32>(maxRequests)
2790
+ this.keyEntries = new Array<i32>(maxRequests)
2791
+ this.keyCounts = new Array<i32>(maxRequests)
2792
+ this.takenKeys = new Array<i32>(maxRequests)
2793
+ this.runs = []
2794
+ for (let r: i32 = 0; r < config.runs; r++) {
2795
+ this.runs.push(new PgBatchRun(maxRequests))
2796
+ }
2797
+ }
2798
+
2799
+ /** The number of requests waiting for `take`. */
2800
+ waiting(): i32 {
2801
+ return this.count
2802
+ }
2803
+
2804
+ /** Queues a request for the int8 `key`; false when `queueSize` are waiting. Panics on a text batcher. */
2805
+ addInt(key: i64, tag: i32): boolean {
2806
+ if (this.keyType !== PG_BATCH_INT) {
2807
+ panic("nish/net/postgres: PgBatcher.addInt on a text batcher")
2808
+ }
2809
+ const q: i32 = this.slotToAdd()
2810
+ if (q < 0) {
2811
+ return false
2812
+ }
2813
+ // Both halves folded to 31 bits, so that no step can overflow.
2814
+ const h: i32 = toI32(key & toI64(0x7fffffff)) ^ toI32((key >> toI64(31)) & toI64(0x7fffffff))
2815
+ this.qInts[q] = key
2816
+ this.added(q, tag, h ^ (h >> 15), pgDecimalLength(key))
2817
+ return true
2818
+ }
2819
+
2820
+ /**
2821
+ * Queues a request for the text key `bytes[start, start + length)`,
2822
+ * which is copied; false when the key is not UTF-8 or holds a NUL (the
2823
+ * server would fail the whole execution over it), `queueSize` are waiting,
2824
+ * the key would not fit in `queueBytes` beside those waiting, or it is too
2825
+ * long for one execution's `maxBytes`. Panics on an int8 batcher, or when
2826
+ * the window is not inside `bytes`.
2827
+ */
2828
+ addText(bytes: u8[], start: i32, length: i32, tag: i32): boolean {
2829
+ if (this.keyType !== PG_BATCH_TEXT) {
2830
+ panic("nish/net/postgres: PgBatcher.addText on an int8 batcher")
2831
+ }
2832
+ if (start < 0 || length < 0 || length > toI32(bytes.length) - start) {
2833
+ panic("nish/net/postgres: PgBatcher.addText outside its source")
2834
+ }
2835
+ const end: i32 = start + length
2836
+ if (!pgIsText(bytes, start, end)) {
2837
+ return false
2838
+ }
2839
+ // The element as `arrayElement` writes it: quoted, `"` and `\` escaped;
2840
+ // and FNV-1a, whose unsigned multiply wraps by definition.
2841
+ let size: i64 = toI64(length) + 2
2842
+ let h: u32 = 2166136261
2843
+ for (let i: i32 = start; i < end && i < toI32(bytes.length); i++) {
2844
+ const c: u8 = bytes[i]
2845
+ if (toI32(c) === PG_QUOTE || toI32(c) === PG_BACKSLASH) {
2846
+ size += 1
2847
+ }
2848
+ h = (h ^ toU32(c)) * 16777619
2849
+ }
2850
+ if (size + 2 > toI64(this.maxBytes)) {
2851
+ return false
2852
+ }
2853
+ const q: i32 = this.slotToAdd()
2854
+ if (q < 0) {
2855
+ return false
2856
+ }
2857
+ let at: i32 = this.storeEnd
2858
+ if (length > toI32(this.store.length) - at) {
2859
+ // The waiting keys are moved to the front, the room the taken ones left after them.
2860
+ // Refused first when even that would not make room, so a full store is not moved for nothing.
2861
+ const from: i32 = this.count > 0 ? this.qStarts[this.head] : at
2862
+ if (length > toI32(this.store.length) - (at - from)) {
2863
+ return false
2864
+ }
2865
+ copyBytes(this.store, 0, this.store, from, at - from)
2866
+ for (let i: i32 = 0; i < this.count; i++) {
2867
+ const w: i32 = (this.head + i) % toI32(this.qStarts.length)
2868
+ this.qStarts[w] -= from
2869
+ }
2870
+ at -= from
2871
+ this.storeEnd = at
2872
+ }
2873
+ copyBytes(this.store, at, bytes, start, length)
2874
+ this.storeEnd = at + length
2875
+ this.qStarts[q] = at
2876
+ this.qLengths[q] = length
2877
+ this.added(q, tag, toI32(h >> 1), toI32(size))
2878
+ return true
2879
+ }
2880
+
2881
+ /** The ring slot the next request goes in, or -1 when the queue is full. */
2882
+ private slotToAdd(): i32 {
2883
+ const size: i32 = toI32(this.qTags.length)
2884
+ return this.count === size ? -1 : (this.head + this.count) % size
2885
+ }
2886
+
2887
+ private added(q: i32, tag: i32, hash: i32, size: i32): void {
2888
+ this.qTags[q] = tag
2889
+ this.qHashes[q] = hash
2890
+ this.qSizes[q] = size
2891
+ this.count += 1
2892
+ }
2893
+
2894
+ /** Whether the keys of the queued requests `a` and `b` are equal. */
2895
+ private sameKey(a: i32, b: i32): boolean {
2896
+ if (this.keyType === PG_BATCH_INT) {
2897
+ return this.qInts[a] === this.qInts[b]
2898
+ }
2899
+ const n: i32 = this.qLengths[a]
2900
+ if (n !== this.qLengths[b]) {
2901
+ return false
2902
+ }
2903
+ const x: i32 = this.qStarts[a]
2904
+ const y: i32 = this.qStarts[b]
2905
+ for (let i: i32 = 0; i < n; i++) {
2906
+ if (this.store[x + i] !== this.store[y + i]) {
2907
+ return false
2908
+ }
2909
+ }
2910
+ return true
2911
+ }
2912
+
2913
+ /**
2914
+ * Binds the oldest waiting requests on `conn` as one execution of `sql`,
2915
+ * prepared there already, and answers its run's handle, which names this
2916
+ * execution and no later one of the same run; the caller ends the group
2917
+ * (`sync()`, or `PgPool.send()`). It takes up to `maxRequests` requests,
2918
+ * oldest first, and stops at the first whose key would pass `maxBytes` or
2919
+ * the connection's `maxOutput`; those not taken stay queued. -1, and
2920
+ * nothing written, when none waits, every run is in flight, or `conn`
2921
+ * refused the bind or the first key.
2922
+ */
2923
+ take(conn: PgConnection): i32 {
2924
+ if (this.count === 0) {
2925
+ return -1
2926
+ }
2927
+ let r: i32 = -1
2928
+ for (let i: i32 = 0; i < toI32(this.runs.length); i++) {
2929
+ if (!this.runs[i].open) {
2930
+ r = i
2931
+ break
2932
+ }
2933
+ }
2934
+ if (r < 0 || !conn.bindStart(this.name)) {
2935
+ return -1
2936
+ }
2937
+ if (!conn.arrayStart()) {
2938
+ conn.bindCancel()
2939
+ return -1
2940
+ }
2941
+ const ring: i32 = toI32(this.qTags.length)
2942
+ const mask: i32 = toI32(this.table.length) - 1
2943
+ let keys: i32 = 0
2944
+ let taken: i32 = 0
2945
+ let bytes: i32 = 2
2946
+ while (taken < this.count && taken < this.maxRequests && taken < toI32(this.takenKeys.length)) {
2947
+ const q: i32 = (this.head + taken) % ring
2948
+ // Probe for the key: a slot holding it, or the empty slot it would take.
2949
+ let slot: i32 = this.qHashes[q] & mask
2950
+ let k: i32 = -1
2951
+ while (this.table[slot] > 0) {
2952
+ const seen: i32 = this.table[slot] - 1
2953
+ const e: i32 = this.keyEntries[seen]
2954
+ if (this.qHashes[e] === this.qHashes[q] && this.sameKey(e, q)) {
2955
+ k = seen
2956
+ break
2957
+ }
2958
+ slot = (slot + 1) & mask
2959
+ }
2960
+ if (k < 0) {
2961
+ const cost: i32 = this.qSizes[q] + (keys > 0 ? 1 : 0)
2962
+ if (cost > this.maxBytes - bytes || !this.bindKey(conn, q)) {
2963
+ break
2964
+ }
2965
+ bytes += cost
2966
+ k = keys
2967
+ keys += 1
2968
+ this.table[slot] = keys
2969
+ this.keySlots[k] = slot
2970
+ this.keyEntries[k] = q
2971
+ this.keyCounts[k] = 0
2972
+ }
2973
+ this.keyCounts[k] += 1
2974
+ this.takenKeys[taken] = k
2975
+ taken += 1
2976
+ }
2977
+ for (let k: i32 = 0; k < keys && k < toI32(this.keySlots.length); k++) {
2978
+ this.table[this.keySlots[k]] = 0
2979
+ }
2980
+ if (taken === 0) {
2981
+ conn.bindCancel()
2982
+ return -1
2983
+ }
2984
+ conn.arrayEnd()
2985
+ conn.bindEnd()
2986
+ // The requests grouped by key, each key's in the order they were added.
2987
+ const run: PgBatchRun = this.runs[r]
2988
+ run.first[0] = 0
2989
+ for (let k: i32 = 0; k < keys && k < toI32(this.keyCounts.length); k++) {
2990
+ run.first[k + 1] = run.first[k] + this.keyCounts[k]
2991
+ this.keyCounts[k] = run.first[k]
2992
+ }
2993
+ for (let i: i32 = 0; i < taken && i < toI32(this.takenKeys.length); i++) {
2994
+ const k: i32 = this.takenKeys[i]
2995
+ run.tags[this.keyCounts[k]] = this.qTags[(this.head + i) % ring]
2996
+ this.keyCounts[k] += 1
2997
+ }
2998
+ run.reset(keys)
2999
+ run.handle = run.uses * toI32(this.runs.length) + r
3000
+ run.uses = (run.uses + 1) % PG_BATCH_USES
3001
+ this.head = (this.head + taken) % ring
3002
+ this.count -= taken
3003
+ if (this.count === 0) {
3004
+ this.storeEnd = 0
3005
+ }
3006
+ return run.handle
3007
+ }
3008
+
3009
+ /** Writes the queued request `q`'s key as the open array's next element; false when `conn` has no room. */
3010
+ private bindKey(conn: PgConnection, q: i32): boolean {
3011
+ if (this.keyType === PG_BATCH_INT) {
3012
+ return conn.arrayInt(this.qInts[q])
3013
+ }
3014
+ return conn.arrayElement(this.store, this.qStarts[q], this.qLengths[q])
3015
+ }
3016
+
3017
+ /** The run handle `h` names while its execution's events are owed; null once it is spent. Panics on a handle `take` cannot give. */
3018
+ private run(h: i32): PgBatchRun | null {
3019
+ const n: i32 = toI32(this.runs.length)
3020
+ if (h < 0 || h >= n * PG_BATCH_USES) {
3021
+ panic("nish/net/postgres: a PgBatcher run out of range")
3022
+ }
3023
+ const run: PgBatchRun = this.runs[h % n]
3024
+ if (run.handle !== h || !run.open) {
3025
+ return null
3026
+ }
3027
+ return run
3028
+ }
3029
+
3030
+ /** The run handle `h` names, not ended and with its events all answered; null, counted in `stray`, once it is spent. */
3031
+ private quietRun(h: i32, what: string): PgBatchRun | null {
3032
+ const run: PgBatchRun | null = this.run(h)
3033
+ if (run === null) {
3034
+ if (this.stray < 2147483647) {
3035
+ this.stray += 1
3036
+ }
3037
+ return null
3038
+ }
3039
+ if (run.target >= 0 || run.ended) {
3040
+ panic(`nish/net/postgres: PgBatcher.${what} before next(run) answered PG_BATCH_NONE`)
3041
+ }
3042
+ return run
3043
+ }
3044
+
3045
+ /**
3046
+ * Routes `conn`'s current row, of run `r`'s execution, by the ordinal in
3047
+ * its last column: `next(r)` then answers `PG_BATCH_ROW` for each request
3048
+ * of its key, after the last events of the keys before it. False, and the
3049
+ * row dropped, when the run has failed; and when the ordinal is NULL, not
3050
+ * an integer, or names no key still open (one out of range, or one before
3051
+ * the key the last row was for): that counts in `refused` and fails the
3052
+ * run; and on a spent handle, which counts in `stray`. Panics on a handle
3053
+ * `take` cannot give, or when the run's events are not all answered.
3054
+ */
3055
+ row(r: i32, conn: PgConnection): boolean {
3056
+ const run: PgBatchRun | null = this.quietRun(r, "row")
3057
+ if (run === null) {
3058
+ return false
3059
+ }
3060
+ if (run.failed) {
3061
+ return false
3062
+ }
3063
+ const last: i32 = conn.columns - 1
3064
+ let ordinal: i64 = 0
3065
+ let bad: boolean = last < 0 || conn.isNull(last)
3066
+ if (!bad) {
3067
+ // Read without touching `text.failed`, which belongs to the program's own decoding.
3068
+ const failed: boolean = conn.text.failed
3069
+ conn.text.failed = false
3070
+ ordinal = conn.int(last)
3071
+ bad = conn.text.failed
3072
+ conn.text.failed = failed
3073
+ }
3074
+ // Ordinals count from 1 and come in order: a row may name the current key again, never one before it.
3075
+ if (bad || ordinal < toI64(run.cur) + 1 || ordinal > toI64(run.keys)) {
3076
+ if (this.refused < 2147483647) {
3077
+ this.refused += 1
3078
+ }
3079
+ run.failed = true
3080
+ return false
3081
+ }
3082
+ run.target = toI32(ordinal) - 1
3083
+ return true
3084
+ }
3085
+
3086
+ /** Ends run `r`'s execution, on its `PG_COMPLETE`: `next(r)` then closes every request left. Ignores a spent handle, and panics, as `row` does. */
3087
+ end(r: i32): void {
3088
+ const run: PgBatchRun | null = this.quietRun(r, "end")
3089
+ if (run !== null) {
3090
+ run.ended = true
3091
+ }
3092
+ }
3093
+
3094
+ /** Fails run `r`'s execution, on its `PG_ERROR` or `PG_LOST`: `next(r)` then answers `PG_BATCH_FAILED` for every request left. Ignores a spent handle, and panics, as `row` does. */
3095
+ fail(r: i32): void {
3096
+ const run: PgBatchRun | null = this.quietRun(r, "fail")
3097
+ if (run !== null) {
3098
+ run.failed = true
3099
+ run.ended = true
3100
+ }
3101
+ }
3102
+
3103
+ // Names the current key's next unanswered request as `tag`; false when it has none left.
3104
+ private emit(run: PgBatchRun): boolean {
3105
+ const at: i32 = run.first[run.cur] + run.at
3106
+ if (at >= run.first[run.cur + 1]) {
3107
+ return false
3108
+ }
3109
+ run.at += 1
3110
+ this.tag = run.tags[at]
3111
+ return true
3112
+ }
3113
+
3114
+ /**
3115
+ * The next event `row`, `end` or `fail` owes on run `r`, with `tag` the
3116
+ * request it is about; `PG_BATCH_NONE`, and `tag` -1, when there is none.
3117
+ * Once `end` or `fail` has had all its events the run is over, free for
3118
+ * `take` again, and its handle spent: `PG_BATCH_NONE` ever after. Panics
3119
+ * on a handle `take` cannot give.
3120
+ */
3121
+ next(r: i32): i32 {
3122
+ const found: PgBatchRun | null = this.run(r)
3123
+ this.tag = -1
3124
+ if (found === null) {
3125
+ return PG_BATCH_NONE
3126
+ }
3127
+ const run: PgBatchRun = found
3128
+ // The keys before the row's, or every key once the execution is over, have had all their rows.
3129
+ let limit: i32 = run.cur
3130
+ if (run.target >= 0) {
3131
+ limit = run.target
3132
+ } else if (run.ended) {
3133
+ limit = run.keys
3134
+ }
3135
+ while (run.cur < limit && run.cur < toI32(run.tags.length)) {
3136
+ if (this.emit(run)) {
3137
+ if (run.failed) {
3138
+ return PG_BATCH_FAILED
3139
+ }
3140
+ return run.curRows ? PG_BATCH_DONE : PG_BATCH_MISSING
3141
+ }
3142
+ run.cur += 1
3143
+ run.at = 0
3144
+ run.curRows = false
3145
+ }
3146
+ if (run.target >= 0 && run.cur < toI32(run.tags.length)) {
3147
+ run.curRows = true
3148
+ if (this.emit(run)) {
3149
+ return PG_BATCH_ROW
3150
+ }
3151
+ run.target = -1
3152
+ run.at = 0
3153
+ }
3154
+ if (run.ended && run.cur >= run.keys) {
3155
+ run.open = false
3156
+ }
3157
+ return PG_BATCH_NONE
3158
+ }
3159
+ }