@mark1russell7/harness 0.11.0 → 0.11.2

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.
@@ -0,0 +1,11 @@
1
+ # The harness's PostgreSQL client: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0. It
2
+ # runs as a container in the client's network namespace, as the harness's own TCP client does, and
3
+ # prints the same JSON lines, with the events of the pool beside them.
4
+ # lab.ts stages the build context: this folder, under harness/image/pg. The lockfile is
5
+ # npm-lock.json here, because npm leaves each package-lock.json out of a published tarball.
6
+ FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1
7
+ WORKDIR /opt/harness/pg
8
+ COPY harness/image/pg/package.json ./
9
+ COPY harness/image/pg/npm-lock.json ./package-lock.json
10
+ RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
11
+ COPY harness/image/pg/client-pg.mjs ./
@@ -0,0 +1,162 @@
1
+ // client-pg HOST PORT --mode idle|inflight|stream [--interval-ms N] [--max-s N] [--go PATH]
2
+ // [--query TEXT] [--before] [--options JSON]
3
+ //
4
+ // The client of a PostgreSQL cell: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0.
5
+ // Each event is a JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds
6
+ // (process.hrtime, which libuv reads from it). The prober stamps the records of the kernel on the
7
+ // same clock, and the injector stamps each fault on it.
8
+ //
9
+ // The lines:
10
+ // - start: the versions of pg, pg-pool and Node, the mode and the options of the pool.
11
+ // - socket, socket-end, socket-error: each socket of the pool, with its local address.
12
+ // - connected: the first socket. The TCP client of the harness names its own socket so.
13
+ // - pool: each event of the pool: connect, acquire, release, remove, and error with its errno.
14
+ // - ready: the reply to the first query: the connection is idle in the pool (idle, inflight), or
15
+ // the stream starts (stream).
16
+ // - sent, ack, fail: each query by its sequence number, with its error.
17
+ // - data, error: the end of the query in flight, as the TCP client of the harness tells it. In idle
18
+ // mode, an error of the pool for the first socket is an error line too: the pool removes it.
19
+ // - done, deadline: the end of the client.
20
+ //
21
+ // The modes: idle sends one query, gives the client back to the pool, then holds until --max-s.
22
+ // inflight sends one query, waits for the file --go, sends one more query into the fault, and holds
23
+ // after its end. stream sends a query each --interval-ms after the reply before, until --max-s or
24
+ // SIGTERM. --before starts the client into a fault that holds already: its first connect meets it.
25
+
26
+ import { readFileSync } from "node:fs";
27
+ import { existsSync } from "node:fs";
28
+
29
+ const now = () => process.hrtime.bigint();
30
+ const emit = (ev, fields = {}, at = now()) => process.stdout.write(`${JSON.stringify({ ev, t: Number(at), ...fields })}\n`);
31
+
32
+ const argv = process.argv.slice(2);
33
+ const flag = (name) => {
34
+ const at = argv.indexOf(name);
35
+ return at === -1 ? undefined : argv[at + 1];
36
+ };
37
+ const [host, port] = argv;
38
+ const mode = flag("--mode");
39
+ if (host === undefined || port === undefined || !["idle", "inflight", "stream"].includes(mode)) {
40
+ emit("fail", { why: "client-pg HOST PORT --mode idle|inflight|stream" });
41
+ process.exit(1);
42
+ }
43
+ const interval = Number(flag("--interval-ms") ?? 50);
44
+ const max = Number(flag("--max-s") ?? 120) * 1000;
45
+ const go = flag("--go") ?? "/tmp/go";
46
+ const query = flag("--query") ?? "SELECT 1";
47
+ const before = argv.includes("--before");
48
+ // the options of the pool. A cell gives each option that it changes, and pg-pool gives the others
49
+ const options = JSON.parse(flag("--options") ?? "{}");
50
+
51
+ const { default: pg } = await import("pg");
52
+ const versionOf = (name) => JSON.parse(readFileSync(new URL(`./node_modules/${name}/package.json`, import.meta.url), "utf8")).version;
53
+
54
+ const bootId = () => {
55
+ try {
56
+ return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
57
+ } catch {
58
+ return null;
59
+ }
60
+ };
61
+ emit("start", { boot_id: bootId(), client: "pg", driver: versionOf("pg"), pool: versionOf("pg-pool"), node: process.versions.node, mode, query, options });
62
+
63
+ const pool = new pg.Pool({ host, port: Number(port), user: "postgres", database: "postgres", ...options });
64
+
65
+ /** This function gives the error of pg as data: its name, its code, its severity and its message. */
66
+ const failure = (error) =>
67
+ error === undefined || error === null
68
+ ? null
69
+ : { name: error.name, code: error.code ?? null, errno: error.errno ?? null, severity: error.severity ?? null, message: error.message };
70
+ /** This function gives the errno of an error: the code of the socket (ECONNRESET), the SQLSTATE of the server, or the message of pg. */
71
+ const errnoOf = (error) => error?.code ?? error?.message ?? null;
72
+
73
+ // ---- each socket of the pool: pg-pool emits connect after the first ReadyForQuery of a new client
74
+ let connected = null;
75
+ const localOf = (client) => {
76
+ const socket = client?.connection?.stream;
77
+ return socket?.localPort === undefined ? null : `${socket.localAddress}:${socket.localPort}`;
78
+ };
79
+ pool.on("connect", (client) => {
80
+ const socket = client.connection.stream;
81
+ const local = localOf(client);
82
+ emit("socket", { role: "pool", local });
83
+ emit("pool", { event: "connect", local });
84
+ if (connected === null && local !== null) {
85
+ connected = local;
86
+ emit("connected", { local });
87
+ }
88
+ socket.once("close", (hadError) => emit("socket-end", { role: "pool", local, error: hadError }));
89
+ socket.once("error", (error) => emit("socket-error", { role: "pool", local, errno: errnoOf(error), message: error.message }));
90
+ });
91
+ pool.on("acquire", (client) => emit("pool", { event: "acquire", local: localOf(client) }));
92
+ pool.on("release", (error, client) => emit("pool", { event: "release", local: localOf(client), failure: failure(error) }));
93
+ // the pool removes a client: after its idle timer, its lifetime or an error. A driver band names the close CLOSED
94
+ pool.on("remove", (client) => emit("pool", { event: "remove", name: "CLOSED", local: localOf(client) }));
95
+ // an idle client that fails: the pool removes it and emits error. Without this listener, Node throws
96
+ pool.on("error", (error, client) => {
97
+ const local = localOf(client);
98
+ emit("pool", { event: "error", local, failure: failure(error) });
99
+ // the socket of the client in idle mode: its error is the detection, as for the TCP client
100
+ if (mode === "idle" && local !== null && local === connected) emit("error", { errno: errnoOf(error) });
101
+ });
102
+
103
+ // ---- the end of the client
104
+ let stopped = false;
105
+ const stop = (code = 0) => {
106
+ if (stopped) return;
107
+ stopped = true;
108
+ emit("done");
109
+ pool.end().finally(() => setTimeout(() => process.exit(code), 50));
110
+ };
111
+ process.on("SIGTERM", () => stop(0));
112
+ setTimeout(() => {
113
+ emit("deadline");
114
+ stop(0);
115
+ }, max);
116
+
117
+ /** This function sends query n through the pool. Its stamp is before the call. It gives null for a reply, or the error. */
118
+ const operate = async (n) => {
119
+ const at = now();
120
+ const reply = pool.query(query);
121
+ emit("sent", { n }, at);
122
+ try {
123
+ const result = await reply;
124
+ emit("ack", { n, rows: result.rowCount ?? null });
125
+ return null;
126
+ } catch (error) {
127
+ emit("fail", { n, ...failure(error) });
128
+ return error;
129
+ }
130
+ };
131
+
132
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
133
+
134
+ if (mode === "stream") {
135
+ emit("ready", { at: `${host}:${port}` });
136
+ for (let n = 1; !stopped; n += 1) {
137
+ await operate(n);
138
+ await sleep(interval);
139
+ }
140
+ } else {
141
+ // the first query makes the connection of the pool. Its reply is the last data before the fault
142
+ const failed = await operate(1);
143
+ if (failed !== null) {
144
+ emit("error", { errno: errnoOf(failed), message: failed.message });
145
+ stop(before ? 0 : 1);
146
+ } else {
147
+ // pool.query gives the client back to the pool at its reply: the connection is idle there
148
+ emit("ready", { reply: "ack" });
149
+ if (mode === "inflight") {
150
+ await new Promise((resolve) => {
151
+ const wait = setInterval(() => {
152
+ if (!existsSync(go)) return;
153
+ clearInterval(wait);
154
+ resolve();
155
+ }, 2);
156
+ });
157
+ const error = await operate(2);
158
+ if (error === null) emit("data", { n: 2 });
159
+ else emit("error", { errno: errnoOf(error), message: error.message });
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,160 @@
1
+ {
2
+ "name": "loom-harness-pg-client",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "loom-harness-pg-client",
8
+ "dependencies": {
9
+ "pg": "8.23.1",
10
+ "pg-pool": "3.14.0"
11
+ }
12
+ },
13
+ "node_modules/pg": {
14
+ "version": "8.23.1",
15
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz",
16
+ "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "pg-connection-string": "^2.14.1",
20
+ "pg-pool": "^3.14.0",
21
+ "pg-protocol": "^1.16.1",
22
+ "pg-types": "2.2.0",
23
+ "pgpass": "1.0.5"
24
+ },
25
+ "engines": {
26
+ "node": ">= 16.0.0"
27
+ },
28
+ "optionalDependencies": {
29
+ "pg-cloudflare": "^1.4.1"
30
+ },
31
+ "peerDependencies": {
32
+ "pg-native": ">=3.0.1"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "pg-native": {
36
+ "optional": true
37
+ }
38
+ }
39
+ },
40
+ "node_modules/pg-cloudflare": {
41
+ "version": "1.4.1",
42
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz",
43
+ "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==",
44
+ "license": "MIT",
45
+ "optional": true
46
+ },
47
+ "node_modules/pg-connection-string": {
48
+ "version": "2.14.1",
49
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz",
50
+ "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==",
51
+ "license": "MIT"
52
+ },
53
+ "node_modules/pg-int8": {
54
+ "version": "1.0.1",
55
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
56
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
57
+ "license": "ISC",
58
+ "engines": {
59
+ "node": ">=4.0.0"
60
+ }
61
+ },
62
+ "node_modules/pg-pool": {
63
+ "version": "3.14.0",
64
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
65
+ "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
66
+ "license": "MIT",
67
+ "peerDependencies": {
68
+ "pg": ">=8.0"
69
+ }
70
+ },
71
+ "node_modules/pg-protocol": {
72
+ "version": "1.16.1",
73
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz",
74
+ "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==",
75
+ "license": "MIT"
76
+ },
77
+ "node_modules/pg-types": {
78
+ "version": "2.2.0",
79
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
80
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
81
+ "license": "MIT",
82
+ "dependencies": {
83
+ "pg-int8": "1.0.1",
84
+ "postgres-array": "~2.0.0",
85
+ "postgres-bytea": "~1.0.0",
86
+ "postgres-date": "~1.0.4",
87
+ "postgres-interval": "^1.1.0"
88
+ },
89
+ "engines": {
90
+ "node": ">=4"
91
+ }
92
+ },
93
+ "node_modules/pgpass": {
94
+ "version": "1.0.5",
95
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
96
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
97
+ "license": "MIT",
98
+ "dependencies": {
99
+ "split2": "^4.1.0"
100
+ }
101
+ },
102
+ "node_modules/postgres-array": {
103
+ "version": "2.0.0",
104
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
105
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
106
+ "license": "MIT",
107
+ "engines": {
108
+ "node": ">=4"
109
+ }
110
+ },
111
+ "node_modules/postgres-bytea": {
112
+ "version": "1.0.1",
113
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
114
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
115
+ "license": "MIT",
116
+ "engines": {
117
+ "node": ">=0.10.0"
118
+ }
119
+ },
120
+ "node_modules/postgres-date": {
121
+ "version": "1.0.7",
122
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
123
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
124
+ "license": "MIT",
125
+ "engines": {
126
+ "node": ">=0.10.0"
127
+ }
128
+ },
129
+ "node_modules/postgres-interval": {
130
+ "version": "1.2.0",
131
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
132
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
133
+ "license": "MIT",
134
+ "dependencies": {
135
+ "xtend": "^4.0.0"
136
+ },
137
+ "engines": {
138
+ "node": ">=0.10.0"
139
+ }
140
+ },
141
+ "node_modules/split2": {
142
+ "version": "4.2.0",
143
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
144
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
145
+ "license": "ISC",
146
+ "engines": {
147
+ "node": ">= 10.x"
148
+ }
149
+ },
150
+ "node_modules/xtend": {
151
+ "version": "4.0.2",
152
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
153
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
154
+ "license": "MIT",
155
+ "engines": {
156
+ "node": ">=0.4"
157
+ }
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "loom-harness-pg-client",
3
+ "private": true,
4
+ "type": "module",
5
+ "dependencies": {
6
+ "pg": "8.23.1",
7
+ "pg-pool": "3.14.0"
8
+ }
9
+ }
@@ -1,8 +1,9 @@
1
- # The procedures image of the harness: @procedures/client 0.3.0 and ws 8.22.0 on Node 24.21.0. Its WebSocket
1
+ # The procedures image of the harness: @procedures/client 0.7.0 and ws 8.22.0 on Node 24.21.0. Its WebSocket
2
2
  # client runs as a container in the client's network namespace, as the harness's own TCP client
3
3
  # does, and prints the same JSON lines (client-ws.mjs). A target runs the same image with the
4
4
  # script of the procedures server (server-ws.mjs), thus the client and the server share one
5
- # release of the package. The digest of the image and an npm lockfile pin them.
5
+ # release of the package. The digest of the image and an npm lockfile pin them. The lockfile
6
+ # holds one copy of ws, thus @procedures/client/node and the scripts use one ws class.
6
7
  # lab.ts stages the build context: this folder, under harness/image/procedures. The lockfile is
7
8
  # npm-lock.json here, because npm leaves each package-lock.json out of a published tarball.
8
9
  FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1
@@ -4,13 +4,18 @@
4
4
  // --reconnect-attempts N --reconnect-jitter X]
5
5
  // --request-timeout-ms N --connection-timeout-ms N [--hold-ms N] [--go PATH] [--max-s N]
6
6
  //
7
- // A cell's client: the WebSocket client of @procedures/client 0.3.0 (WebSocketTransport) on Node
8
- // 24.21.0. Its sockets are of the global WebSocket of Node, which is undici 7.29.1, or of the
9
- // WebSocket of the ws package (the option `webSocket`): --websocket. Each setting of the transport
10
- // is a flag, and the client has no default: a missing flag is a failure. 0.3.0 takes each time as a
11
- // duration, not a number. Both modes call `harness.echo` first; its reply is `ready`. idle: then
12
- // hold the connection. inflight: then wait for the file --go, and call `harness.echo` (or
13
- // `harness.hold` for --hold-ms) into the fault.
7
+ // A cell's client: the WebSocket client of @procedures/client 0.7.0 (WebSocketTransport) on Node
8
+ // 24.21.0. --websocket gives the class of its sockets:
9
+ // global the WebSocketTransport of the root entry, whose default class is the global WebSocket
10
+ // of Node: undici 7.29.1.
11
+ // ws the WebSocketTransport of the Node entry @procedures/client/node, whose default class
12
+ // is the WebSocket of the ws package (since 0.4.0). The client gives the transport a
13
+ // subclass of that class, from the one copy of ws in the image: it stops when the Node
14
+ // entry resolves another copy.
15
+ // Each setting of the transport is a flag, and the client has no default: a missing flag is a
16
+ // failure. 0.7.0 takes each time as a duration, not a number. Both modes call `harness.echo`
17
+ // first, and its reply is `ready`. idle: then hold the connection. inflight: then wait for the file
18
+ // --go, and call `harness.echo` (or `harness.hold` for --hold-ms) into the fault.
14
19
  //
15
20
  // The lines are those of the harness's TCP client, on CLOCK_MONOTONIC, and the program's own acts:
16
21
  // dial {conn} before each new socket of the transport (conn counts from 1)
@@ -27,9 +32,13 @@
27
32
  // The client gives the transport a subclass of the class of its sockets. The subclass writes a
28
33
  // line, then calls its class: the transport, undici and ws do what they do without it. Its
29
34
  // listeners of `open` and `close` are its own, thus the transport cannot remove them. A drop line
30
- // with no local address is a socket that never opened: a try that failed.
35
+ // with no local address is a socket that never opened: a try that failed. A WebSocket of ws emits
36
+ // no `error` for an error of its socket (lib/websocket.js:1411-1421), thus the subclass reads that
37
+ // error on the socket itself.
31
38
 
32
39
  import { subscribe } from "node:diagnostics_channel";
40
+ import { createRequire } from "node:module";
41
+ import { fileURLToPath } from "node:url";
33
42
  import { WebSocket as WsWebSocket } from "ws";
34
43
  import { bootId, commandLine, emit, now, waitFor } from "./lines.mjs";
35
44
 
@@ -41,7 +50,7 @@ const mode = line.required("--mode");
41
50
  if (mode !== "idle" && mode !== "inflight") line.fail("--mode is idle or inflight");
42
51
  const socketClass = line.required("--websocket");
43
52
  if (socketClass !== "global" && socketClass !== "ws") line.fail("--websocket is global or ws");
44
- /** This function gives a duration of milliseconds, as 0.3.0 takes each time. */
53
+ /** This function gives a duration of milliseconds, as 0.7.0 takes each time. */
45
54
  const ms = (value) => ({ value, unit: "ms" });
46
55
  const heartbeat = line.toggle("--heartbeat")
47
56
  ? {
@@ -67,11 +76,22 @@ const hold = line.flag("--hold-ms") === undefined ? undefined : line.number("--h
67
76
  const go = line.flag("--go") ?? "/tmp/go";
68
77
  const max = Number(line.flag("--max-s") ?? 60) * 1000;
69
78
 
79
+ /** The entry of the transport for each class: the root for the global WebSocket, the Node entry for ws. */
80
+ const ENTRY = socketClass === "ws" ? "@procedures/client/node" : "@procedures/client";
81
+ // the class of the Node entry is the class of the ws that it resolves: it must be the ws of this client
82
+ if (socketClass === "ws") {
83
+ const nodeEntry = fileURLToPath(import.meta.resolve(ENTRY));
84
+ const theirs = createRequire(nodeEntry).resolve("ws");
85
+ const ours = createRequire(import.meta.url).resolve("ws");
86
+ if (theirs !== ours) line.fail(`${ENTRY} resolves ws at ${theirs}, and this client at ${ours}`);
87
+ }
88
+
70
89
  emit("start", {
71
90
  boot_id: bootId(),
72
91
  mode,
73
92
  client: "procedures-ws",
74
- package: "@procedures/client 0.3.0",
93
+ package: "@procedures/client 0.7.0",
94
+ entry: ENTRY,
75
95
  websocket: socketClass,
76
96
  node: process.version,
77
97
  undici: process.versions.undici ?? null,
@@ -120,10 +140,14 @@ const stamped = (Base) => {
120
140
  super(...args);
121
141
  const socket = { conn, local: undefined, dropped: false };
122
142
  sockets.set(this, socket);
123
- // a socket of ws gives its local address at its open; undici gives it in a diagnostics channel
143
+ // a socket of ws gives its local address at its open, and undici gives it in a diagnostics
144
+ // channel. ws keeps the error of its socket to itself, thus the client reads it there
124
145
  this.addEventListener("open", () => {
125
146
  const raw = this._socket;
126
147
  if (raw?.localAddress !== undefined) opened(socket, `${raw.localAddress}:${raw.localPort}`);
148
+ raw?.on?.("error", (error) => {
149
+ pending = { errno: error?.code ?? error?.name ?? "unknown", at: now() };
150
+ });
127
151
  });
128
152
  this.addEventListener("error", (event) => {
129
153
  const code = event?.error?.code;
@@ -174,7 +198,8 @@ setTimeout(() => {
174
198
  process.exit(0);
175
199
  }, max);
176
200
 
177
- const { Client, WebSocketTransport } = await import("@procedures/client");
201
+ const { Client } = await import("@procedures/client");
202
+ const { WebSocketTransport } = await import(ENTRY);
178
203
 
179
204
  let first;
180
205
  const firstOpen = new Promise((resolve) => {
@@ -6,14 +6,14 @@
6
6
  "": {
7
7
  "name": "loom-harness-procedures",
8
8
  "dependencies": {
9
- "@procedures/client": "0.3.0",
9
+ "@procedures/client": "0.7.0",
10
10
  "ws": "8.22.0"
11
11
  }
12
12
  },
13
13
  "node_modules/@mark1russell7/algebra": {
14
- "version": "0.5.0",
15
- "resolved": "https://registry.npmjs.org/@mark1russell7/algebra/-/algebra-0.5.0.tgz",
16
- "integrity": "sha512-rNKCWvZBGDUYtA1BOPftPY7+zG6LWdISWrjkZwsHf3IS4bfz6JCeWVpJ0LV/vEvNRz1OnVjh9WgwoEUn1Y4cCQ==",
14
+ "version": "0.7.1",
15
+ "resolved": "https://registry.npmjs.org/@mark1russell7/algebra/-/algebra-0.7.1.tgz",
16
+ "integrity": "sha512-+nimxDO9pEOwNxVJ7/F4vOAba8CnytN/DbRJKN7FxF/Q+g5Yy2u6e+3bbjiuq0eH9vCWwc3WlIuyDJhSFfuNow==",
17
17
  "license": "MIT",
18
18
  "dependencies": {
19
19
  "@mark1russell7/quantity": "^0.2.0"
@@ -43,12 +43,12 @@
43
43
  }
44
44
  },
45
45
  "node_modules/@mark1russell7/service": {
46
- "version": "0.4.0",
47
- "resolved": "https://registry.npmjs.org/@mark1russell7/service/-/service-0.4.0.tgz",
48
- "integrity": "sha512-Zby/XIGWTWax927/pkQgeY9ljlzLmBkUnTlDcpGDKnNpEmXhM0WpnK948hnDqvUd2bwXM3/CcM2cYQDAaTA+2A==",
46
+ "version": "0.4.1",
47
+ "resolved": "https://registry.npmjs.org/@mark1russell7/service/-/service-0.4.1.tgz",
48
+ "integrity": "sha512-9lxVPWJ/oZ1wfyl0NhZ7WX0w53L2HwG++78XmWr3gh0IhCiMZSJi3cuvgiRfF/uS2uf3u7TGXmb2MctEZDtF3g==",
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
- "@mark1russell7/algebra": "^0.5.0",
51
+ "@mark1russell7/algebra": "^0.7.0",
52
52
  "@mark1russell7/quantity": "^0.2.0"
53
53
  },
54
54
  "engines": {
@@ -65,20 +65,17 @@
65
65
  }
66
66
  },
67
67
  "node_modules/@procedures/client": {
68
- "version": "0.3.0",
69
- "resolved": "https://registry.npmjs.org/@procedures/client/-/client-0.3.0.tgz",
70
- "integrity": "sha512-DzOOZIhv1DH87zv3Je/D4gg+Jou386i69hPkQU5bz8Gs1vRzZoTrBcb0kojNMS4Ex1cwDxXYr0GDxVmlpVuEnA==",
68
+ "version": "0.7.0",
69
+ "resolved": "https://registry.npmjs.org/@procedures/client/-/client-0.7.0.tgz",
70
+ "integrity": "sha512-eBsvnfEYBJ7yrNpeX3Wi847Q9VlZMc8K92ydFfYIoZDwdfmj/Fxzu/IWQMfpCq7Fvof7Xt0m0Wk6YbcFZx8Frg==",
71
71
  "license": "MIT",
72
72
  "dependencies": {
73
- "@mark1russell7/algebra": "^0.5.0",
73
+ "@mark1russell7/algebra": "^0.7.0",
74
74
  "@mark1russell7/quantity": "^0.2.0",
75
- "@mark1russell7/service": "^0.4.0",
75
+ "@mark1russell7/service": "^0.4.1",
76
76
  "ws": "^8.18.3",
77
77
  "zod": "^4.6.5"
78
78
  },
79
- "bin": {
80
- "client-registry": "dist/procedures/cli/index.js"
81
- },
82
79
  "engines": {
83
80
  "node": ">=22"
84
81
  },
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@procedures/client": "0.3.0",
6
+ "@procedures/client": "0.7.0",
7
7
  "ws": "8.22.0"
8
8
  }
9
9
  }
@@ -1,6 +1,6 @@
1
1
  // server-ws --port N --tcp-keepalive off|N --heartbeat off|on [--heartbeat-interval-ms N --heartbeat-misses N]
2
2
  //
3
- // A cell's target: the procedures server of @procedures/client 0.3.0 (Server and
3
+ // A cell's target: the procedures server of @procedures/client 0.7.0 (Server and
4
4
  // WebSocketServerTransport) on the http.Server of Node 24.21.0, at the path /ws. It has two
5
5
  // procedures: `harness.echo` answers at once, and `harness.hold` answers after `ms` milliseconds
6
6
  // (a held reply, as BLPOP is for Redis).
@@ -10,7 +10,9 @@
10
10
  //
11
11
  // --heartbeat is the heartbeat of the server (roadmap R3): a WebSocket ping to each connection at
12
12
  // each interval, and terminate() for a connection that missed `misses` pongs. Each setting is a
13
- // flag, and the server has no default. 0.3.0 takes each time as a duration, not a number.
13
+ // flag, and the server has no default. 0.7.0 takes each time as a duration, not a number. One
14
+ // setInterval of the transport, from its start, beats for all connections: the first ping of a
15
+ // connection comes at the next beat after its accept (transport.js:94-112, :147-151).
14
16
  //
15
17
  // Each event is a JSON line on the clock of the clients: a connection (`accepted`), a request,
16
18
  // a reply, and the close of a connection. The `ws` package does the WebSocket of the server. Its
@@ -87,7 +89,7 @@ httpServer.listen(port, "0.0.0.0", () =>
87
89
  port,
88
90
  boot_id: bootId(),
89
91
  node: process.version,
90
- package: "@procedures/client 0.3.0",
92
+ package: "@procedures/client 0.7.0",
91
93
  tcpKeepalive: keepAliveMs,
92
94
  heartbeat,
93
95
  }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/harness",
4
- "version": "0.11.0",
4
+ "version": "0.11.2",
5
5
  "description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, a replicated group under Sentinels or a replica set, injects a fault, measures the kernel's own records and judges a prediction. HTTP clients of Go, Node and undici race the keep-alive of a Node server, and the MongoDB driver meets the faults of a replica set. The WebSocket client of @procedures/client drops a silent connection on its own heartbeat, and the harness times that detection and the reconnect from the program's lines and the kernel's stamps.",
6
6
  "keywords": [
7
7
  "fault-injection",
@@ -61,6 +61,10 @@
61
61
  "image/mongo/stepdown-mongo.mjs",
62
62
  "image/mongo/package.json",
63
63
  "image/mongo/npm-lock.json",
64
+ "image/pg/Dockerfile",
65
+ "image/pg/client-pg.mjs",
66
+ "image/pg/package.json",
67
+ "image/pg/npm-lock.json",
64
68
  "image/procedures/Dockerfile",
65
69
  "image/procedures/package.json",
66
70
  "image/procedures/npm-lock.json",