@akterjs/cli 0.0.0-stage → 0.1.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.
Files changed (109) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +47 -2
  3. package/dist/Argument-D81dqizB.mjs +57 -0
  4. package/dist/Array-B4kuTQeW.mjs +2323 -0
  5. package/dist/Boolean-kILIvmhE.mjs +26 -0
  6. package/dist/ChildProcess-BCIwxPGc.mjs +577 -0
  7. package/dist/CliError-Cf5xqkkj.mjs +633 -0
  8. package/dist/Command-Dzq9KM-h.mjs +5135 -0
  9. package/dist/Config-B6OND9WR.mjs +854 -0
  10. package/dist/Console-BKHUkH6a.mjs +141 -0
  11. package/dist/Data-CeCULr0-.mjs +251 -0
  12. package/dist/Digest-CBd3uv5B.mjs +348 -0
  13. package/dist/Duration-EH9Vy4U6.mjs +791 -0
  14. package/dist/Effect-CKFpqbmw.mjs +12911 -0
  15. package/dist/FileSystem-4pPcEb_i.mjs +1366 -0
  16. package/dist/HttpClientError-BUeMWx70.mjs +303 -0
  17. package/dist/HttpClientResponse-D5ZKICVV.mjs +855 -0
  18. package/dist/Match-CGPLZYrV.mjs +434 -0
  19. package/dist/MutableHashMap-f8nW8AJR.mjs +376 -0
  20. package/dist/MutableHashSet-_HI572Ya.mjs +155 -0
  21. package/dist/MutableRef-D4Qv592C.mjs +107 -0
  22. package/dist/Number-BvkRkfc-.mjs +137 -0
  23. package/dist/Option-DkxSnBhp.mjs +3967 -0
  24. package/dist/Order-C1JC991I.mjs +481 -0
  25. package/dist/Path-Djdv_eF5.mjs +130 -0
  26. package/dist/Queue-CO5UQ6os.mjs +1394 -0
  27. package/dist/Redacted-RkJrG5Me.mjs +160 -0
  28. package/dist/Ref-BIPOxJNH.mjs +267 -0
  29. package/dist/Runtime-BInIAwK3.mjs +297 -0
  30. package/dist/Schema-MiKt5K1v.mjs +10999 -0
  31. package/dist/SecureRandom-B_U4ysO3.mjs +279 -0
  32. package/dist/Semaphore-DOVJ4ZGS.mjs +177 -0
  33. package/dist/Stdio-KylmOBX_.mjs +74 -0
  34. package/dist/Stream-bK9QIETN.mjs +4012 -0
  35. package/dist/String-q5dcsVe4.mjs +351 -0
  36. package/dist/TerminalCapabilities-w_44MAca.mjs +1769 -0
  37. package/dist/WebCrypto-ZI_GNVlw.mjs +33 -0
  38. package/dist/actor-call.service-CGM_vD9u.mjs +1269 -0
  39. package/dist/actor-client.adapter-Coem7gF1.mjs +3691 -0
  40. package/dist/akter-program.config-D8yb9xQP.mjs +7034 -0
  41. package/dist/akter.cli.mjs +5 -0
  42. package/dist/app-cli.config.mjs +49 -0
  43. package/dist/app-entry.pure.mjs +31 -0
  44. package/dist/archive/akter-archive.schema.mjs +65 -0
  45. package/dist/archive/akter-archive.service.mjs +309 -0
  46. package/dist/archive/archive-cli.config.mjs +45 -0
  47. package/dist/archive/archive-invalid.errors.mjs +5 -0
  48. package/dist/archive/archived-lineage.pure.mjs +13 -0
  49. package/dist/archive/tar.pure.mjs +291 -0
  50. package/dist/archive/zstd.adapter.mjs +46 -0
  51. package/dist/bench/bench-report.pure.mjs +120 -0
  52. package/dist/bench/bench-runner.pure.mjs +89 -0
  53. package/dist/bench/bench.service.mjs +301 -0
  54. package/dist/cli-runtime.layer-BqK2mmVL.mjs +1191 -0
  55. package/dist/cli-runtime.layer.mjs +1 -0
  56. package/dist/cloud-client.adapter-Cy1BjDbG.mjs +3186 -0
  57. package/dist/cloud-client.adapter-DOVMoYAj.mjs +304 -0
  58. package/dist/command/akter-commands.config.mjs +32 -0
  59. package/dist/command/akter-program.config.mjs +1 -0
  60. package/dist/credentials/credentials.service.mjs +226 -0
  61. package/dist/credentials/keychain.adapter.mjs +92 -0
  62. package/dist/credentials/login.config.mjs +251 -0
  63. package/dist/deploy/auth/api-attempt.service.mjs +30 -0
  64. package/dist/deploy/auth/token-refresh.errors.mjs +4 -0
  65. package/dist/deploy/build-bundle.adapter.mjs +133 -0
  66. package/dist/deploy/build-manifest.pure.mjs +44 -0
  67. package/dist/deploy/cloud-client.adapter.mjs +1 -0
  68. package/dist/deploy/cloud-url.pure.mjs +17 -0
  69. package/dist/deploy/deploy-failed.errors.mjs +17 -0
  70. package/dist/deploy/deploy.service.mjs +156 -0
  71. package/dist/deploy/request-failure.pure.mjs +87 -0
  72. package/dist/dev-runner.pure.mjs +30 -0
  73. package/dist/docs/doc-topics.pure.mjs +21 -0
  74. package/dist/docs/docs.config.mjs +1 -0
  75. package/dist/docs.config-D2amjsje.mjs +190 -0
  76. package/dist/docs.config-D3zzLzLR.mjs +190 -0
  77. package/dist/export/actor-export.service.mjs +267 -0
  78. package/dist/export/export-failed.errors.mjs +4 -0
  79. package/dist/export/export-failure.pure.mjs +18 -0
  80. package/dist/export/ordered-lines.pure.mjs +43 -0
  81. package/dist/export/ranged-download.pure.mjs +66 -0
  82. package/dist/export/snapshot-download.pure.mjs +86 -0
  83. package/dist/history-runner.pure.mjs +186 -0
  84. package/dist/history-since.pure.mjs +17 -0
  85. package/dist/hosted-runtime.layer-Cx3IPsEd.mjs +16358 -0
  86. package/dist/local/environment-cli.config.mjs +90 -0
  87. package/dist/local/environment-runner.pure.mjs +75 -0
  88. package/dist/local/local-cli.config.mjs +159 -0
  89. package/dist/logs/log-follow.config.mjs +21 -0
  90. package/dist/logs/log-follow.service.mjs +87 -0
  91. package/dist/logs/log-lines.pure.mjs +44 -0
  92. package/dist/migrate-runner.pure.mjs +28 -0
  93. package/dist/operation-input.pure-CS9iUhqK.mjs +1383 -0
  94. package/dist/operation-registry.pure-DVC8EDgj.mjs +283 -0
  95. package/dist/registry/actor-call.service.mjs +1 -0
  96. package/dist/registry/actor-commands.config.mjs +127 -0
  97. package/dist/registry/actor-refusal.pure.mjs +34 -0
  98. package/dist/registry/approval.service.mjs +80 -0
  99. package/dist/registry/cloud-commands.config.mjs +151 -0
  100. package/dist/registry/cloud-served.pure.mjs +31 -0
  101. package/dist/registry/operation-call.service.mjs +262 -0
  102. package/dist/registry/operation-input.pure.mjs +1 -0
  103. package/dist/registry/operation-output.pure.mjs +91 -0
  104. package/dist/registry/served-command.config.mjs +57 -0
  105. package/dist/runner-exited.errors.mjs +10 -0
  106. package/dist/terminal/cli-exit.errors.mjs +38 -0
  107. package/dist/terminal/cli-failure.pure.mjs +31 -0
  108. package/dist/terminal/confirm.adapter.mjs +27 -0
  109. package/package.json +61 -3
@@ -0,0 +1,291 @@
1
+ import { L as reduce, T as join, _ as getSomes, c as every, h as fromIterable, j as map, l as filter, u as findFirst } from "../Array-B4kuTQeW.mjs";
2
+ import { S as none, b as map$1, f as getOrElse, h as isNone, u as fromNullishOr, w as some } from "../Option-DkxSnBhp.mjs";
3
+ import { D as mapAccum, T as make, r as concat$1, y as fromIterable$1 } from "../Stream-bK9QIETN.mjs";
4
+ import { d as replace, m as split, p as slice, u as padStart } from "../String-q5dcsVe4.mjs";
5
+ // src/archive/tar.pure.ts
6
+ /**
7
+ * The tar subset `.akter` archives use (POSIX ustar with pax `path` and `size` records), read and written as streams so
8
+ * an actor database of any size never sits in memory. GNU tar and bsdtar read what it writes.
9
+ */
10
+ const blockBytes = 512;
11
+ const endBlocks = 2;
12
+ const nameBytes = 100;
13
+ const sizeDigits = 11;
14
+ const octalBase = 8;
15
+ const decimalBase = 10;
16
+ const nothing = 0;
17
+ const terminator = 1;
18
+ /** The largest size the ustar size field holds; a larger entry carries a pax `size` record. */
19
+ const largestUstarSize = octalBase ** sizeDigits - terminator;
20
+ const fileMode = 420;
21
+ const checksumDigits = 6;
22
+ const space = 32;
23
+ const field = {
24
+ checksum: {
25
+ at: 148,
26
+ width: 8
27
+ },
28
+ gid: {
29
+ at: 116,
30
+ width: 8
31
+ },
32
+ magic: {
33
+ at: 257,
34
+ width: 8
35
+ },
36
+ mode: {
37
+ at: 100,
38
+ width: 8
39
+ },
40
+ mtime: {
41
+ at: 136,
42
+ width: 12
43
+ },
44
+ name: {
45
+ at: 0,
46
+ width: nameBytes
47
+ },
48
+ size: {
49
+ at: 124,
50
+ width: 12
51
+ },
52
+ type: {
53
+ at: 156,
54
+ width: 1
55
+ },
56
+ uid: {
57
+ at: 108,
58
+ width: 8
59
+ }
60
+ };
61
+ const encoder = new globalThis.TextEncoder();
62
+ const decoder = new globalThis.TextDecoder();
63
+ const paddingOf = (size) => (blockBytes - size % blockBytes) % blockBytes;
64
+ const octal = (value, width) => `${padStart(width - terminator, "0")(value.toString(octalBase))}\0`;
65
+ const isChecksumByte = (index) => index >= field.checksum.at && index < field.checksum.at + field.checksum.width;
66
+ const checksumOf = (block) => reduce(fromIterable(block), nothing, (total, byte, index) => {
67
+ if (isChecksumByte(index)) return total + space;
68
+ return total + byte;
69
+ });
70
+ const fieldsOf = (name, size, typeFlag) => [
71
+ [field.name.at, name],
72
+ [field.mode.at, octal(fileMode, field.mode.width)],
73
+ [field.uid.at, octal(nothing, field.uid.width)],
74
+ [field.gid.at, octal(nothing, field.gid.width)],
75
+ [field.size.at, octal(Math.min(size, largestUstarSize), field.size.width)],
76
+ [field.mtime.at, octal(nothing, field.mtime.width)],
77
+ [field.type.at, typeFlag],
78
+ [field.magic.at, "ustar\x0000"]
79
+ ];
80
+ const header = (name, size, typeFlag) => {
81
+ const block = new Uint8Array(blockBytes);
82
+ for (const [at, text] of fieldsOf(name, size, typeFlag)) block.set(encoder.encode(text), at);
83
+ const checksum = `${padStart(checksumDigits, "0")(checksumOf(block).toString(octalBase))}\0 `;
84
+ block.set(encoder.encode(checksum), field.checksum.at);
85
+ return block;
86
+ };
87
+ /** One pax record, `<length> <key>=<value>\n`, where the length counts its own digits. */
88
+ const paxRecord = (key, value) => {
89
+ const body = ` ${key}=${value}\n`;
90
+ const bodyBytes = encoder.encode(body).length;
91
+ const guess = bodyBytes + String(bodyBytes).length;
92
+ const length = bodyBytes + String(guess).length;
93
+ return `${String(length)}${body}`;
94
+ };
95
+ const paxRecords = ({ path, size }) => {
96
+ const records = [...filter([paxRecord("path", path)], () => encoder.encode(path).length > nameBytes), ...filter([paxRecord("size", String(size))], () => size > largestUstarSize)];
97
+ return join(records, "");
98
+ };
99
+ const headersFor = (entry) => {
100
+ const records = encoder.encode(paxRecords(entry));
101
+ const file = header(slice(nothing, nameBytes)(entry.path), entry.size, "0");
102
+ if (records.length === nothing) return [file];
103
+ return [
104
+ header("PaxHeader", records.length, "x"),
105
+ records,
106
+ new Uint8Array(paddingOf(records.length)),
107
+ file
108
+ ];
109
+ };
110
+ const paddingStream = (size) => make(new Uint8Array(paddingOf(size)));
111
+ /** The bytes of one entry: its headers, its content, and the padding that ends it on a block boundary. */
112
+ const tarEntry = ({ content, path, size }) => concat$1(fromIterable$1(headersFor({
113
+ path,
114
+ size
115
+ })), concat$1(content, paddingStream(size)));
116
+ /** The two zero blocks that end an archive. */
117
+ const tarEnd = make(new Uint8Array(blockBytes * endBlocks));
118
+ const initial = {
119
+ buffered: /* @__PURE__ */ new Uint8Array(),
120
+ collecting: none(),
121
+ done: false,
122
+ pax: none(),
123
+ remaining: nothing,
124
+ skip: nothing
125
+ };
126
+ const textAt = (block, { at, width }) => replace(/\0.*$/su, "")(decoder.decode(block.subarray(at, at + width)));
127
+ const paxLine = /^\d+ (?<key>[^=]+)=(?<value>.*)$/su;
128
+ const pairOf = (line) => {
129
+ const groups = fromNullishOr(paxLine.exec(line)?.groups);
130
+ return map$1(groups, (found) => [found.key ?? "", found.value ?? ""]);
131
+ };
132
+ const parsePax = (records) => {
133
+ const lines = split("\n")(decoder.decode(records));
134
+ const pairs = getSomes(map(lines, pairOf));
135
+ const valueOf = (key) => map$1(findFirst(pairs, ([name]) => name === key), ([, value]) => value);
136
+ return {
137
+ path: valueOf("path"),
138
+ size: map$1(valueOf("size"), (text) => Number.parseInt(text, decimalBase))
139
+ };
140
+ };
141
+ const concat = (left, right) => {
142
+ const joined = new Uint8Array(left.length + right.length);
143
+ joined.set(left);
144
+ joined.set(right, left.length);
145
+ return joined;
146
+ };
147
+ const isZero = (block) => every(fromIterable(block), (byte) => byte === nothing);
148
+ /** A file header: the entry it starts, with any pax path and size before it applied. */
149
+ const fileHeader = (state, block, ustarSize) => {
150
+ const pax = getOrElse(state.pax, () => ({
151
+ path: none(),
152
+ size: none()
153
+ }));
154
+ const path = getOrElse(pax.path, () => textAt(block, field.name));
155
+ const size = getOrElse(pax.size, () => ustarSize);
156
+ const type = textAt(block, field.type);
157
+ const next = {
158
+ ...state,
159
+ pax: none(),
160
+ remaining: size,
161
+ skip: paddingOf(size)
162
+ };
163
+ if (type !== "0" && type !== "") return [next, []];
164
+ return [next, [{
165
+ kind: "entry",
166
+ path,
167
+ size
168
+ }]];
169
+ };
170
+ /** Reads one header block; a zero block ends the archive and a corrupt one stops it. */
171
+ const readHeader = (state, block) => {
172
+ if (isZero(block)) return [{
173
+ ...state,
174
+ done: true
175
+ }, [{ kind: "end" }]];
176
+ if (Number.parseInt(textAt(block, field.checksum), octalBase) !== checksumOf(block)) return [{
177
+ ...state,
178
+ done: true
179
+ }, [{
180
+ kind: "invalid",
181
+ reason: "a header failed its checksum"
182
+ }]];
183
+ const size = Number.parseInt(textAt(block, field.size), octalBase);
184
+ if (textAt(block, field.type) === "x") return [{
185
+ ...state,
186
+ collecting: some(/* @__PURE__ */ new Uint8Array()),
187
+ remaining: size,
188
+ skip: paddingOf(size)
189
+ }, []];
190
+ return fileHeader(state, block, size);
191
+ };
192
+ const readContent = (state, bytes) => {
193
+ const taken = bytes.subarray(nothing, state.remaining);
194
+ const rest = bytes.subarray(taken.length);
195
+ const remaining = state.remaining - taken.length;
196
+ if (isNone(state.collecting)) return [
197
+ {
198
+ ...state,
199
+ remaining
200
+ },
201
+ rest,
202
+ [{
203
+ bytes: taken,
204
+ kind: "data"
205
+ }]
206
+ ];
207
+ const collected = concat(state.collecting.value, taken);
208
+ if (remaining > nothing) return [
209
+ {
210
+ ...state,
211
+ collecting: some(collected),
212
+ remaining
213
+ },
214
+ rest,
215
+ []
216
+ ];
217
+ return [
218
+ {
219
+ ...state,
220
+ collecting: none(),
221
+ pax: some(parsePax(collected)),
222
+ remaining
223
+ },
224
+ rest,
225
+ []
226
+ ];
227
+ };
228
+ const readBlock = (state, bytes) => {
229
+ const pending = concat(state.buffered, bytes);
230
+ if (pending.length < blockBytes) return [
231
+ {
232
+ ...state,
233
+ buffered: pending
234
+ },
235
+ /* @__PURE__ */ new Uint8Array(),
236
+ []
237
+ ];
238
+ const [next, events] = readHeader({
239
+ ...state,
240
+ buffered: /* @__PURE__ */ new Uint8Array()
241
+ }, pending.subarray(nothing, blockBytes));
242
+ return [
243
+ next,
244
+ pending.subarray(blockBytes),
245
+ events
246
+ ];
247
+ };
248
+ /** Consumes what it can of `bytes` in the current state: entry content, padding, or a header block. */
249
+ const advance = (state, bytes) => {
250
+ if (state.remaining > nothing) return readContent(state, bytes);
251
+ if (state.skip > nothing) {
252
+ const skipped = Math.min(state.skip, bytes.length);
253
+ return [
254
+ {
255
+ ...state,
256
+ skip: state.skip - skipped
257
+ },
258
+ bytes.subarray(skipped),
259
+ []
260
+ ];
261
+ }
262
+ return readBlock(state, bytes);
263
+ };
264
+ const hasWork = (state, rest) => {
265
+ if (state.done) return false;
266
+ const atHeader = state.remaining === nothing && state.skip === nothing;
267
+ return rest.length > nothing || atHeader && state.buffered.length >= blockBytes;
268
+ };
269
+ const parseChunk = (state, chunk) => {
270
+ const events = [];
271
+ let current = state;
272
+ let rest = chunk;
273
+ while (hasWork(current, rest)) {
274
+ const [next, left, emitted] = advance(current, rest);
275
+ current = next;
276
+ rest = left;
277
+ events.push(...emitted);
278
+ }
279
+ return [current, events];
280
+ };
281
+ const halted = (state) => {
282
+ if (state.done) return [];
283
+ return [{ kind: "truncated" }];
284
+ };
285
+ /**
286
+ * Parses a tar stream into entry, data and end events. An archive that stops before its end blocks ends with a
287
+ * `truncated` event, and a corrupt header with an `invalid` one, so a reader never mistakes a cut archive for a
288
+ * complete one.
289
+ */
290
+ const untar = (bytes) => mapAccum(bytes, () => initial, parseChunk, { onHalt: halted });
291
+ export { tarEnd, tarEntry, untar };
@@ -0,0 +1,46 @@
1
+ import { Jn as succeed, M as fail, X as forkScoped, aa as Service, en as sync, et as gen, gn as void_, o as andThen, p as callback, rn as tapError, tt as ignore } from "../Effect-CKFpqbmw.mjs";
2
+ import { Xt as isError } from "../Option-DkxSnBhp.mjs";
3
+ import { W as runForEach, g as fromAsyncIterable, nt as unwrap } from "../Stream-bK9QIETN.mjs";
4
+ import { ArchiveInvalidError } from "./archive-invalid.errors.mjs";
5
+ import { createZstdCompress, createZstdDecompress } from "node:zlib";
6
+ // src/archive/zstd.adapter.ts
7
+ const failedTo = (doing) => (cause) => ArchiveInvalidError.make({ message: `zstd could not ${doing} the archive: ${String(cause)}` });
8
+ const settle = (failed) => (resume) => (error) => {
9
+ if (isError(error)) {
10
+ resume(fail(failed(error)));
11
+ return;
12
+ }
13
+ resume(void_);
14
+ };
15
+ /**
16
+ * Pipes bytes through a `node:zlib` zstd stream with backpressure: a fiber writes each chunk and waits for zlib to take
17
+ * it before the next, and the output is read as the stream's async iterable. A failed input destroys the codec, which
18
+ * fails the output.
19
+ */
20
+ const through = (make, doing) => (bytes) => unwrap(gen(function* zstdStream() {
21
+ const codec = make();
22
+ const failed = failedTo(doing);
23
+ const done = settle(failed);
24
+ const write = (chunk) => callback((resume) => {
25
+ codec.write(chunk, done(resume));
26
+ });
27
+ const end = callback((resume) => {
28
+ codec.end(() => {
29
+ done(resume)();
30
+ });
31
+ });
32
+ const fed = andThen(runForEach(bytes, write), end);
33
+ const destroyed = ignore(tapError(fed, (cause) => sync(() => {
34
+ codec.destroy(new Error(cause.message));
35
+ })));
36
+ yield* forkScoped(destroyed);
37
+ return fromAsyncIterable(codec, failed);
38
+ }));
39
+ /** Zstandard through `node:zlib`, which Node 24 and Bun both provide. */
40
+ var Zstd = class extends Service()("@akterjs/cli/archive/zstd.adapter/Zstd") {
41
+ static layer = succeed(this, {
42
+ compress: through(createZstdCompress, "compress"),
43
+ decompress: through(createZstdDecompress, "decompress")
44
+ });
45
+ };
46
+ export { Zstd };
@@ -0,0 +1,120 @@
1
+ import { L as reduce, T as join, j as map, v as groupBy, z as sort } from "../Array-B4kuTQeW.mjs";
2
+ import { n as max } from "../Number-BvkRkfc-.mjs";
3
+ import { a as String$1, f as mapInput, i as Number, n as Boolean, o as combine } from "../Order-C1JC991I.mjs";
4
+ import { F as String$2, I as Struct, U as Tuple, g as Finite, hr as toEntries, n as ArraySchema, v as Int } from "../Schema-MiKt5K1v.mjs";
5
+ import { h as startsWith, l as padEnd, y as trimEnd } from "../String-q5dcsVe4.mjs";
6
+ // src/bench/bench-report.pure.ts
7
+ const Sample = Tuple([String$2, Finite]);
8
+ const Hardware = Struct({
9
+ arch: String$2,
10
+ cores: Int,
11
+ cpu: String$2,
12
+ memoryGiB: Finite,
13
+ os: String$2
14
+ });
15
+ const BenchSamples = Struct({
16
+ elapsedMillis: Finite,
17
+ hardware: Hardware,
18
+ name: String$2,
19
+ runtime: String$2,
20
+ samples: ArraySchema(Sample)
21
+ });
22
+ const failedPrefix = "failed: ";
23
+ const millisPerSecond = 1e3;
24
+ const percent = 100;
25
+ const first = 0;
26
+ const step = 1;
27
+ const median = 50;
28
+ const tail = 90;
29
+ const farTail = 99;
30
+ const decimals = 2;
31
+ const notices = {
32
+ cloud: {
33
+ cost: "cost per row: unmeasured. akter bench doesn't read the app's usage yet.",
34
+ notice: "AKTER CLOUD RUN: steps ran from the machine below to the app over the network, so every latency includes that path. They describe Akter Cloud only when this generator ran off the machine under load; a published number links this command and states the workload's caveats."
35
+ },
36
+ comparison: {
37
+ cost: "cost per row: unmeasured. akter bench doesn't read another system's bill.",
38
+ notice: "COMPARISON RUN: another system, not Akter, driven by the same runner. Steps ran from the machine below to it over the network, so every latency includes that path. A published comparison links this command and the workload's script, and states the caveats of each system's setup."
39
+ },
40
+ local: {
41
+ cost: "cost per row: unmeasured. It needs an Akter Cloud run; a local run has no cost to report.",
42
+ notice: "LOCAL RUN: akter dev on this machine. These numbers describe this machine and this checkout, not Akter Cloud, and aren't published results."
43
+ }
44
+ };
45
+ const rank = (sorted, quantile) => sorted[Math.max(first, Math.ceil(quantile / percent * sorted.length) - step)] ?? first;
46
+ const latencyOf = (latencies) => {
47
+ const sorted = sort(latencies, Number);
48
+ return {
49
+ max: rank(sorted, percent),
50
+ p50: rank(sorted, median),
51
+ p90: rank(sorted, tail),
52
+ p99: rank(sorted, farTail)
53
+ };
54
+ };
55
+ const rowOf = (outcome, latencies, elapsedMillis) => ({
56
+ costPerRow: "unmeasured",
57
+ failed: startsWith(failedPrefix)(outcome),
58
+ latencyMillis: latencyOf(latencies),
59
+ outcome,
60
+ perSecond: latencies.length / (elapsedMillis / millisPerSecond),
61
+ steps: latencies.length
62
+ });
63
+ const byFailedThenName = combine(mapInput(Boolean, (row) => row.failed), mapInput(String$1, (row) => row.outcome));
64
+ const benchReport = ({ environment, run, settings }) => {
65
+ const { elapsedMillis, hardware, name, runtime, samples } = run;
66
+ const groups = groupBy(samples, ([outcome]) => outcome);
67
+ const rows = map(toEntries(groups), ([outcome, group]) => rowOf(outcome, map(group, ([, latency]) => latency), elapsedMillis));
68
+ const total = rowOf("total", map(samples, ([, latency]) => latency), elapsedMillis);
69
+ return {
70
+ ...environment,
71
+ ...settings,
72
+ costNote: notices[settings.kind].cost,
73
+ elapsedMillis,
74
+ hardware,
75
+ local: settings.kind === "local",
76
+ notice: notices[settings.kind].notice,
77
+ rows: sort(rows, byFailedThenName),
78
+ runtime,
79
+ total,
80
+ workload: name
81
+ };
82
+ };
83
+ const fixed = (value) => value.toFixed(decimals);
84
+ const tableOf = (rows) => {
85
+ const cells = map(rows, (row) => [
86
+ row.outcome,
87
+ String(row.steps),
88
+ fixed(row.perSecond),
89
+ fixed(row.latencyMillis.p50),
90
+ fixed(row.latencyMillis.p90),
91
+ fixed(row.latencyMillis.p99),
92
+ fixed(row.latencyMillis.max),
93
+ row.costPerRow
94
+ ]);
95
+ const header = [
96
+ "outcome",
97
+ "steps",
98
+ "per second",
99
+ "p50 ms",
100
+ "p90 ms",
101
+ "p99 ms",
102
+ "max ms",
103
+ "cost per row"
104
+ ];
105
+ const widths = map(header, (title, column) => reduce(cells, title.length, (width, cell) => max(width, cell[column]?.length ?? first)));
106
+ return map([header, ...cells], (cell) => trimEnd(join(map(cell, (text, column) => padEnd(widths[column] ?? first)(text)), " ")));
107
+ };
108
+ const reportLines = (report) => [
109
+ report.notice,
110
+ `workload ${report.workload}, ${fixed(report.durationMillis / millisPerSecond)} s at concurrency ${report.concurrency}, against ${report.target}; ${report.total.steps} steps in ${fixed(report.elapsedMillis / millisPerSecond)} s`,
111
+ "",
112
+ ...tableOf([...report.rows, report.total]),
113
+ "",
114
+ `hardware: ${report.hardware.cpu}, ${report.hardware.cores} cores, ${fixed(report.hardware.memoryGiB)} GiB, ${report.hardware.os} ${report.hardware.arch}`,
115
+ `runtime: ${report.runtime}`,
116
+ `versions: ${join(map(toEntries(report.versions), ([name, version]) => `${name} ${version}`), ", ")}`,
117
+ `pins: commit ${report.pins.commit} (${report.pins.worktree}), ${report.pins.lockfile} sha256 ${report.pins.lockfileSha256}`,
118
+ report.costNote
119
+ ];
120
+ export { BenchSamples, benchReport, reportLines };
@@ -0,0 +1,89 @@
1
+ import { T as join } from "../Array-B4kuTQeW.mjs";
2
+ import { typeScriptImports } from "../app-entry.pure.mjs";
3
+ // src/bench/bench-runner.pure.ts
4
+ const benchRunner = join([
5
+ "import { writeFileSync } from \"node:fs\";",
6
+ "import { arch, cpus, platform, release, totalmem } from \"node:os\";",
7
+ "import { createInterface } from \"node:readline\";",
8
+ "import { Cause, Effect, Exit, Layer, Option } from \"effect\";",
9
+ ...typeScriptImports,
10
+ "",
11
+ "const config = JSON.parse(process.argv[2]);",
12
+ "const module = await import(config.workload);",
13
+ "",
14
+ "if (typeof module.workload !== \"function\") {",
15
+ " console.error(\"akter bench: \" + config.workload + \" has no workload export. Export it as: export const workload = ({ id }) => ({ name, open, step });\");",
16
+ " process.exit(1);",
17
+ "}",
18
+ "",
19
+ "// stdin: one JSON line with the URL and, on Akter Cloud, the bearer header; then each refreshed header.",
20
+ "const lines = createInterface({ input: process.stdin })[Symbol.asyncIterator]();",
21
+ "const connection = JSON.parse((await lines.next()).value);",
22
+ "let authorization = connection.authorization;",
23
+ "void (async () => {",
24
+ " for await (const line of lines) authorization = JSON.parse(line).authorization;",
25
+ "})();",
26
+ "const headers = () => (authorization === undefined ? {} : { authorization });",
27
+ "",
28
+ "// kind is local (akter dev), cloud or comparison, so a workload sends a development identity only to akter dev.",
29
+ "const workload = module.workload({ id: config.id, kind: connection.kind, url: connection.url });",
30
+ "const samples = [];",
31
+ "const failureOf = (cause) => {",
32
+ " const error = Cause.squash(cause);",
33
+ " return \"failed: \" + (error?._tag ?? error?.name ?? \"defect\");",
34
+ "};",
35
+ "// A step with no answer within --step-timeout counts as failed, so one stuck step can't hold the run open.",
36
+ "const timedOut = \"failed: no answer within \" + config.stepTimeoutMillis + \" ms\";",
37
+ "const outcomeOf = (exit) => {",
38
+ " if (Exit.isFailure(exit)) return failureOf(exit.cause);",
39
+ " return Option.isSome(exit.value) ? String(exit.value.value) : timedOut;",
40
+ "};",
41
+ "const worker = (session, deadline) =>",
42
+ " Effect.gen(function* () {",
43
+ " while (performance.now() < deadline) {",
44
+ " const started = performance.now();",
45
+ " const exit = yield* Effect.exit(Effect.timeoutOption(workload.step(session), config.stepTimeoutMillis));",
46
+ " samples.push([outcomeOf(exit), performance.now() - started]);",
47
+ " }",
48
+ " });",
49
+ "const opened = Effect.flatMap(Effect.timeoutOption(workload.open, config.stepTimeoutMillis), (session) =>",
50
+ " Option.isSome(session) ? Effect.succeed(session.value) : Effect.fail(new Error(\"open got no answer within \" + config.stepTimeoutMillis + \" ms\")),",
51
+ ");",
52
+ "const run = Effect.gen(function* () {",
53
+ " const session = yield* opened;",
54
+ " const started = performance.now();",
55
+ " const workers = Array.from({ length: config.concurrency }, () => worker(session, started + config.durationMillis));",
56
+ " yield* Effect.all(workers, { concurrency: \"unbounded\", discard: true });",
57
+ " return performance.now() - started;",
58
+ "});",
59
+ "// A comparison workload reaches its own system; an Akter workload gets the Akter client.",
60
+ "const client = connection.kind === \"comparison\"",
61
+ " ? Layer.empty",
62
+ " : (await import(\"@akterjs/client/effect\")).akterClient({ headers, url: connection.url });",
63
+ "const exit = await Effect.runPromiseExit(Effect.scoped(run).pipe(Effect.provide(client)));",
64
+ "",
65
+ "if (Exit.isFailure(exit)) {",
66
+ " console.error(\"akter bench: \" + workload.name + \" failed before its steps ran.\");",
67
+ " console.error(Cause.pretty(exit.cause));",
68
+ " // An RPC or HTTP failure keeps its reason, such as an unresolved host, in its reason and cause chain.",
69
+ " const next = (error) => error?.reason ?? error?.cause;",
70
+ " for (let reason = next(Cause.squash(exit.cause)); reason !== undefined; reason = next(reason)) {",
71
+ " console.error(\"caused by: \" + (reason?.message || reason?._tag || String(reason)));",
72
+ " }",
73
+ " process.exit(1);",
74
+ "}",
75
+ "",
76
+ "const processors = cpus();",
77
+ "const hardware = {",
78
+ " arch: arch(),",
79
+ " cores: processors.length,",
80
+ " cpu: processors[0]?.model ?? \"unknown\",",
81
+ " memoryGiB: totalmem() / 1073741824,",
82
+ " os: platform() + \" \" + release(),",
83
+ "};",
84
+ "const runtime = process.versions.bun === undefined ? \"node \" + process.versions.node : \"bun \" + process.versions.bun;",
85
+ "writeFileSync(config.out, JSON.stringify({ elapsedMillis: exit.value, hardware, name: workload.name, runtime, samples }));",
86
+ "process.exit(0);",
87
+ ""
88
+ ], "\n");
89
+ export { benchRunner };