@mark1russell7/harness 0.11.26 → 0.12.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/README.md +11 -10
- package/build/{cell-D4HZ1off.d.ts → cell-jEkGFgGL.d.ts} +9 -19
- package/build/{image-vB51btI1.js → image-Lg8LL6C3.js} +5 -33
- package/build/image-Lg8LL6C3.js.map +1 -0
- package/build/index.d.ts +46 -17
- package/build/index.js +37 -11
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/build/schema.js +1 -1
- package/package.json +1 -7
- package/build/image-vB51btI1.js.map +0 -1
- package/image/procedures/Dockerfile +0 -14
- package/image/procedures/client-ws.mjs +0 -259
- package/image/procedures/lines.mjs +0 -68
- package/image/procedures/npm-lock.json +0 -122
- package/image/procedures/package.json +0 -9
- package/image/procedures/server-ws.mjs +0 -96
package/README.md
CHANGED
|
@@ -130,14 +130,13 @@ A step-down (`step-down`) moves the leadership to the first follower (`rabbitmq-
|
|
|
130
130
|
- The client of undici then writes an `attempt` line for each send of a request, with its number `n` and its socket. A `RetryAgent` that gives up gives `UND_ERR_REQ_RETRY` on the `error` line, with the status of the last reply and the count of sends.
|
|
131
131
|
- **The lines of an HTTP client.** It writes the lines of the TCP client, with the number `i` of the request on each line of a request. It also writes the connection of each try of a request (`request`, then `retry`), with its local address and a flag for a reused connection. Each connection gets a `conn` line, and each end of a connection a line (`eof`, `conn-error`, `closed`). The end of the second request is a `done` line.
|
|
132
132
|
- **A target with a script.** A target can take files before it starts (`Target.files`), for example the script of a Node server on the stock Node image. A target that writes JSON lines (`Target.lines`) gives them to each run (`Run.server`). The manifest holds the sha256 of each file and the first line of the target.
|
|
133
|
-
- **The WebSocket client**
|
|
133
|
+
- **The WebSocket client** is the `WebSocketTransport` of `@procedures/client`: the `client` program of a `procedures` subject (`Cell.client` is its client image). The client repository builds that subject (CLI-33). The cell gives each of its settings (`probe.ws`): the class of its sockets, the heartbeat, the reconnect, and the times of a call.
|
|
134
134
|
- Its first request is `harness.echo`. In flight, its request into the fault is `harness.echo`, or `harness.hold` for a held reply.
|
|
135
135
|
- It writes the lines of the TCP client. It also writes a line for each act of the program: `ping`, `drop`, `reconnecting` and `dial`.
|
|
136
136
|
- The class of its sockets is the global `WebSocket` of Node, with the transport of the root entry. Or it is the `WebSocket` of the `ws` package, with the transport of the Node entry `@procedures/client/node`.
|
|
137
137
|
- The transport gets a subclass of that class. The subclass writes a line, then gives the work to its class. Thus the transport, undici and ws operate as they do without it.
|
|
138
138
|
- The subclass has `terminate()` only when its class has it, because the transport drops a socket with `terminate()` when it exists.
|
|
139
|
-
- **A target from the image of a subject** (`Target.subject`). The target operates the image of a subject in place of its stock image. `Target.image` is then the stock base of that image, and the manifest names both. The procedures server is a
|
|
140
|
-
- **A target from an image of the harness** (`Target.harness`) is a short form. `harness: "procedures"` is `subject: { kind: "shipped", name: "procedures" }`.
|
|
139
|
+
- **A target from the image of a subject** (`Target.subject`). The target operates the image of a subject in place of its stock image. `Target.image` is then the stock base of that image, and the manifest names both. The procedures server is a program in the image of the WebSocket client (`server-ws.mjs`). Thus the client and the server use one release of the package.
|
|
141
140
|
- **An agent** (`lab.agent(image, command, { pid })`) is a container in the network namespace of the target. With `pid`, it also shares the process namespace of the target, thus it can signal the server process. Its JSON lines and its `exec` stay available to the caller.
|
|
142
141
|
|
|
143
142
|
## The subjects
|
|
@@ -151,8 +150,8 @@ A subject (`Subject`, `image.ts`) is the program under test, as data. The harnes
|
|
|
151
150
|
- **A relative folder** of a build starts at the working folder of the process. A record holds the source of each image as the cell gives it. Thus give a relative folder, and the record names no path of the machine.
|
|
152
151
|
- **A subject** gives the source of its image and its stock base, pinned by digest. It also gives the command of each of its programs by their roles, for example `client` and `server`.
|
|
153
152
|
- **A subject in a cell.** `subjectClient(subject)` gives the client of a cell, and `programOf(subject, role)` gives the command of a program. A target operates the image of the subject (`Target.subject`), and its stock image is the base of the subject.
|
|
154
|
-
- **A client that the harness ships keeps its name.**
|
|
155
|
-
- **The
|
|
153
|
+
- **A client that the harness ships keeps its name.** When the client of a subject is a client that the harness ships, `subjectClient` gives its name. Thus the records of a cell on that subject stay the same.
|
|
154
|
+
- **The procedures subject** is a subject of the client repository: `harness/procedures-subject/subject.json` of github.com/mark1russell7/client. From 0.12.0, the harness ships no procedures subject. The cells check out the client repository and give that subject.
|
|
156
155
|
|
|
157
156
|
These rules keep a build the same in each run:
|
|
158
157
|
|
|
@@ -210,7 +209,6 @@ From 0.11.20, the harness builds each image with the CLI of Docker (`docker buil
|
|
|
210
209
|
- The events of the driver are `sdam` lines (the heartbeats and the changes of each server), `cmap` lines (the pools) and `command` lines (each try of an operation).
|
|
211
210
|
- Each operation is a `sent` line, then an `ack` line or a `fail` line. An `ack` line has the server that accepted it. A `fail` line has the name and the labels of its error.
|
|
212
211
|
- The same image holds `stepdown-mongo.mjs`, the sender of `replSetStepDown`.
|
|
213
|
-
- **The procedures image** (`image/procedures`) is the image of the `procedures` subject. It holds `@procedures/client` 0.7.0 and `ws` 8.22.0 on Node 24.21.0, whose WebSocket is undici 7.29.1. The digest of the image and an npm lockfile pin them. It holds the WebSocket client (`client-ws.mjs`) and the script of the procedures server (`server-ws.mjs`). Each takes each of its settings as a flag, and it has no default. The lockfile holds one copy of `ws`, and the client stops when `@procedures/client/node` resolves another one.
|
|
214
212
|
- **The router image** (`image/router`) is a digest-pinned Alpine with conntrack-tools, nftables, iproute2 and Python. `mbx.sh` sets the form of the router in one nftables table, `ctwatch.py` writes the conntrack events, and `resetter.py` sends the resets of the `reset-both` form. Each run records its packages in the manifest.
|
|
215
213
|
- **The PgBouncer image** (`image/pgbouncer`) is PgBouncer 1.26.1, a pooler hop, on a digest-pinned Alpine. The build gets the release tarball of pgbouncer.org and checks its SHA-256 before it builds. The C sources of that tarball are those of the tag that the machine documents cite.
|
|
216
214
|
- A hop takes the image as a shipped source: `{ kind: "shipped", name: "pgbouncer" }`. A proxy hop also takes a stock image by its digest.
|
|
@@ -295,13 +293,15 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
|
|
|
295
293
|
- **A detection by the program** (`application.ts`). The kernel has no report of it. Thus the program of the observer writes a line before each of its own acts, and the kernel stamps each act on the socket.
|
|
296
294
|
- The program writes `ping` before each ping of its heartbeat. The kernel stamps the write of the ping (`tx`).
|
|
297
295
|
- The reference is the first ping that got no answer (`ping-sent`): no segment with data reached the socket after its write, up to the detection. Its write starts the timer of the heartbeat that the fault met. The fault can come at any time, and its stamps are not necessary.
|
|
298
|
-
- The program writes `drop` before it drops the connection itself. The detection is the kernel's stamp of that drop: the write of its close frame, or the change of its socket out of ESTABLISHED. Without such a record in
|
|
296
|
+
- The program writes `drop` before it drops the connection itself. The detection is the kernel's stamp of that drop: the write of its close frame, or the change of its socket out of ESTABLISHED. Without such a record in the handling time of the program (`DROP_HANDLING`, 1.5 ms), it is the stamp of the line. Where δ_abs is longer, the window is δ_abs.
|
|
297
|
+
- The handling time is not δ_abs. δ_abs is the noise of the pipeline, from a reset. The handling is the time of the code of the program, from the firing of its timer to its act.
|
|
298
|
+
- In 585 drops with a close frame in CI, the write came a median of 0.652 ms after the `drop` line, and 1.017 ms at the most. Two of them came more than the δ_abs of 1 ms after it. Each failed its cell, with its time in the band. These are the runs 39390222333 and 39903020657 of the client repository. The bound is that maximum and a margin of 0.48 ms.
|
|
299
299
|
- A drop counts when it comes before each report of the kernel. A report of the kernel that comes first is the detection, and the drop after it is not.
|
|
300
300
|
- The second view is the stamps of the program: from its `ping` line to its `drop` line. The steps are the later pings, each from the reference.
|
|
301
301
|
- The lines of a client name the socket by its local address (`local`), and the lines of a server by its peer (`peer`). For the server, the lines are the lines of the target. A line whose address is null is about a try that did not open.
|
|
302
302
|
- The harness reads the peer of a server from the records of its socket. Behind a router, that peer is the address of the router, not the address of the client.
|
|
303
303
|
- A first ping without an answer can come late, for example at a beat of a server whose phase no document gives. Then the prediction gives its latest time after the reply (`startsBy`), and the run waits that much longer.
|
|
304
|
-
- A prediction can say how the program drops (`drop`). A close frame (`frame`) is a write, and the socket stays ESTABLISHED after it. A close (`close`) closes the socket in
|
|
304
|
+
- A prediction can say how the program drops (`drop`). A close frame (`frame`) is a write, and the socket stays ESTABLISHED after it. A close (`close`) closes the socket at once, in the same window: a FIN or an abort. Another drop is a finding. A FIN of the peer that answers a close frame is no close by the program. The run gives the time from the `drop` line to the close by the program (`left`).
|
|
305
305
|
- The client process ends at the end of each run, and its kernel then closes each socket that is still open. A prediction can say that the program is that client process (`exits`). Then a close in δ_abs of that end is no close by the program: `left` stays null, and `exited` gives the time.
|
|
306
306
|
- The end of the process is the first change out of ESTABLISHED of another socket of the client after its last `dial` line (`exitOf`). The program dropped none of these sockets.
|
|
307
307
|
- A heartbeat is a timer of the program. The retransmit timer of its ping fires in the same interval. Thus a run whose retransmit timer was two jiffies or more behind its wheel slot is void: the VM stopped a vCPU.
|
|
@@ -353,8 +353,9 @@ Each run starts from kernels that keep nothing from the run before it (`lab.forg
|
|
|
353
353
|
`verdictOf(prediction, runs)` (`verdict.ts`) judges a cell:
|
|
354
354
|
|
|
355
355
|
- **Containment.** Each run lands in [T_lo − δ_abs, T_hi + δ_abs]. `NOISE` (δ_abs) is 4 ms, from the calibration of the first environment: the worst reset took 1.141 ms, and δ_abs is one jiffy at the minimum.
|
|
356
|
-
- **δ_abs of an environment.** A cell can give the δ_abs of its environment (`Cell.noise`). Stage 0 measures it in each environment. On a hosted runner of GitHub at HZ 1000, the worst reset took 0.147 ms, thus δ_abs is one jiffy, 1 ms. These read it (`noiseOf`): the bands, the pairing of a program line with its kernel record, and the drop of a program.
|
|
356
|
+
- **δ_abs of an environment.** A cell can give the δ_abs of its environment (`Cell.noise`). Stage 0 measures it in each environment. On a hosted runner of GitHub at HZ 1000, the worst reset took 0.147 ms, thus δ_abs is one jiffy, 1 ms. These read it (`noiseOf`): the bands, the pairing of a program line with its kernel record, and the drop of a program (with `DROP_HANDLING`).
|
|
357
357
|
- **The injector has its own bound.** A fault that the injector commits or lifts late makes a run void. That bound is `INJECT` (4 ms), not δ_abs. Stage 0 measures the commit apart. On a hosted runner it took up to 2.75 ms at 4 environments.
|
|
358
|
+
- **The drop of a program has its own bound.** The pairing of a `drop` line with its act uses `DROP_HANDLING` (1.5 ms), or δ_abs when δ_abs is longer. The band of a drop in the cells adds `DROP_HANDLING` to its high bound.
|
|
358
359
|
- **Median equivalence.** The order-statistic interval of the median must be in the band, made 5 % wider.
|
|
359
360
|
- **Extension.** A median across the edge of the band extends the cell from 11 runs to 22, then 44. Then the verdict is inconclusive.
|
|
360
361
|
- **Recovery.** The recovery after a stall gets the same test as a detection, by its band. A detection is then a finding.
|
|
@@ -424,7 +425,7 @@ The tests of `application.ts` measure a heartbeat of a program from test lines a
|
|
|
424
425
|
|
|
425
426
|
The cells themselves need Docker and privileged containers. CI does not start them. `HARNESS_LAB=1` starts the lab tests on Docker. They start a client image and an agent, and they put a reset and netem at a router. They also fail a group of Valkey 9.1 over with a planned failover.
|
|
426
427
|
|
|
427
|
-
In the lab tests, each HTTP client sends two requests on one connection to a Node server, from a script in the stock Node image. The procedures WebSocket client drops a silent connection on its heartbeat, and it connects again to the procedures server. With the global `WebSocket`, the drop writes a close frame. With the `WebSocket` of ws, the socket leaves ESTABLISHED at once. The subject of that test is
|
|
428
|
+
In the lab tests, each HTTP client sends two requests on one connection to a Node server, from a script in the stock Node image. The procedures WebSocket client drops a silent connection on its heartbeat, and it connects again to the procedures server. With the global `WebSocket`, the drop writes a close frame. With the `WebSocket` of ws, the socket leaves ESTABLISHED at once. The subject of that test is the procedures subject of a checkout of the client repository (`HARNESS_PROCEDURES`). Without the checkout, that test waits.
|
|
428
429
|
|
|
429
430
|
The lab tests also step a replica set of three members of MongoDB 9.0.4 down, and they write the free memory of the machine. They also reset a write of the MongoDB driver in flight to a set of one member. They move the leadership of a quorum queue of a cluster of three nodes of RabbitMQ 4.3.6.
|
|
430
431
|
|
|
@@ -679,25 +679,14 @@ interface Target {
|
|
|
679
679
|
* and the script of its server.
|
|
680
680
|
*/
|
|
681
681
|
readonly subject?: ImageSource;
|
|
682
|
-
/**
|
|
683
|
-
* This name is an image of the harness that the target operates in place of `image`. It is a
|
|
684
|
-
* short form: `harness: "procedures"` is `subject: { kind: "shipped", name: "procedures" }`.
|
|
685
|
-
* `subject` replaces it.
|
|
686
|
-
*/
|
|
687
|
-
readonly harness?: HarnessImage;
|
|
688
682
|
}
|
|
689
|
-
/**
|
|
690
|
-
* A harness image is an image of the harness that a target can operate by its name (`Target.harness`).
|
|
691
|
-
* `procedures` is the image of `@procedures/client` on Node: its WebSocket client and its server script.
|
|
692
|
-
*/
|
|
693
|
-
type HarnessImage = "procedures";
|
|
694
683
|
/**
|
|
695
684
|
* A shipped image is an image that the harness ships, by its name. Its build context is in the
|
|
696
685
|
* package of the harness, and in the package of the prober for `harness`. `grpc-go` is a server of
|
|
697
686
|
* grpc-go and `etcd` is etcd on Alpine: each is a subject of a target (`Target.subject`).
|
|
698
687
|
* `docker-proxy` is the userland proxy of Docker Engine 29.1.5, the program of a proxy hop.
|
|
699
688
|
*/
|
|
700
|
-
type Shipped = "harness" | "node" | "go-http" | "node-http" | "mongo" | "pg" | "hikari" | "amqp" | "grpc" | "grpc-go" | "etcd" | "
|
|
689
|
+
type Shipped = "harness" | "node" | "go-http" | "node-http" | "mongo" | "pg" | "hikari" | "amqp" | "grpc" | "grpc-go" | "etcd" | "router" | "pgbouncer" | "pgpool" | "docker-proxy";
|
|
701
690
|
/**
|
|
702
691
|
* An image source tells where an image comes from, as data. The harness resolves each kind to an
|
|
703
692
|
* image that it can start:
|
|
@@ -735,7 +724,8 @@ interface ShippedImage {
|
|
|
735
724
|
/**
|
|
736
725
|
* A subject is a program under test, as data: the image that holds its programs, and the command
|
|
737
726
|
* of each program. A catalog of cells takes a subject. Thus another repository can give its own
|
|
738
|
-
* subject, with its own release in it. The
|
|
727
|
+
* subject, with its own release in it. The procedures subject of the WS cells is a subject of the
|
|
728
|
+
* client repository (CLI-33).
|
|
739
729
|
*/
|
|
740
730
|
interface Subject {
|
|
741
731
|
/** This name is the name of the subject in a catalog and in the records: `procedures`. */
|
|
@@ -754,12 +744,12 @@ interface Subject {
|
|
|
754
744
|
* A client is what the runs of a cell use: the TCP client of the harness, or a driver on its
|
|
755
745
|
* socket. The HTTP clients are the `http.Transport` of Go (`go-http`), the `http.Agent` of Node
|
|
756
746
|
* (`http-agent`) and an HTTP/1.1 `Client` of undici (`undici`). The MongoDB driver is `mongodb`, node-postgres is `pg`, and HikariCP over pgjdbc is `hikari`.
|
|
757
|
-
* The client of amqplib is `amqp`, the client of grpc-js is `grpc
|
|
747
|
+
* The client of amqplib is `amqp`, and the client of grpc-js is `grpc`. The WebSocket client of `@procedures/client` is a program of its subject (`ClientImage`).
|
|
758
748
|
*/
|
|
759
|
-
type Client = "tcp" | "ioredis" | "go-http" | "http-agent" | "undici" | "mongodb" | "pg" | "hikari" | "amqp" | "grpc"
|
|
749
|
+
type Client = "tcp" | "ioredis" | "go-http" | "http-agent" | "undici" | "mongodb" | "pg" | "hikari" | "amqp" | "grpc";
|
|
760
750
|
/**
|
|
761
751
|
* A WebSocket probe is the workload and the settings of the WebSocket client of
|
|
762
|
-
* `@procedures/client
|
|
752
|
+
* `@procedures/client`, the `client` program of its subject. The cell gives each setting, thus the client takes
|
|
763
753
|
* nothing from a default.
|
|
764
754
|
*/
|
|
765
755
|
interface WsProbe {
|
|
@@ -910,7 +900,7 @@ interface Cell {
|
|
|
910
900
|
};
|
|
911
901
|
/** This workload is the workload of an HTTP client (`Cell.client` is an HTTP client). The client then ignores `keepalive` and `userTimeout`. */
|
|
912
902
|
readonly http?: HttpProbe;
|
|
913
|
-
/** These are the settings of the WebSocket client (`Cell.client` is
|
|
903
|
+
/** These are the settings of the WebSocket client (`Cell.client` is the client of a procedures subject). The client then ignores `keepalive` and `userTimeout`. */
|
|
914
904
|
readonly ws?: WsProbe;
|
|
915
905
|
};
|
|
916
906
|
readonly fault: Fault;
|
|
@@ -1153,5 +1143,5 @@ interface RunTopology {
|
|
|
1153
1143
|
}>>;
|
|
1154
1144
|
}
|
|
1155
1145
|
//#endregion
|
|
1156
|
-
export {
|
|
1157
|
-
//# sourceMappingURL=cell-
|
|
1146
|
+
export { Subject as $, Prediction as A, RequestPrediction as B, KernelRecord as C, MiddleboxForm as D, MiddleboxExpectation as E, Recovers as F, RunPart as G, RttRange as H, Reference as I, SentinelStage as J, RunTopology as K, ReplicaSet as L, ProxyHop as M, ProxyTime as N, Observer as O, Reconnects as P, StageBand as Q, ReplicaSetStage as R, ImageSource as S, MiddleboxEvent as T, Run as U, RouterHop as V, RunGroup as W, ShippedImage as X, Shipped as Y, Stage as Z, HopEvent as _, Cluster as a, latestEndOf as at, HopWrite as b, DriverBand as c, FaultPlace as d, Target as et, Group as f, Hop as g, GroupRole as h, ClientImage as i, isHopPlace as it, ProbeEvent as j, PinnedImage as k, DriverStep as l, GroupPrediction as m, Cell as n, Toxic as nt, ClusterClientAt as o, GroupPlace as p, SentinelEvent as q, Client as r, WsProbe as rt, ClusterStage as s, BuildImage as t, Topology as tt, Fault as u, HopPlace as v, LineMatch as w, HttpProbe as x, HopSignal as y, Replies as z };
|
|
1147
|
+
//# sourceMappingURL=cell-jEkGFgGL.d.ts.map
|
|
@@ -11,7 +11,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
11
11
|
* This module resolves the source of each image (`ImageSource`) to an image that a lab can start.
|
|
12
12
|
* A pinned image is its own name. A build context, or an image that the harness ships, becomes a
|
|
13
13
|
* build with a tag from its content. Each image of the harness is a shipped source in one table.
|
|
14
|
-
* Thus the
|
|
14
|
+
* Thus the image of a subject is data, as the image of a caller is.
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
17
|
* These are the roots of the two packages that the shipped images read: this package, and the
|
|
@@ -93,11 +93,6 @@ const SHIPPED = {
|
|
|
93
93
|
dockerfile: "harness/image/etcd/Dockerfile",
|
|
94
94
|
from: ["harness/image/etcd"]
|
|
95
95
|
},
|
|
96
|
-
procedures: {
|
|
97
|
-
name: "loom-harness-procedures",
|
|
98
|
-
dockerfile: "harness/image/procedures/Dockerfile",
|
|
99
|
-
from: ["harness/image/procedures"]
|
|
100
|
-
},
|
|
101
96
|
router: {
|
|
102
97
|
name: "loom-harness-router",
|
|
103
98
|
dockerfile: "harness/image/router/Dockerfile",
|
|
@@ -121,23 +116,6 @@ const SHIPPED = {
|
|
|
121
116
|
};
|
|
122
117
|
/** These are the names of the images that the harness ships. */
|
|
123
118
|
const SHIPPED_NAMES = Object.keys(SHIPPED);
|
|
124
|
-
/**
|
|
125
|
-
* These are the subjects that the harness ships, by their names. `procedures` is
|
|
126
|
-
* `@procedures/client` on Node: its WebSocket client (`client-ws.mjs`) and its server
|
|
127
|
-
* (`server-ws.mjs`), from one image. Thus the client and the server use one release.
|
|
128
|
-
*/
|
|
129
|
-
const SUBJECTS = { procedures: {
|
|
130
|
-
name: "procedures",
|
|
131
|
-
image: {
|
|
132
|
-
kind: "shipped",
|
|
133
|
-
name: "procedures"
|
|
134
|
-
},
|
|
135
|
-
base: "node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1",
|
|
136
|
-
programs: {
|
|
137
|
-
client: ["node", "/opt/harness/procedures/client-ws.mjs"],
|
|
138
|
-
server: ["node", "/opt/harness/procedures/server-ws.mjs"]
|
|
139
|
-
}
|
|
140
|
-
} };
|
|
141
119
|
/** This function gives the source of a shipped image. */
|
|
142
120
|
const shipped = (name) => ({
|
|
143
121
|
kind: "shipped",
|
|
@@ -193,10 +171,6 @@ const SHIPPED_CLIENTS = {
|
|
|
193
171
|
grpc: {
|
|
194
172
|
image: shipped("grpc"),
|
|
195
173
|
command: ["node", "/opt/harness/grpc/client-grpc.mjs"]
|
|
196
|
-
},
|
|
197
|
-
"procedures-ws": {
|
|
198
|
-
image: SUBJECTS.procedures.image,
|
|
199
|
-
command: SUBJECTS.procedures.programs["client"]
|
|
200
174
|
}
|
|
201
175
|
};
|
|
202
176
|
/** These are the names of the clients that the harness ships. */
|
|
@@ -509,16 +483,14 @@ const hikariClientImage = () => imageOf(shipped("hikari"));
|
|
|
509
483
|
const amqpClientImage = () => imageOf(shipped("amqp"));
|
|
510
484
|
/** This function gives the image of the gRPC client of the harness: grpc-js 1.14.6 on Node 24.21.0, with its client. */
|
|
511
485
|
const grpcClientImage = () => imageOf(shipped("grpc"));
|
|
512
|
-
/** This function gives the image of the procedures subject: the WebSocket client of `@procedures/client`, and its server. */
|
|
513
|
-
const proceduresImage = () => imageOf(SUBJECTS.procedures.image);
|
|
514
486
|
/**
|
|
515
487
|
* This function gives the source of the image that a target operates. It is the image of its
|
|
516
|
-
* subject (`Target.subject`),
|
|
488
|
+
* subject (`Target.subject`), or its stock image.
|
|
517
489
|
*/
|
|
518
|
-
const targetSourceOf = (target) => target.subject ??
|
|
490
|
+
const targetSourceOf = (target) => target.subject ?? target.image;
|
|
519
491
|
/** This function gives the image that a target operates: the image of its subject, or its stock image. */
|
|
520
492
|
const targetImageOf = (target) => imageOf(targetSourceOf(target));
|
|
521
493
|
//#endregion
|
|
522
|
-
export {
|
|
494
|
+
export { targetImageOf as C, unpinnedBases as E, subjectProblems as S, unlockedManifests as T, pgClientImage as _, SHIPPED_NAMES as a, routerImage as b, grpcClientImage as c, imageOf as d, imageProblems as f, nodeHttpImage as g, nodeClientImage as h, SHIPPED_CLIENTS as i, harnessImage as l, mongoClientImage as m, PINNED as n, amqpClientImage as o, inContext as p, REPOSITORY as r, goHttpImage as s, CLIENT_NAMES as t, hikariClientImage as u, planOf as v, targetSourceOf as w, subjectClient as x, programOf as y };
|
|
523
495
|
|
|
524
|
-
//# sourceMappingURL=image-
|
|
496
|
+
//# sourceMappingURL=image-Lg8LL6C3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-Lg8LL6C3.js","names":[],"sources":["../src/image.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, posix, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport type { BuildImage, Client, ClientImage, ImageSource, Shipped, ShippedImage, Subject, Target } from \"./cell.ts\";\n\n/**\n * This module resolves the source of each image (`ImageSource`) to an image that a lab can start.\n * A pinned image is its own name. A build context, or an image that the harness ships, becomes a\n * build with a tag from its content. Each image of the harness is a shipped source in one table.\n * Thus the image of a subject is data, as the image of a caller is.\n */\n\n/**\n * These are the roots of the two packages that the shipped images read: this package, and the\n * prober that Node resolves for it. In a workspace, they are two folders side by side. From npm,\n * the prober is a link, and a Docker build context does not follow a link. Thus each image builds\n * from a context that the harness copies from the two roots.\n */\nconst ROOTS: Readonly<Record<string, string>> = {\n harness: fileURLToPath(new URL(\"..\", import.meta.url)),\n prober: dirname(realpathSync(createRequire(import.meta.url).resolve(\"@mark1russell7/prober/package.json\"))),\n};\n\n/** This function gives the path of a part of a shipped image, which is `<package>/<path>` in the context. */\nconst pathOf = (part: string): string => {\n const [root, ...rest] = part.split(\"/\");\n const base = ROOTS[root!];\n if (base === undefined) throw new Error(`${part} is in no package that the images read`);\n return join(base, ...rest);\n};\n\n/** A shipped context is the build context of a shipped image: its Dockerfile, and each file or folder that the Dockerfile reads, as `<package>/<path>`. */\ninterface ShippedContext {\n readonly name: string;\n readonly dockerfile: string;\n readonly from: readonly string[];\n}\n\n/** These are the build contexts of the images that the harness ships, by their names. */\nconst SHIPPED: Readonly<Record<Shipped, ShippedContext>> = {\n // the prober, the TCP client, the injector and the namespace admin\n harness: {\n name: \"loom-harness\",\n dockerfile: \"harness/image/Dockerfile\",\n from: [\"harness/image/Dockerfile\", \"harness/image/native\", \"prober/bpf\", \"prober/native\", \"prober/fixtures\"],\n },\n // the Node client: ioredis 6.0.0 on Node 24.21.0\n node: { name: \"loom-harness-node\", dockerfile: \"harness/image/node/Dockerfile\", from: [\"harness/image/node\"] },\n // the Go HTTP client: the http.Transport of Go 1.27.2\n \"go-http\": { name: \"loom-harness-go-http\", dockerfile: \"harness/image/go-http/Dockerfile\", from: [\"harness/image/go-http\"] },\n // the Node HTTP clients: the http.Agent of Node 24.21.0, and undici 8.12.2\n \"node-http\": { name: \"loom-harness-node-http\", dockerfile: \"harness/image/node-http/Dockerfile\", from: [\"harness/image/node-http\"] },\n // the MongoDB client: the driver mongodb 7.7.0 on Node 24.21.0, and its step-down script\n mongo: { name: \"loom-harness-mongo\", dockerfile: \"harness/image/mongo/Dockerfile\", from: [\"harness/image/mongo\"] },\n // the PostgreSQL client: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0\n pg: { name: \"loom-harness-pg\", dockerfile: \"harness/image/pg/Dockerfile\", from: [\"harness/image/pg\"] },\n // the client of HikariCP 7.1.0 over pgjdbc 42.7.13 on Temurin 21.0.12.1: its jars by their SHA-256\n hikari: { name: \"loom-harness-hikari\", dockerfile: \"harness/image/hikari/Dockerfile\", from: [\"harness/image/hikari\"] },\n // the RabbitMQ client: amqplib 2.2.0 on Node 24.21.0\n amqp: { name: \"loom-harness-amqp\", dockerfile: \"harness/image/amqp/Dockerfile\", from: [\"harness/image/amqp\"] },\n // the gRPC client: grpc-js 1.14.6 on Node 24.21.0\n grpc: { name: \"loom-harness-grpc\", dockerfile: \"harness/image/grpc/Dockerfile\", from: [\"harness/image/grpc\"] },\n // the gRPC server: a server of grpc-go v1.84.0, built with Go 1.27.2, that can hold a reply\n \"grpc-go\": { name: \"loom-harness-grpc-go\", dockerfile: \"harness/image/grpc-go/Dockerfile\", from: [\"harness/image/grpc-go\"] },\n // etcd v3.6.15 from its image, on Alpine: a shell to hold it, and its binaries unchanged\n etcd: { name: \"loom-harness-etcd\", dockerfile: \"harness/image/etcd/Dockerfile\", from: [\"harness/image/etcd\"] },\n // the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow\n router: { name: \"loom-harness-router\", dockerfile: \"harness/image/router/Dockerfile\", from: [\"harness/image/router\"] },\n // PgBouncer 1.26.1, a pooler hop: built from its release tarball, by its SHA-256\n pgbouncer: { name: \"loom-harness-pgbouncer\", dockerfile: \"harness/image/pgbouncer/Dockerfile\", from: [\"harness/image/pgbouncer\"] },\n // Pgpool-II 4.7.3, a pooler hop: built from the source of its tag, by its SHA-256\n pgpool: { name: \"loom-harness-pgpool\", dockerfile: \"harness/image/pgpool/Dockerfile\", from: [\"harness/image/pgpool\"] },\n // docker-proxy of Docker Engine 29.1.5, a proxy hop: its binary from the static bundle of Docker, by its SHA-256\n \"docker-proxy\": { name: \"loom-harness-docker-proxy\", dockerfile: \"harness/image/docker-proxy/Dockerfile\", from: [\"harness/image/docker-proxy\"] },\n};\n\n/** These are the names of the images that the harness ships. */\nexport const SHIPPED_NAMES: readonly [Shipped, ...Shipped[]] = Object.keys(SHIPPED) as [Shipped, ...Shipped[]];\n\n/** This function gives the source of a shipped image. */\nconst shipped = (name: Shipped): ImageSource => ({ kind: \"shipped\", name });\n\n/**\n * These are the clients that the harness ships, by their names (`Client`): the image of each, and\n * its command. A cell that names a client gets this image and this command.\n */\nexport const SHIPPED_CLIENTS: Readonly<Record<Client, ClientImage>> = {\n tcp: { image: shipped(\"harness\"), command: [\"/opt/harness/client\"] },\n ioredis: { image: shipped(\"node\"), command: [\"node\", \"/opt/harness/node/client-ioredis.mjs\"] },\n \"go-http\": { image: shipped(\"go-http\"), command: [\"/opt/harness/client-go\"] },\n \"http-agent\": { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-agent.mjs\"] },\n undici: { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-undici.mjs\"] },\n mongodb: { image: shipped(\"mongo\"), command: [\"node\", \"/opt/harness/mongo/client-mongo.mjs\"] },\n pg: { image: shipped(\"pg\"), command: [\"node\", \"/opt/harness/pg/client-pg.mjs\"] },\n hikari: {\n image: shipped(\"hikari\"),\n command: [\"java\", \"-Dslf4j.internal.verbosity=ERROR\", \"-cp\", \"/opt/harness/hikari/classes:/opt/harness/hikari/lib/*\", \"ClientHikari\"],\n },\n amqp: { image: shipped(\"amqp\"), command: [\"node\", \"/opt/harness/amqp/client-amqp.mjs\"] },\n grpc: { image: shipped(\"grpc\"), command: [\"node\", \"/opt/harness/grpc/client-grpc.mjs\"] },\n};\n\n/** These are the names of the clients that the harness ships. */\nexport const CLIENT_NAMES: readonly [Client, ...Client[]] = Object.keys(SHIPPED_CLIENTS) as [Client, ...Client[]];\n\n/** This function gives the command of a program of a subject by its role. It fails when the subject has no such program. */\nexport const programOf = (subject: Subject, role: string): readonly string[] => {\n const program = Object.hasOwn(subject.programs, role) ? subject.programs[role] : undefined;\n if (program === undefined) throw new Error(`the subject ${subject.name} has no program ${role}`);\n return program;\n};\n\n/**\n * This function gives the client of a subject for a cell (`Cell.client`): the image of the subject,\n * with the command of a program. A client that the harness ships goes by its name, thus its records\n * stay the same.\n */\nexport const subjectClient = (subject: Subject, role = \"client\"): Client | ClientImage => {\n const client: ClientImage = { image: subject.image, command: programOf(subject, role) };\n return CLIENT_NAMES.find((name) => isDeepStrictEqual(SHIPPED_CLIENTS[name], client)) ?? client;\n};\n\n/** This pattern matches an image pinned by digest: a name, a tag or none, then `@sha256:` and 64 hex digits. */\nexport const PINNED: RegExp = /^[a-z0-9][A-Za-z0-9._\\-/:]*@sha256:[0-9a-f]{64}$/;\n/** This pattern matches the repository of a tag that the harness builds. Its parts are lower case, with `/` between them. */\nexport const REPOSITORY: RegExp = /^[a-z0-9]+(?:[._-]+[a-z0-9]+)*(?:\\/[a-z0-9]+(?:[._-]+[a-z0-9]+)*)*$/;\n\n/** This function tells if a path stays in its context: relative, with `/` between its parts, and no `..`. */\nexport const inContext = (path: string): boolean =>\n typeof path === \"string\" &&\n path.length > 0 &&\n !path.startsWith(\"/\") &&\n !path.includes(\"\\\\\") &&\n !/^[A-Za-z]:/.test(path) &&\n !path.split(\"/\").includes(\"..\");\n\n/** This function tells if a value is a command: an array of one text or more. */\nconst isCommand = (value: unknown): boolean =>\n Array.isArray(value) && value.length > 0 && value.every((word) => typeof word === \"string\" && word.length > 0);\n\n/**\n * This function gives the problems of the source of an image, as data. A pinned image names a\n * digest. A build names a repository, a context, and paths that stay in the context. A shipped image\n * is one that the harness ships.\n */\nexport const imageProblems = (source: ImageSource): string[] => {\n const kind = (source as { readonly kind?: unknown } | null)?.kind;\n if (kind === \"pinned\") {\n const { image } = source as { readonly image?: unknown };\n return typeof image === \"string\" && PINNED.test(image) ? [] : [`${String(image)} is not pinned by digest: name@sha256:<64 hex digits>`];\n }\n if (kind === \"shipped\") {\n const { name } = source as { readonly name?: unknown };\n return typeof name === \"string\" && Object.hasOwn(SHIPPED, name) ? [] : [`the harness ships no image ${String(name)}: ${SHIPPED_NAMES.join(\", \")}`];\n }\n if (kind === \"build\") {\n const build = source as Partial<BuildImage>;\n const problems: string[] = [];\n if (typeof build.name !== \"string\" || !REPOSITORY.test(build.name)) problems.push(`${String(build.name)} is not the name of a repository`);\n if (typeof build.context !== \"string\" || build.context.length === 0) problems.push(\"the build names no context\");\n if (build.files !== undefined && (!Array.isArray(build.files) || build.files.length === 0)) problems.push(\"the build names no files\");\n for (const path of [build.dockerfile ?? \"Dockerfile\", ...(Array.isArray(build.files) ? build.files : [])])\n if (!inContext(path)) problems.push(`${String(path)} is not a path in the context`);\n return problems;\n }\n return [`${JSON.stringify(kind)} is not a kind of image source: pinned, build or shipped`];\n};\n\n/**\n * This function gives the problems of a subject, as data. A subject has a name and a base that a\n * digest pins. Its image source has no problems, and each of its programs is a command.\n */\nexport const subjectProblems = (subject: Subject): string[] => {\n const programs = (subject as { readonly programs?: unknown }).programs;\n const roles = typeof programs === \"object\" && programs !== null ? Object.entries(programs) : [];\n return [\n ...(typeof subject.name === \"string\" && subject.name.length > 0 ? [] : [\"the subject has no name\"]),\n ...(typeof subject.base === \"string\" && PINNED.test(subject.base) ? [] : [`the base ${String(subject.base)} is not pinned by digest`]),\n ...imageProblems(subject.image),\n ...(roles.length === 0 ? [\"the subject has no programs\"] : []),\n ...roles.flatMap(([role, command]) => (isCommand(command) ? [] : [`the program ${role} is not a command`])),\n ];\n};\n\n/**\n * This function gives each `FROM` of a Dockerfile that no digest pins. A `FROM` of an earlier stage\n * by its name, or of `scratch`, needs no digest. A base from an `ARG` has no digest that the harness\n * can read, thus it is in the list too.\n */\nexport const unpinnedBases = (dockerfile: string): string[] => {\n const stages = new Set<string>([\"scratch\"]);\n const found: string[] = [];\n for (const line of dockerfile.replace(/\\\\\\r?\\n/g, \" \").split(/\\r?\\n/)) {\n const from = /^\\s*FROM\\s+(.+)$/i.exec(line)?.[1];\n if (from === undefined) continue;\n const [image, as, stage] = from.split(/\\s+/).filter((word) => !word.startsWith(\"--\"));\n if (image === undefined) continue;\n if (!stages.has(image.toLowerCase()) && !PINNED.test(image)) found.push(image);\n if (as?.toLowerCase() === \"as\" && stage !== undefined) stages.add(stage.toLowerCase());\n }\n return found;\n};\n\n/** These are the lockfiles of each manifest of packages. A manifest without a lockfile installs other packages each time. */\nconst LOCKFILES: Readonly<Record<string, readonly string[]>> = {\n \"package.json\": [\"package-lock.json\", \"npm-lock.json\", \"npm-shrinkwrap.json\", \"pnpm-lock.yaml\", \"yarn.lock\", \"bun.lock\"],\n \"Cargo.toml\": [\"Cargo.lock\"],\n};\n\n/**\n * This function gives each manifest of packages in a context (`package.json`, `Cargo.toml`) that\n * has no lockfile in its folder or in a folder above it. The paths are the paths in the context.\n */\nexport const unlockedManifests = (paths: readonly string[]): string[] => {\n const present = new Set(paths);\n return paths.filter((path) => {\n const parts = path.split(\"/\");\n const locks = LOCKFILES[parts.at(-1)!];\n if (locks === undefined) return false;\n for (let depth = parts.length - 1; depth >= 0; depth -= 1) {\n const folder = parts.slice(0, depth).join(\"/\");\n if (locks.some((lock) => present.has(folder === \"\" ? lock : `${folder}/${lock}`))) return false;\n }\n return true;\n });\n};\n\n/**\n * An image plan tells how the harness gets the image of a source. It is a pull of an image by its\n * name, or a build of a context. A build has its tag, its Dockerfile, and each file with its path.\n */\nexport type ImagePlan =\n | { readonly kind: \"pull\"; readonly image: string }\n | {\n readonly kind: \"build\";\n readonly tag: string;\n readonly dockerfile: string;\n readonly files: ReadonlyArray<{ readonly file: string; readonly at: string }>;\n };\n\n/** This function gives each file under a path, in a stable sequence, but not the output of a local build. */\nconst sources = (path: string): string[] => {\n if (statSync(path).isFile()) return [path];\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.name !== \"target\" && entry.name !== \"node_modules\")\n .flatMap((entry) => sources(join(path, entry.name)))\n .sort();\n};\n\n/** A part is a file or a folder of a context: its path on the machine, and its path in the context. */\ninterface Part {\n readonly path: string;\n readonly at: string;\n}\n\n/** This function gives the name, the Dockerfile and the parts of the context of a build or of a shipped image. */\nconst contextOf = (source: BuildImage | ShippedImage): { name: string; dockerfile: string; parts: Part[] } => {\n if (source.kind === \"shipped\") {\n const { name, dockerfile, from } = SHIPPED[source.name];\n return { name, dockerfile, parts: from.map((part) => ({ path: pathOf(part), at: part })) };\n }\n const root = resolve(source.context);\n return {\n name: source.name,\n dockerfile: posix.normalize(source.dockerfile ?? \"Dockerfile\"),\n parts: (source.files ?? [\".\"]).map((file) => ({ path: resolve(root, file), at: file === \".\" ? \"\" : file })),\n };\n};\n\n/** This function gives a text for a source in a message. */\nconst labelOf = (source: ImageSource): string =>\n source.kind === \"pinned\" ? source.image : `the ${source.kind === \"build\" ? \"build\" : \"shipped image\"} ${source.name}`;\n\nconst shippedPlans = new Map<Shipped, ImagePlan>();\n\n/**\n * This function gives the plan of the image of a source. A stock image by its name, or a pinned\n * image, is a pull. A build or a shipped image is a build. Its tag is its name, then 12 digits of a\n * sha256. The sha256 reads the path of the Dockerfile, and the path and the content of each file.\n * Thus a changed program cannot operate as an old one.\n *\n * A build fails before it starts on a problem of its source. It also fails on a `FROM` that no\n * digest pins, and on a manifest of packages without a lockfile.\n */\nexport const planOf = (source: string | ImageSource): ImagePlan => {\n if (typeof source === \"string\") return { kind: \"pull\", image: source };\n const problems = imageProblems(source);\n if (problems.length > 0) throw new Error(`${labelOf(source)}: ${problems.join(\", \")}`);\n if (source.kind === \"pinned\") return { kind: \"pull\", image: source.image };\n const known = source.kind === \"shipped\" ? shippedPlans.get(source.name) : undefined;\n if (known !== undefined) return known;\n const { name, dockerfile, parts } = contextOf(source);\n const files = parts.flatMap((part) =>\n sources(part.path).map((file) => ({ file, at: join(part.at, relative(part.path, file)).replaceAll(\"\\\\\", \"/\") })),\n );\n const docker = files.find((each) => each.at === dockerfile);\n if (docker === undefined) throw new Error(`${labelOf(source)}: the context has no ${dockerfile}`);\n const unpinned = unpinnedBases(readFileSync(docker.file, \"utf8\"));\n const unlocked = unlockedManifests(files.map((each) => each.at));\n if (unpinned.length > 0 || unlocked.length > 0)\n throw new Error(\n `${labelOf(source)}: ${[\n ...unpinned.map((image) => `the base ${image} is not pinned by digest`),\n ...unlocked.map((path) => `${path} has no lockfile`),\n ].join(\", \")}`,\n );\n const hash = createHash(\"sha256\");\n hash.update(dockerfile);\n for (const { file, at } of files) {\n hash.update(at);\n hash.update(readFileSync(file));\n }\n const plan: ImagePlan = { kind: \"build\", tag: `${name}:${hash.digest(\"hex\").slice(0, 12)}`, dockerfile, files };\n if (source.kind === \"shipped\") shippedPlans.set(source.name, plan);\n return plan;\n};\n\n/** These counts are how many lines of the output of a failed build its error holds: its error lines, then its last lines. */\nconst BUILD_ERRORS = 10;\nconst BUILD_TAIL = 15;\n\n/**\n * This function gives the lines of the output of a failed build that its error holds. They are each\n * line that names an error, for example the diagnostic of a compiler, then the last lines.\n */\nexport const buildFailure = (output: string): string => {\n const lines = output.trim().split(/\\r?\\n/);\n const tail = lines.slice(-BUILD_TAIL);\n const errors = lines.slice(0, -BUILD_TAIL).filter((line) => /\\berror\\b/i.test(line)).slice(0, BUILD_ERRORS);\n return [...errors, ...(errors.length > 0 ? [\"...\"] : []), ...tail].join(\"\\n\");\n};\n\n/**\n * This function builds a staged context with the CLI of Docker, thus with BuildKit where the CLI has\n * it. Testcontainers reads nothing of the output of a build, and it fails with \"Failed to build\n * image\" only. The error of a failed build here holds its error lines and the end of its output.\n */\nconst dockerBuild = (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>): Promise<void> =>\n new Promise((resolve, reject) => {\n const args = [\"build\", \"--progress=plain\", \"--tag\", plan.tag, \"--file\", join(context, plan.dockerfile), context];\n execFile(\"docker\", args, { maxBuffer: 64 * 1024 * 1024 }, (error, _stdout, stderr) => {\n if (error === null) {\n resolve();\n return;\n }\n reject(new Error(`docker build of ${plan.tag} failed (${error.message.split(\"\\n\")[0]}):\\n${buildFailure(String(stderr))}`));\n });\n });\n\nconst built = new Map<string, Promise<string>>();\n\n/**\n * These are the waits before each next try of a build that fails, in milliseconds. A fault of a\n * registry or of the network often stops in seconds. In run 39278390448 of the cells, two runners\n * failed a build in the same second, and the same builds then passed.\n */\nexport const BUILD_WAITS: readonly number[] = [5_000, 20_000];\n\n/**\n * This function builds the image of a plan one time for each process, from a new context that it\n * copies the files into. Docker keeps its layers between runs. A build that fails tries again after\n * each wait (`waits`), and it writes each failure to stderr. A build that fails each try leaves the\n * cache. Thus a later cell builds again after a fault of Docker or of the network.\n */\nexport const buildOnce = (\n plan: Extract<ImagePlan, { readonly kind: \"build\" }>,\n builder: (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>) => Promise<void> = dockerBuild,\n waits: readonly number[] = BUILD_WAITS,\n): Promise<string> => {\n let tag = built.get(plan.tag);\n if (tag === undefined) {\n const building = (async () => {\n const context = mkdtempSync(join(tmpdir(), `${plan.tag.split(\":\")[0]!.replaceAll(\"/\", \"-\")}-`));\n try {\n for (const { file, at } of plan.files) {\n mkdirSync(dirname(join(context, at)), { recursive: true });\n copyFileSync(file, join(context, at));\n }\n for (let attempt = 0; ; attempt += 1) {\n try {\n await builder(context, plan);\n break;\n } catch (error) {\n const wait = waits[attempt];\n if (wait === undefined) throw error;\n process.stderr.write(`the build of ${plan.tag} failed (${error instanceof Error ? error.message : String(error)}): try ${attempt + 2} after ${wait} ms\\n`);\n await new Promise((resolve) => setTimeout(resolve, wait));\n }\n }\n } finally {\n rmSync(context, { recursive: true, force: true });\n }\n return plan.tag;\n })();\n // a failed build does not stay: the next caller tries again\n building.catch(() => {\n if (built.get(plan.tag) === building) built.delete(plan.tag);\n });\n built.set(plan.tag, building);\n tag = building;\n }\n return tag;\n};\n\n/**\n * This function gives an image that a lab can start, from its source. It is the name of a stock\n * image or of a pinned image, which the lab pulls, or the tag of a build. A build goes through the CLI\n * of Docker.\n */\nexport const imageOf = async (source: string | ImageSource): Promise<string> => {\n const plan = planOf(source);\n return plan.kind === \"pull\" ? plan.image : buildOnce(plan);\n};\n\n/** This function gives the image of the harness: the prober, the TCP client, the injector and the namespace admin. */\nexport const harnessImage = (): Promise<string> => imageOf(shipped(\"harness\"));\n/** This function gives the image of the Node client. */\nexport const nodeClientImage = (): Promise<string> => imageOf(shipped(\"node\"));\n/** This function gives the image of the router. */\nexport const routerImage = (): Promise<string> => imageOf(shipped(\"router\"));\n/** This function gives the image of the Go HTTP client. */\nexport const goHttpImage = (): Promise<string> => imageOf(shipped(\"go-http\"));\n/** This function gives the image of the Node HTTP clients. */\nexport const nodeHttpImage = (): Promise<string> => imageOf(shipped(\"node-http\"));\n/** This function gives the image of the MongoDB client. */\nexport const mongoClientImage = (): Promise<string> => imageOf(shipped(\"mongo\"));\n/** This function gives the image of the PostgreSQL client. */\nexport const pgClientImage = (): Promise<string> => imageOf(shipped(\"pg\"));\n/** This function gives the image of the client of HikariCP over pgjdbc. */\nexport const hikariClientImage = (): Promise<string> => imageOf(shipped(\"hikari\"));\n/** This function gives the image of the RabbitMQ client: amqplib, with the client and the admin of a cluster cell. */\nexport const amqpClientImage = (): Promise<string> => imageOf(shipped(\"amqp\"));\n\n/** This function gives the image of the gRPC client of the harness: grpc-js 1.14.6 on Node 24.21.0, with its client. */\nexport const grpcClientImage = (): Promise<string> => imageOf(shipped(\"grpc\"));\n\n/**\n * This function gives the source of the image that a target operates. It is the image of its\n * subject (`Target.subject`), or its stock image.\n */\nexport const targetSourceOf = (target: Target): string | ImageSource => target.subject ?? target.image;\n\n/** This function gives the image that a target operates: the image of its subject, or its stock image. */\nexport const targetImageOf = (target: Target): Promise<string> => imageOf(targetSourceOf(target));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,QAA0C;CAC9C,SAAS,cAAc,IAAI,IAAI,MAAM,YAAY,GAAG,CAAC;CACrD,QAAQ,QAAQ,aAAa,cAAc,YAAY,GAAG,CAAC,CAAC,QAAQ,oCAAoC,CAAC,CAAC;AAC5G;;AAGA,MAAM,UAAU,SAAyB;CACvC,MAAM,CAAC,MAAM,GAAG,QAAQ,KAAK,MAAM,GAAG;CACtC,MAAM,OAAO,MAAM;CACnB,IAAI,SAAS,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,KAAK,uCAAuC;CACvF,OAAO,KAAK,MAAM,GAAG,IAAI;AAC3B;;AAUA,MAAM,UAAqD;CAEzD,SAAS;EACP,MAAM;EACN,YAAY;EACZ,MAAM;GAAC;GAA4B;GAAwB;GAAc;GAAiB;EAAiB;CAC7G;CAEA,MAAM;EAAE,MAAM;EAAqB,YAAY;EAAiC,MAAM,CAAC,oBAAoB;CAAE;CAE7G,WAAW;EAAE,MAAM;EAAwB,YAAY;EAAoC,MAAM,CAAC,uBAAuB;CAAE;CAE3H,aAAa;EAAE,MAAM;EAA0B,YAAY;EAAsC,MAAM,CAAC,yBAAyB;CAAE;CAEnI,OAAO;EAAE,MAAM;EAAsB,YAAY;EAAkC,MAAM,CAAC,qBAAqB;CAAE;CAEjH,IAAI;EAAE,MAAM;EAAmB,YAAY;EAA+B,MAAM,CAAC,kBAAkB;CAAE;CAErG,QAAQ;EAAE,MAAM;EAAuB,YAAY;EAAmC,MAAM,CAAC,sBAAsB;CAAE;CAErH,MAAM;EAAE,MAAM;EAAqB,YAAY;EAAiC,MAAM,CAAC,oBAAoB;CAAE;CAE7G,MAAM;EAAE,MAAM;EAAqB,YAAY;EAAiC,MAAM,CAAC,oBAAoB;CAAE;CAE7G,WAAW;EAAE,MAAM;EAAwB,YAAY;EAAoC,MAAM,CAAC,uBAAuB;CAAE;CAE3H,MAAM;EAAE,MAAM;EAAqB,YAAY;EAAiC,MAAM,CAAC,oBAAoB;CAAE;CAE7G,QAAQ;EAAE,MAAM;EAAuB,YAAY;EAAmC,MAAM,CAAC,sBAAsB;CAAE;CAErH,WAAW;EAAE,MAAM;EAA0B,YAAY;EAAsC,MAAM,CAAC,yBAAyB;CAAE;CAEjI,QAAQ;EAAE,MAAM;EAAuB,YAAY;EAAmC,MAAM,CAAC,sBAAsB;CAAE;CAErH,gBAAgB;EAAE,MAAM;EAA6B,YAAY;EAAyC,MAAM,CAAC,4BAA4B;CAAE;AACjJ;;AAGA,MAAa,gBAAkD,OAAO,KAAK,OAAO;;AAGlF,MAAM,WAAW,UAAgC;CAAE,MAAM;CAAW;AAAK;;;;;AAMzE,MAAa,kBAAyD;CACpE,KAAK;EAAE,OAAO,QAAQ,SAAS;EAAG,SAAS,CAAC,qBAAqB;CAAE;CACnE,SAAS;EAAE,OAAO,QAAQ,MAAM;EAAG,SAAS,CAAC,QAAQ,sCAAsC;CAAE;CAC7F,WAAW;EAAE,OAAO,QAAQ,SAAS;EAAG,SAAS,CAAC,wBAAwB;CAAE;CAC5E,cAAc;EAAE,OAAO,QAAQ,WAAW;EAAG,SAAS,CAAC,QAAQ,yCAAyC;CAAE;CAC1G,QAAQ;EAAE,OAAO,QAAQ,WAAW;EAAG,SAAS,CAAC,QAAQ,0CAA0C;CAAE;CACrG,SAAS;EAAE,OAAO,QAAQ,OAAO;EAAG,SAAS,CAAC,QAAQ,qCAAqC;CAAE;CAC7F,IAAI;EAAE,OAAO,QAAQ,IAAI;EAAG,SAAS,CAAC,QAAQ,+BAA+B;CAAE;CAC/E,QAAQ;EACN,OAAO,QAAQ,QAAQ;EACvB,SAAS;GAAC;GAAQ;GAAoC;GAAO;GAAyD;EAAc;CACtI;CACA,MAAM;EAAE,OAAO,QAAQ,MAAM;EAAG,SAAS,CAAC,QAAQ,mCAAmC;CAAE;CACvF,MAAM;EAAE,OAAO,QAAQ,MAAM;EAAG,SAAS,CAAC,QAAQ,mCAAmC;CAAE;AACzF;;AAGA,MAAa,eAA+C,OAAO,KAAK,eAAe;;AAGvF,MAAa,aAAa,SAAkB,SAAoC;CAC9E,MAAM,UAAU,OAAO,OAAO,QAAQ,UAAU,IAAI,IAAI,QAAQ,SAAS,QAAQ,KAAA;CACjF,IAAI,YAAY,KAAA,GAAW,MAAM,IAAI,MAAM,eAAe,QAAQ,KAAK,kBAAkB,MAAM;CAC/F,OAAO;AACT;;;;;;AAOA,MAAa,iBAAiB,SAAkB,OAAO,aAAmC;CACxF,MAAM,SAAsB;EAAE,OAAO,QAAQ;EAAO,SAAS,UAAU,SAAS,IAAI;CAAE;CACtF,OAAO,aAAa,MAAM,SAAS,kBAAkB,gBAAgB,OAAO,MAAM,CAAC,KAAK;AAC1F;;AAGA,MAAa,SAAiB;;AAE9B,MAAa,aAAqB;;AAGlC,MAAa,aAAa,SACxB,OAAO,SAAS,YAChB,KAAK,SAAS,KACd,CAAC,KAAK,WAAW,GAAG,KACpB,CAAC,KAAK,SAAS,IAAI,KACnB,CAAC,aAAa,KAAK,IAAI,KACvB,CAAC,KAAK,MAAM,GAAG,CAAC,CAAC,SAAS,IAAI;;AAGhC,MAAM,aAAa,UACjB,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,KAAK,MAAM,OAAO,SAAS,OAAO,SAAS,YAAY,KAAK,SAAS,CAAC;;;;;;AAO/G,MAAa,iBAAiB,WAAkC;CAC9D,MAAM,OAAQ,QAA+C;CAC7D,IAAI,SAAS,UAAU;EACrB,MAAM,EAAE,UAAU;EAClB,OAAO,OAAO,UAAU,YAAY,OAAO,KAAK,KAAK,IAAI,CAAC,IAAI,CAAC,GAAG,OAAO,KAAK,EAAE,sDAAsD;CACxI;CACA,IAAI,SAAS,WAAW;EACtB,MAAM,EAAE,SAAS;EACjB,OAAO,OAAO,SAAS,YAAY,OAAO,OAAO,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,8BAA8B,OAAO,IAAI,EAAE,IAAI,cAAc,KAAK,IAAI,GAAG;CACnJ;CACA,IAAI,SAAS,SAAS;EACpB,MAAM,QAAQ;EACd,MAAM,WAAqB,CAAC;EAC5B,IAAI,OAAO,MAAM,SAAS,YAAY,CAAC,WAAW,KAAK,MAAM,IAAI,GAAG,SAAS,KAAK,GAAG,OAAO,MAAM,IAAI,EAAE,iCAAiC;EACzI,IAAI,OAAO,MAAM,YAAY,YAAY,MAAM,QAAQ,WAAW,GAAG,SAAS,KAAK,4BAA4B;EAC/G,IAAI,MAAM,UAAU,KAAA,MAAc,CAAC,MAAM,QAAQ,MAAM,KAAK,KAAK,MAAM,MAAM,WAAW,IAAI,SAAS,KAAK,0BAA0B;EACpI,KAAK,MAAM,QAAQ,CAAC,MAAM,cAAc,cAAc,GAAI,MAAM,QAAQ,MAAM,KAAK,IAAI,MAAM,QAAQ,CAAC,CAAE,GACtG,IAAI,CAAC,UAAU,IAAI,GAAG,SAAS,KAAK,GAAG,OAAO,IAAI,EAAE,8BAA8B;EACpF,OAAO;CACT;CACA,OAAO,CAAC,GAAG,KAAK,UAAU,IAAI,EAAE,yDAAyD;AAC3F;;;;;AAMA,MAAa,mBAAmB,YAA+B;CAC7D,MAAM,WAAY,QAA4C;CAC9D,MAAM,QAAQ,OAAO,aAAa,YAAY,aAAa,OAAO,OAAO,QAAQ,QAAQ,IAAI,CAAC;CAC9F,OAAO;EACL,GAAI,OAAO,QAAQ,SAAS,YAAY,QAAQ,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,yBAAyB;EACjG,GAAI,OAAO,QAAQ,SAAS,YAAY,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,OAAO,QAAQ,IAAI,EAAE,yBAAyB;EACpI,GAAG,cAAc,QAAQ,KAAK;EAC9B,GAAI,MAAM,WAAW,IAAI,CAAC,6BAA6B,IAAI,CAAC;EAC5D,GAAG,MAAM,SAAS,CAAC,MAAM,aAAc,UAAU,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,KAAK,kBAAkB,CAAE;CAC5G;AACF;;;;;;AAOA,MAAa,iBAAiB,eAAiC;CAC7D,MAAM,yBAAS,IAAI,IAAY,CAAC,SAAS,CAAC;CAC1C,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,QAAQ,WAAW,QAAQ,YAAY,GAAG,CAAC,CAAC,MAAM,OAAO,GAAG;EACrE,MAAM,OAAO,oBAAoB,KAAK,IAAI,CAAC,GAAG;EAC9C,IAAI,SAAS,KAAA,GAAW;EACxB,MAAM,CAAC,OAAO,IAAI,SAAS,KAAK,MAAM,KAAK,CAAC,CAAC,QAAQ,SAAS,CAAC,KAAK,WAAW,IAAI,CAAC;EACpF,IAAI,UAAU,KAAA,GAAW;EACzB,IAAI,CAAC,OAAO,IAAI,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,KAAK,KAAK,GAAG,MAAM,KAAK,KAAK;EAC7E,IAAI,IAAI,YAAY,MAAM,QAAQ,UAAU,KAAA,GAAW,OAAO,IAAI,MAAM,YAAY,CAAC;CACvF;CACA,OAAO;AACT;;AAGA,MAAM,YAAyD;CAC7D,gBAAgB;EAAC;EAAqB;EAAiB;EAAuB;EAAkB;EAAa;CAAU;CACvH,cAAc,CAAC,YAAY;AAC7B;;;;;AAMA,MAAa,qBAAqB,UAAuC;CACvE,MAAM,UAAU,IAAI,IAAI,KAAK;CAC7B,OAAO,MAAM,QAAQ,SAAS;EAC5B,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC5B,MAAM,QAAQ,UAAU,MAAM,GAAG,EAAE;EACnC,IAAI,UAAU,KAAA,GAAW,OAAO;EAChC,KAAK,IAAI,QAAQ,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;GACzD,MAAM,SAAS,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG;GAC7C,IAAI,MAAM,MAAM,SAAS,QAAQ,IAAI,WAAW,KAAK,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,OAAO;EAC5F;EACA,OAAO;CACT,CAAC;AACH;;AAgBA,MAAM,WAAW,SAA2B;CAC1C,IAAI,SAAS,IAAI,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC,IAAI;CACzC,OAAO,YAAY,MAAM,EAAE,eAAe,KAAK,CAAC,CAAC,CAC9C,QAAQ,UAAU,MAAM,SAAS,YAAY,MAAM,SAAS,cAAc,CAAC,CAC3E,SAAS,UAAU,QAAQ,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,CAAC,CACnD,KAAK;AACV;;AASA,MAAM,aAAa,WAA2F;CAC5G,IAAI,OAAO,SAAS,WAAW;EAC7B,MAAM,EAAE,MAAM,YAAY,SAAS,QAAQ,OAAO;EAClD,OAAO;GAAE;GAAM;GAAY,OAAO,KAAK,KAAK,UAAU;IAAE,MAAM,OAAO,IAAI;IAAG,IAAI;GAAK,EAAE;EAAE;CAC3F;CACA,MAAM,OAAO,QAAQ,OAAO,OAAO;CACnC,OAAO;EACL,MAAM,OAAO;EACb,YAAY,MAAM,UAAU,OAAO,cAAc,YAAY;EAC7D,QAAQ,OAAO,SAAS,CAAC,GAAG,EAAA,CAAG,KAAK,UAAU;GAAE,MAAM,QAAQ,MAAM,IAAI;GAAG,IAAI,SAAS,MAAM,KAAK;EAAK,EAAE;CAC5G;AACF;;AAGA,MAAM,WAAW,WACf,OAAO,SAAS,WAAW,OAAO,QAAQ,OAAO,OAAO,SAAS,UAAU,UAAU,gBAAgB,GAAG,OAAO;AAEjH,MAAM,+BAAe,IAAI,IAAwB;;;;;;;;;;AAWjD,MAAa,UAAU,WAA4C;CACjE,IAAI,OAAO,WAAW,UAAU,OAAO;EAAE,MAAM;EAAQ,OAAO;CAAO;CACrE,MAAM,WAAW,cAAc,MAAM;CACrC,IAAI,SAAS,SAAS,GAAG,MAAM,IAAI,MAAM,GAAG,QAAQ,MAAM,EAAE,IAAI,SAAS,KAAK,IAAI,GAAG;CACrF,IAAI,OAAO,SAAS,UAAU,OAAO;EAAE,MAAM;EAAQ,OAAO,OAAO;CAAM;CACzE,MAAM,QAAQ,OAAO,SAAS,YAAY,aAAa,IAAI,OAAO,IAAI,IAAI,KAAA;CAC1E,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,EAAE,MAAM,YAAY,UAAU,UAAU,MAAM;CACpD,MAAM,QAAQ,MAAM,SAAS,SAC3B,QAAQ,KAAK,IAAI,CAAC,CAAC,KAAK,UAAU;EAAE;EAAM,IAAI,KAAK,KAAK,IAAI,SAAS,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,WAAW,MAAM,GAAG;CAAE,EAAE,CACjH;CACA,MAAM,SAAS,MAAM,MAAM,SAAS,KAAK,OAAO,UAAU;CAC1D,IAAI,WAAW,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,MAAM,EAAE,uBAAuB,YAAY;CAChG,MAAM,WAAW,cAAc,aAAa,OAAO,MAAM,MAAM,CAAC;CAChE,MAAM,WAAW,kBAAkB,MAAM,KAAK,SAAS,KAAK,EAAE,CAAC;CAC/D,IAAI,SAAS,SAAS,KAAK,SAAS,SAAS,GAC3C,MAAM,IAAI,MACR,GAAG,QAAQ,MAAM,EAAE,IAAI,CACrB,GAAG,SAAS,KAAK,UAAU,YAAY,MAAM,yBAAyB,GACtE,GAAG,SAAS,KAAK,SAAS,GAAG,KAAK,iBAAiB,CACrD,CAAC,CAAC,KAAK,IAAI,GACb;CACF,MAAM,OAAO,WAAW,QAAQ;CAChC,KAAK,OAAO,UAAU;CACtB,KAAK,MAAM,EAAE,MAAM,QAAQ,OAAO;EAChC,KAAK,OAAO,EAAE;EACd,KAAK,OAAO,aAAa,IAAI,CAAC;CAChC;CACA,MAAM,OAAkB;EAAE,MAAM;EAAS,KAAK,GAAG,KAAK,GAAG,KAAK,OAAO,KAAK,CAAC,CAAC,MAAM,GAAG,EAAE;EAAK;EAAY;CAAM;CAC9G,IAAI,OAAO,SAAS,WAAW,aAAa,IAAI,OAAO,MAAM,IAAI;CACjE,OAAO;AACT;;AAGA,MAAM,eAAe;;;;;AAOrB,MAAa,gBAAgB,WAA2B;CACtD,MAAM,QAAQ,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;CACzC,MAAM,OAAO,MAAM,MAAM,GAAW;CACpC,MAAM,SAAS,MAAM,MAAM,GAAG,GAAW,CAAC,CAAC,QAAQ,SAAS,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,YAAY;CAC1G,OAAO;EAAC,GAAG;EAAQ,GAAI,OAAO,SAAS,IAAI,CAAC,KAAK,IAAI,CAAC;EAAI,GAAG;CAAI,CAAC,CAAC,KAAK,IAAI;AAC9E;;;;;;AAOA,MAAM,eAAe,SAAiB,SACpC,IAAI,SAAS,SAAS,WAAW;CAC/B,MAAM,OAAO;EAAC;EAAS;EAAoB;EAAS,KAAK;EAAK;EAAU,KAAK,SAAS,KAAK,UAAU;EAAG;CAAO;CAC/G,SAAS,UAAU,MAAM,EAAE,WAAW,SAAiB,IAAI,OAAO,SAAS,WAAW;EACpF,IAAI,UAAU,MAAM;GAClB,QAAQ;GACR;EACF;EACA,uBAAO,IAAI,MAAM,mBAAmB,KAAK,IAAI,WAAW,MAAM,QAAQ,MAAM,IAAI,CAAC,CAAC,GAAG,MAAM,aAAa,OAAO,MAAM,CAAC,GAAG,CAAC;CAC5H,CAAC;AACH,CAAC;AAEH,MAAM,wBAAQ,IAAI,IAA6B;;;;;;AAO/C,MAAa,cAAiC,CAAC,KAAO,GAAM;;;;;;;AAQ5D,MAAa,aACX,MACA,UAAoG,aACpG,QAA2B,gBACP;CACpB,IAAI,MAAM,MAAM,IAAI,KAAK,GAAG;CAC5B,IAAI,QAAQ,KAAA,GAAW;EACrB,MAAM,YAAY,YAAY;GAC5B,MAAM,UAAU,YAAY,KAAK,OAAO,GAAG,GAAG,KAAK,IAAI,MAAM,GAAG,CAAC,CAAC,EAAE,CAAE,WAAW,KAAK,GAAG,EAAE,EAAE,CAAC;GAC9F,IAAI;IACF,KAAK,MAAM,EAAE,MAAM,QAAQ,KAAK,OAAO;KACrC,UAAU,QAAQ,KAAK,SAAS,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;KACzD,aAAa,MAAM,KAAK,SAAS,EAAE,CAAC;IACtC;IACA,KAAK,IAAI,UAAU,IAAK,WAAW,GACjC,IAAI;KACF,MAAM,QAAQ,SAAS,IAAI;KAC3B;IACF,SAAS,OAAO;KACd,MAAM,OAAO,MAAM;KACnB,IAAI,SAAS,KAAA,GAAW,MAAM;KAC9B,QAAQ,OAAO,MAAM,gBAAgB,KAAK,IAAI,WAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,SAAS,UAAU,EAAE,SAAS,KAAK,MAAM;KACzJ,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,IAAI,CAAC;IAC1D;GAEJ,UAAU;IACR,OAAO,SAAS;KAAE,WAAW;KAAM,OAAO;IAAK,CAAC;GAClD;GACA,OAAO,KAAK;EACd,EAAA,CAAG;EAEH,SAAS,YAAY;GACnB,IAAI,MAAM,IAAI,KAAK,GAAG,MAAM,UAAU,MAAM,OAAO,KAAK,GAAG;EAC7D,CAAC;EACD,MAAM,IAAI,KAAK,KAAK,QAAQ;EAC5B,MAAM;CACR;CACA,OAAO;AACT;;;;;;AAOA,MAAa,UAAU,OAAO,WAAkD;CAC9E,MAAM,OAAO,OAAO,MAAM;CAC1B,OAAO,KAAK,SAAS,SAAS,KAAK,QAAQ,UAAU,IAAI;AAC3D;;AAGA,MAAa,qBAAsC,QAAQ,QAAQ,SAAS,CAAC;;AAE7E,MAAa,wBAAyC,QAAQ,QAAQ,MAAM,CAAC;;AAE7E,MAAa,oBAAqC,QAAQ,QAAQ,QAAQ,CAAC;;AAE3E,MAAa,oBAAqC,QAAQ,QAAQ,SAAS,CAAC;;AAE5E,MAAa,sBAAuC,QAAQ,QAAQ,WAAW,CAAC;;AAEhF,MAAa,yBAA0C,QAAQ,QAAQ,OAAO,CAAC;;AAE/E,MAAa,sBAAuC,QAAQ,QAAQ,IAAI,CAAC;;AAEzE,MAAa,0BAA2C,QAAQ,QAAQ,QAAQ,CAAC;;AAEjF,MAAa,wBAAyC,QAAQ,QAAQ,MAAM,CAAC;;AAG7E,MAAa,wBAAyC,QAAQ,QAAQ,MAAM,CAAC;;;;;AAM7E,MAAa,kBAAkB,WAAyC,OAAO,WAAW,OAAO;;AAGjG,MAAa,iBAAiB,WAAoC,QAAQ,eAAe,MAAM,CAAC"}
|
package/build/index.d.ts
CHANGED
|
@@ -1,11 +1,48 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as Subject, A as Prediction, B as RequestPrediction, C as KernelRecord, D as MiddleboxForm, E as MiddleboxExpectation, F as Recovers, G as RunPart, H as RttRange, I as Reference, J as SentinelStage, K as RunTopology, L as ReplicaSet, M as ProxyHop, N as ProxyTime, O as Observer, P as Reconnects, Q as StageBand, R as ReplicaSetStage, S as ImageSource, T as MiddleboxEvent, U as Run, V as RouterHop, W as RunGroup, X as ShippedImage, Y as Shipped, Z as Stage, _ as HopEvent, a as Cluster, at as latestEndOf, b as HopWrite, c as DriverBand, d as FaultPlace, et as Target, f as Group, g as Hop, h as GroupRole, i as ClientImage, it as isHopPlace, j as ProbeEvent, k as PinnedImage, l as DriverStep, m as GroupPrediction, n as Cell, nt as Toxic, o as ClusterClientAt, p as GroupPlace, q as SentinelEvent, r as Client, rt as WsProbe, s as ClusterStage, t as BuildImage, tt as Topology, u as Fault, v as HopPlace, w as LineMatch, x as HttpProbe, y as HopSignal, z as Replies } from "./cell-jEkGFgGL.js";
|
|
2
2
|
import { Quantity } from "@mark1russell7/quantity";
|
|
3
3
|
import "testcontainers";
|
|
4
4
|
//#region src/application.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* This module reads the acts of the program of the observer: its heartbeat, its drop of the
|
|
7
|
+
* connection, its reconnect and its call in flight. The kernel has no report of a detection by
|
|
8
|
+
* a program. Thus the program writes a line before each of its own acts, on CLOCK_MONOTONIC:
|
|
9
|
+
* - `ping` before each ping of its heartbeat, and `drop` before it closes the connection by itself.
|
|
10
|
+
* - `reconnecting` when it starts its reconnect timer, and `dial` before each new connection.
|
|
11
|
+
*
|
|
12
|
+
* The kernel then stamps each write of the program on the socket (`tx`) and each change of the
|
|
13
|
+
* state of the socket. A line of the client names its socket by its local address (`local`), and
|
|
14
|
+
* a line of a server names it by its peer (`peer`).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* This time bounds the handling of a drop by the program of the observer. It starts when the timer
|
|
18
|
+
* of the program fires. It ends at the kernel's stamp of the act: the write of a close frame, or
|
|
19
|
+
* the close of the socket. The `drop` line comes in that time, before the act.
|
|
20
|
+
*
|
|
21
|
+
* This bound is not δ_abs. δ_abs is the noise of the pipeline, and stage 0 calibrates it from a
|
|
22
|
+
* reset. The handling is the time of the code of the program, and it is longer:
|
|
23
|
+
* - No source and no machine document bounds it (`http/timers.ts` of `@mark1russell7/machines`).
|
|
24
|
+
* - The handling time of the HTTP cells (`HANDLING` in `packages/cells/src/http/race.ts`) is
|
|
25
|
+
* another act. It is the first reply of a new process, up to 11.182 ms.
|
|
26
|
+
* - A drop comes from a timer, long after the start of the process.
|
|
27
|
+
*
|
|
28
|
+
* The bound comes from the CI records of the WS and WST cells, from the `drop` line to the act:
|
|
29
|
+
* - 585 drops with a close frame had a median of 0.652 ms, a 99th percentile of 0.87 ms and a
|
|
30
|
+
* maximum of 1.017 ms.
|
|
31
|
+
* - 559 drops with a close of the socket had a maximum of 0.914 ms.
|
|
32
|
+
* - Two drops took more than the δ_abs of 1 ms. Each failed its cell, and its time was in the band.
|
|
33
|
+
* These are the runs 39390222333 and 39903020657 of the client repository.
|
|
34
|
+
*
|
|
35
|
+
* The bound is 1.5 ms: the maximum and a margin of 0.48 ms. The pairing of a drop with its act
|
|
36
|
+
* uses it, or δ_abs when δ_abs is longer. The band of a drop adds it to its high bound (the WS
|
|
37
|
+
* catalog of the cells). Read the records again after a change to the subject, to Node or to
|
|
38
|
+
* the runners.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DROP_HANDLING: Quantity;
|
|
5
41
|
/**
|
|
6
42
|
* A program event is a line of the program of the observer, with the kernel's stamp of its act.
|
|
7
|
-
* The act is a write, or a change of state of the socket. Without a record of the kernel in
|
|
8
|
-
* after the line, the instant is the stamp of the line.
|
|
43
|
+
* The act is a write, or a change of state of the socket. Without a record of the kernel in its
|
|
44
|
+
* window after the line, the instant is the stamp of the line. The window is δ_abs, and for a drop
|
|
45
|
+
* it is `DROP_HANDLING` when that is longer.
|
|
9
46
|
*/
|
|
10
47
|
interface ProgramEvent {
|
|
11
48
|
readonly line: ProbeEvent;
|
|
@@ -42,7 +79,8 @@ export declare const closeOrExit: (line: ProbeEvent, left: Quantity | null, exit
|
|
|
42
79
|
};
|
|
43
80
|
/**
|
|
44
81
|
* This function tells how a drop differs from the way that its prediction gives, or gives null.
|
|
45
|
-
* A close closes the socket
|
|
82
|
+
* A close closes the socket at once: in the handling time of the program (`DROP_HANDLING`), or in
|
|
83
|
+
* δ_abs (`delta`) when δ_abs is longer. A close frame is a write, and the program does not close
|
|
46
84
|
* the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
|
|
47
85
|
*/
|
|
48
86
|
export declare const dropFinding: (expected: "frame" | "close", drop: ProgramEvent, left: Quantity | null, delta?: Quantity, exited?: Quantity) => string | null;
|
|
@@ -56,7 +94,8 @@ export declare const firstUnanswered: (pings: readonly ProgramEvent[], socket: r
|
|
|
56
94
|
/**
|
|
57
95
|
* This function gives the first drop of the connection by its program. The kernel stamps it as the
|
|
58
96
|
* write of a close frame. Or it stamps the change of the socket out of ESTABLISHED: a close or an
|
|
59
|
-
* abort.
|
|
97
|
+
* abort. That stamp comes in the handling time of the program after the drop line
|
|
98
|
+
* (`DROP_HANDLING`), or in δ_abs (`delta`) when δ_abs is longer.
|
|
60
99
|
*/
|
|
61
100
|
export declare const dropOf: (lines: readonly ProbeEvent[], socket: readonly KernelRecord[], local: unknown, delta?: Quantity) => ProgramEvent | undefined;
|
|
62
101
|
/** A reconnect reading is what the client did after the detection, from its lines and from the records of the kernel. */
|
|
@@ -743,8 +782,6 @@ interface Manifest {
|
|
|
743
782
|
readonly image: string;
|
|
744
783
|
/** This source is the image of the subject that the target operates (`Target.subject`), with its stock base. */
|
|
745
784
|
readonly subject?: ImageSource;
|
|
746
|
-
/** This name is the image of the harness that the target operates, with its stock base (`Target.harness`). */
|
|
747
|
-
readonly harness?: string;
|
|
748
785
|
readonly base?: string;
|
|
749
786
|
readonly id: string;
|
|
750
787
|
readonly settings: Readonly<Record<string, string>>;
|
|
@@ -1018,12 +1055,6 @@ export declare const openLab: (target: Target, kind?: Client | ClientImage, topo
|
|
|
1018
1055
|
//#region src/image.d.ts
|
|
1019
1056
|
/** These are the names of the images that the harness ships. */
|
|
1020
1057
|
export declare const SHIPPED_NAMES: readonly [Shipped, ...Shipped[]];
|
|
1021
|
-
/**
|
|
1022
|
-
* These are the subjects that the harness ships, by their names. `procedures` is
|
|
1023
|
-
* `@procedures/client` on Node: its WebSocket client (`client-ws.mjs`) and its server
|
|
1024
|
-
* (`server-ws.mjs`), from one image. Thus the client and the server use one release.
|
|
1025
|
-
*/
|
|
1026
|
-
export declare const SUBJECTS: Readonly<Record<"procedures", Subject>>;
|
|
1027
1058
|
/**
|
|
1028
1059
|
* These are the clients that the harness ships, by their names (`Client`): the image of each, and
|
|
1029
1060
|
* its command. A cell that names a client gets this image and this command.
|
|
@@ -1119,11 +1150,9 @@ export declare const hikariClientImage: () => Promise<string>;
|
|
|
1119
1150
|
export declare const amqpClientImage: () => Promise<string>;
|
|
1120
1151
|
/** This function gives the image of the gRPC client of the harness: grpc-js 1.14.6 on Node 24.21.0, with its client. */
|
|
1121
1152
|
export declare const grpcClientImage: () => Promise<string>;
|
|
1122
|
-
/** This function gives the image of the procedures subject: the WebSocket client of `@procedures/client`, and its server. */
|
|
1123
|
-
export declare const proceduresImage: () => Promise<string>;
|
|
1124
1153
|
/**
|
|
1125
1154
|
* This function gives the source of the image that a target operates. It is the image of its
|
|
1126
|
-
* subject (`Target.subject`),
|
|
1155
|
+
* subject (`Target.subject`), or its stock image.
|
|
1127
1156
|
*/
|
|
1128
1157
|
export declare const targetSourceOf: (target: Target) => string | ImageSource;
|
|
1129
1158
|
/** This function gives the image that a target operates: the image of its subject, or its stock image. */
|
|
@@ -1633,5 +1662,5 @@ export declare const spread: (runs: number, environments: number) => number[];
|
|
|
1633
1662
|
*/
|
|
1634
1663
|
export declare const runCell: (cell: Cell, say?: (line: string) => void) => Promise<CellResult>;
|
|
1635
1664
|
//#endregion
|
|
1636
|
-
export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Cluster, type ClusterClientAt, type ClusterLab, type ClusterStage, type Crossings, type Cut, type Decision, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type
|
|
1665
|
+
export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Cluster, type ClusterClientAt, type ClusterLab, type ClusterStage, type Crossings, type Cut, type Decision, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type Hop, type HopEvent, type HopPlace, type HopSignal, type HopWrite, type HttpProbe, type ImagePlan, type ImageSource, type Interval, type KernelRecord, type Lab, type LineMatch, type Manifest, type ManifestHop, type Measured, type MemberLine, type MiddleboxEvent, type MiddleboxExpectation, type MiddleboxForm, type NetworkFault, type NodeLine, type Observer, type Outcome, type PinnedImage, type Prediction, type Probe, type ProbeEvent, type ProgramEvent, type ProxyHop, type ProxyTime, type ReconnectReading, type ReconnectVerdict, type Reconnects, type Recovers, type Reference, type Reordering, type ReplicaSet, type ReplicaSetLab, type ReplicaSetStage, type Replies, type RequestPrediction, type RequestReading, type RouterHop, type RttRange, type Run, type RunGroup, type RunPart, type RunTopology, type SentinelEvent, type SentinelStage, type Shipped, type ShippedImage, type SocketPlace, type Spread, type Stage, type StageBand, type StageVerdict, type Stamp, type Status, type StatusRow, type Subject, type Target, type Topology, type Toxic, type Verdict, type WallStamp, type Writes, type WsProbe, isHopPlace, latestEndOf };
|
|
1637
1666
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as targetImageOf, E as unpinnedBases, S as subjectProblems, T as unlockedManifests, _ as pgClientImage, a as SHIPPED_NAMES, b as routerImage, c as grpcClientImage, d as imageOf, f as imageProblems, g as nodeHttpImage, h as nodeClientImage, i as SHIPPED_CLIENTS, l as harnessImage, m as mongoClientImage, n as PINNED, o as amqpClientImage, p as inContext, r as REPOSITORY, s as goHttpImage, t as CLIENT_NAMES, u as hikariClientImage, v as planOf, w as targetSourceOf, x as subjectClient, y as programOf } from "./image-Lg8LL6C3.js";
|
|
2
2
|
import { quantity, valueIn } from "@mark1russell7/quantity";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
//#region src/cell.ts
|
|
@@ -463,6 +463,33 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
|
|
|
463
463
|
* state of the socket. A line of the client names its socket by its local address (`local`), and
|
|
464
464
|
* a line of a server names it by its peer (`peer`).
|
|
465
465
|
*/
|
|
466
|
+
/**
|
|
467
|
+
* This time bounds the handling of a drop by the program of the observer. It starts when the timer
|
|
468
|
+
* of the program fires. It ends at the kernel's stamp of the act: the write of a close frame, or
|
|
469
|
+
* the close of the socket. The `drop` line comes in that time, before the act.
|
|
470
|
+
*
|
|
471
|
+
* This bound is not δ_abs. δ_abs is the noise of the pipeline, and stage 0 calibrates it from a
|
|
472
|
+
* reset. The handling is the time of the code of the program, and it is longer:
|
|
473
|
+
* - No source and no machine document bounds it (`http/timers.ts` of `@mark1russell7/machines`).
|
|
474
|
+
* - The handling time of the HTTP cells (`HANDLING` in `packages/cells/src/http/race.ts`) is
|
|
475
|
+
* another act. It is the first reply of a new process, up to 11.182 ms.
|
|
476
|
+
* - A drop comes from a timer, long after the start of the process.
|
|
477
|
+
*
|
|
478
|
+
* The bound comes from the CI records of the WS and WST cells, from the `drop` line to the act:
|
|
479
|
+
* - 585 drops with a close frame had a median of 0.652 ms, a 99th percentile of 0.87 ms and a
|
|
480
|
+
* maximum of 1.017 ms.
|
|
481
|
+
* - 559 drops with a close of the socket had a maximum of 0.914 ms.
|
|
482
|
+
* - Two drops took more than the δ_abs of 1 ms. Each failed its cell, and its time was in the band.
|
|
483
|
+
* These are the runs 39390222333 and 39903020657 of the client repository.
|
|
484
|
+
*
|
|
485
|
+
* The bound is 1.5 ms: the maximum and a margin of 0.48 ms. The pairing of a drop with its act
|
|
486
|
+
* uses it, or δ_abs when δ_abs is longer. The band of a drop adds it to its high bound (the WS
|
|
487
|
+
* catalog of the cells). Read the records again after a change to the subject, to Node or to
|
|
488
|
+
* the runners.
|
|
489
|
+
*/
|
|
490
|
+
const DROP_HANDLING = quantity(1.5, "ms");
|
|
491
|
+
/** This function gives the window of the act of a drop after its line, in seconds: `DROP_HANDLING`, or δ_abs when δ_abs is longer. */
|
|
492
|
+
const dropWindow = (delta) => Math.max(valueIn(DROP_HANDLING, "s"), valueIn(delta, "s"));
|
|
466
493
|
/** This function gives the interval between two instants on the one clock, in seconds to the microsecond. */
|
|
467
494
|
const between$3 = (from, to) => quantity(Math.round((to - from) / 1e3) / 1e6, "s");
|
|
468
495
|
/** This function gives the state of a state record after the change, and before it. TCP_ESTABLISHED is 1, TCP_SYN_SENT 2. */
|
|
@@ -485,7 +512,7 @@ const onConnection = (line, local) => {
|
|
|
485
512
|
if (!("local" in line) && !("peer" in line)) return true;
|
|
486
513
|
return (line["local"] ?? line["peer"]) === local;
|
|
487
514
|
};
|
|
488
|
-
/** This function gives the first record of a socket that matches, at the stamp of a line or in
|
|
515
|
+
/** This function gives the first record of a socket that matches, at the stamp of a line or in a window (`delta`) after it. */
|
|
489
516
|
const kernelAt = (socket, line, matches, delta) => {
|
|
490
517
|
const noise = valueIn(delta, "s") * 1e9;
|
|
491
518
|
return socket.find((record) => record.mono >= line.t && record.mono <= line.t + noise && matches(record));
|
|
@@ -536,11 +563,12 @@ const closeOrExit = (line, left, exit, delta = NOISE) => {
|
|
|
536
563
|
};
|
|
537
564
|
/**
|
|
538
565
|
* This function tells how a drop differs from the way that its prediction gives, or gives null.
|
|
539
|
-
* A close closes the socket
|
|
566
|
+
* A close closes the socket at once: in the handling time of the program (`DROP_HANDLING`), or in
|
|
567
|
+
* δ_abs (`delta`) when δ_abs is longer. A close frame is a write, and the program does not close
|
|
540
568
|
* the socket at the drop. A peer that answers the close frame can close it, and that is no finding.
|
|
541
569
|
*/
|
|
542
570
|
const dropFinding = (expected, drop, left, delta = NOISE, exited) => {
|
|
543
|
-
const noise =
|
|
571
|
+
const noise = dropWindow(delta);
|
|
544
572
|
const soon = left !== null && valueIn(left, "s") <= noise;
|
|
545
573
|
if (expected === "close") {
|
|
546
574
|
if (left === null) return `the program did not close its socket after the drop, where a close is at once${exited === void 0 ? "" : ": it stayed open until the client process ended"}`;
|
|
@@ -560,11 +588,12 @@ const firstUnanswered = (pings, socket, until = Number.POSITIVE_INFINITY) => pin
|
|
|
560
588
|
/**
|
|
561
589
|
* This function gives the first drop of the connection by its program. The kernel stamps it as the
|
|
562
590
|
* write of a close frame. Or it stamps the change of the socket out of ESTABLISHED: a close or an
|
|
563
|
-
* abort.
|
|
591
|
+
* abort. That stamp comes in the handling time of the program after the drop line
|
|
592
|
+
* (`DROP_HANDLING`), or in δ_abs (`delta`) when δ_abs is longer.
|
|
564
593
|
*/
|
|
565
594
|
const dropOf = (lines, socket, local, delta = NOISE) => {
|
|
566
595
|
const line = lines.find((each) => each.ev === "drop" && onConnection(each, local));
|
|
567
|
-
return line === void 0 ? void 0 : eventOf(socket, line, (record) => isWrite(record) || record.ev === "state" && oldState$2(record) === 1, delta);
|
|
596
|
+
return line === void 0 ? void 0 : eventOf(socket, line, (record) => isWrite(record) || record.ev === "state" && oldState$2(record) === 1, quantity(dropWindow(delta), "s"));
|
|
568
597
|
};
|
|
569
598
|
/**
|
|
570
599
|
* This function reads the reconnect of the client after the detection at `detected`. The socket of
|
|
@@ -2959,12 +2988,9 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
2959
2988
|
docker: `${runtime.info.containerRuntime.serverVersion} on ${runtime.info.containerRuntime.operatingSystem}`,
|
|
2960
2989
|
target: {
|
|
2961
2990
|
image: targetImage,
|
|
2962
|
-
...target.subject
|
|
2991
|
+
...target.subject === void 0 ? {} : {
|
|
2963
2992
|
subject: target.subject,
|
|
2964
2993
|
base: target.image
|
|
2965
|
-
} : target.harness === void 0 ? {} : {
|
|
2966
|
-
harness: target.harness,
|
|
2967
|
-
base: target.image
|
|
2968
2994
|
},
|
|
2969
2995
|
id: (await inspect(server)).Image,
|
|
2970
2996
|
settings,
|
|
@@ -5702,6 +5728,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
5702
5728
|
}
|
|
5703
5729
|
};
|
|
5704
5730
|
//#endregion
|
|
5705
|
-
export { CLIENT_NAMES, CLUSTER_STAGES, CONVERGE_MS, GIVES_AWAY, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN,
|
|
5731
|
+
export { CLIENT_NAMES, CLUSTER_STAGES, CONVERGE_MS, DROP_HANDLING, GIVES_AWAY, INJECT, KEPT, LINGER_MS, LISTING, MEMBER_MARKS, MONITOR_DOWN, NODE_MARKS, NOISE, PINNED, PUMP, PUMP_SLACK, QUIET_MS, RELATIVE, REPLICA_SET_STAGES, REPOSITORY, SHIPPED_CLIENTS, SHIPPED_NAMES, STAGES, STATUS_SCRIPT, STEPS, STEP_DOWN, amqpClientImage, callOf, clockOffsetOf, closeOrExit, clusterArgs, clusterConf, clusterCutOf, clusterHostsOf, clusterOnce, clusterStagesOf, clusterViewOf, crossingsOf, cutOf, deadlineOf, delayVoids, deviceOf, dropFinding, dropOf, endpointAfter, errorsOf, exitOf, fieldOf, fieldsOf, firstUnanswered, goHttpImage, groupOnce, groupVerdictOf, grpcClientImage, harnessImage, hikariClientImage, hzOf, imageOf, imageProblems, inContext, infoOf, initiateScript, isHopPlace, judge, lagsOf, latestEndOf, leftAfter, logNameOf, matches, measure, measureDriver, measureGroup, medianInterval, memberCommand, memberConfig, memberLineOf, mongoClientImage, monotonicOf, nodeClientImage, nodeCommand, nodeHttpImage, nodeLineOf, nodeNameOf, nodesOnMonotonic, noiseOf, onMonotonic, openClusterLab, openGroupLab, openLab, openReplicaSetLab, peersOf, pgClientImage, pingsOf, plainOf, planOf, programLines, programOf, quantileOf, reconnectFinding, reconnectOf, referenceOf, reorderingOf, replicaSetArgs, replicaSetOnce, replicaSetStagesOf, requestOf, requestWords, restamped, routerImage, routesOf, runCell, sentinelConfig, socketAt, socketOf, socketsOf, spread, spreadOf, stagesOf, statusViewOf, subjectClient, subjectProblems, subscriptionsOf, targetImageOf, targetSourceOf, templated, toolJsonOf, toxicBody, unlockedManifests, unpinnedBases, verdictOf, viewOf, writesOf };
|
|
5706
5732
|
|
|
5707
5733
|
//# sourceMappingURL=index.js.map
|