@akterjs/cli 0.0.0-stage → 0.1.0

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 (108) 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-C4cXx4Fr.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-_21Xi6HU.mjs +190 -0
  76. package/dist/export/actor-export.service.mjs +267 -0
  77. package/dist/export/export-failed.errors.mjs +4 -0
  78. package/dist/export/export-failure.pure.mjs +18 -0
  79. package/dist/export/ordered-lines.pure.mjs +43 -0
  80. package/dist/export/ranged-download.pure.mjs +66 -0
  81. package/dist/export/snapshot-download.pure.mjs +86 -0
  82. package/dist/history-runner.pure.mjs +186 -0
  83. package/dist/history-since.pure.mjs +17 -0
  84. package/dist/hosted-runtime.layer-Cx3IPsEd.mjs +16358 -0
  85. package/dist/local/environment-cli.config.mjs +90 -0
  86. package/dist/local/environment-runner.pure.mjs +75 -0
  87. package/dist/local/local-cli.config.mjs +159 -0
  88. package/dist/logs/log-follow.config.mjs +21 -0
  89. package/dist/logs/log-follow.service.mjs +87 -0
  90. package/dist/logs/log-lines.pure.mjs +44 -0
  91. package/dist/migrate-runner.pure.mjs +28 -0
  92. package/dist/operation-input.pure-CS9iUhqK.mjs +1383 -0
  93. package/dist/operation-registry.pure-DVC8EDgj.mjs +283 -0
  94. package/dist/registry/actor-call.service.mjs +1 -0
  95. package/dist/registry/actor-commands.config.mjs +127 -0
  96. package/dist/registry/actor-refusal.pure.mjs +34 -0
  97. package/dist/registry/approval.service.mjs +80 -0
  98. package/dist/registry/cloud-commands.config.mjs +151 -0
  99. package/dist/registry/cloud-served.pure.mjs +31 -0
  100. package/dist/registry/operation-call.service.mjs +262 -0
  101. package/dist/registry/operation-input.pure.mjs +1 -0
  102. package/dist/registry/operation-output.pure.mjs +91 -0
  103. package/dist/registry/served-command.config.mjs +57 -0
  104. package/dist/runner-exited.errors.mjs +10 -0
  105. package/dist/terminal/cli-exit.errors.mjs +38 -0
  106. package/dist/terminal/cli-failure.pure.mjs +31 -0
  107. package/dist/terminal/confirm.adapter.mjs +27 -0
  108. package/package.json +61 -3
@@ -0,0 +1,267 @@
1
+ import { j as map, k as makeBy, l as filter, p as flatMap } from "../Array-B4kuTQeW.mjs";
2
+ import { n as log } from "../Console-BKHUkH6a.mjs";
3
+ import { A as ensuring, At as provideContext, B as flatMap$1, C as catch_, Et as orDie, Gn as mergeAll, K as forEach, Kn as provide, Lt as retry, S as catchTags, T as context, Tt as option, U as fn, Vn as effect, Xt as succeed, Zt as succeedNone, _a as pick, aa as Service, dt as map$1, et as gen, ft as mapBoth, ht as mapError, lt as logWarning, nn as tapCause, tn as tap, u as asSome, x as catchTag } from "../Effect-CKFpqbmw.mjs";
4
+ import { t as FileSystem } from "../FileSystem-4pPcEb_i.mjs";
5
+ import { i as tag, n as exhaustive, s as value } from "../Match-CGPLZYrV.mjs";
6
+ import { S as none, _ as isSome, d as fromUndefinedOr, h as isNone, i as filter$1, x as match } from "../Option-DkxSnBhp.mjs";
7
+ import { t as Path } from "../Path-Djdv_eF5.mjs";
8
+ import { I as Struct, pt as fromJsonString, st as encodeEffect, tt as decodeEffect } from "../Schema-MiKt5K1v.mjs";
9
+ import { f as replaceAll, u as padStart } from "../String-q5dcsVe4.mjs";
10
+ import { a as BlobHashing, n as ArchivedActors, r as blobHashing } from "../hosted-runtime.layer-Cx3IPsEd.mjs";
11
+ import { s as ExportSnapshot, t as AppName, u as OrganizationName } from "../cloud-client.adapter-Cy1BjDbG.mjs";
12
+ import { ArchivedComputersJson, computersPath } from "../archive/akter-archive.schema.mjs";
13
+ import { t as CloudClient } from "../cloud-client.adapter-DOVMoYAj.mjs";
14
+ import { cloudUrl } from "../deploy/cloud-url.pure.mjs";
15
+ import { ExportFailedError } from "./export-failed.errors.mjs";
16
+ import { exportFailure, snapshotRetries } from "./export-failure.pure.mjs";
17
+ import { orderedLines } from "./ordered-lines.pure.mjs";
18
+ import { downloadedSnapshot } from "./snapshot-download.pure.mjs";
19
+ // src/export/actor-export.service.ts
20
+ const Target = Struct({
21
+ app: AppName,
22
+ organization: OrganizationName
23
+ });
24
+ const shardCount = 300;
25
+ const firstShard = 1;
26
+ const shardDigits = 3;
27
+ const ownerOnlyFile = 384;
28
+ const ownerOnlyFolder = 448;
29
+ const emptyKey = "%";
30
+ const dotsOnly = /^\.*$/u;
31
+ const PendingText = fromJsonString(ExportSnapshot);
32
+ const shards = makeBy(shardCount, (index) => index + firstShard);
33
+ const keySegment = (key) => {
34
+ const encoded = encodeURIComponent(key);
35
+ if (encoded === "") return emptyKey;
36
+ if (dotsOnly.test(encoded)) return replaceAll(".", "%2E")(encoded);
37
+ return encoded;
38
+ };
39
+ /** A computer as the archive records it: its checkpoint, when it has one, never leaves the Cloud account. */
40
+ const archivedComputer = ({ actor, checkpoint, key, ...computer }) => ({
41
+ ...computer,
42
+ actor: {
43
+ key,
44
+ type: actor
45
+ },
46
+ ...match(fromUndefinedOr(checkpoint), {
47
+ onNone: () => ({}),
48
+ onSome: (id) => ({ checkpoint: {
49
+ id,
50
+ portable: false
51
+ } })
52
+ })
53
+ });
54
+ const placed = fn("placed")(function* placed(out, shard, address) {
55
+ const path = yield* Path;
56
+ if (encodeURIComponent(address.actor) !== address.actor || dotsOnly.test(address.actor)) return yield* ExportFailedError.make({ message: "The API listed an actor name that is not a safe folder name." });
57
+ const folder = padStart(shardDigits, "0")(String(shard));
58
+ const home = path.join(out, "shards", folder, address.actor, keySegment(address.key));
59
+ return {
60
+ address,
61
+ database: path.join(home, "actor.db"),
62
+ files: path.join(home, "files"),
63
+ home,
64
+ name: `${address.actor}/${address.key}`,
65
+ partial: path.join(home, "actor.db.export"),
66
+ pending: path.join(home, "actor.db.export.json"),
67
+ stripping: path.join(home, "actor.db.strip.export")
68
+ };
69
+ });
70
+ const wipe = fn("wipe")(function* wipe(placing) {
71
+ const fs = yield* FileSystem;
72
+ return yield* forEach([
73
+ placing.partial,
74
+ placing.pending,
75
+ placing.stripping
76
+ ], (file) => fs.remove(file, { force: true }), { discard: true });
77
+ });
78
+ const refused = fn("refused")(function* refused(placing, message) {
79
+ yield* wipe(placing);
80
+ return yield* ExportFailedError.make({ message });
81
+ });
82
+ const goneOrErased = (placing) => (cause) => value(cause).pipe(tag("ActorErasedError", () => succeedNone), tag("ExportSnapshotGoneError", (gone) => refused(placing, `The snapshot of ${placing.name} is gone (${gone.reason}). Run akter export again to start that actor over.`)), exhaustive);
83
+ const pendingOf = fn("pendingOf")(function* pendingOf({ address, pending }) {
84
+ const fs = yield* FileSystem;
85
+ if (!(yield* fs.exists(pending))) return none();
86
+ const text = yield* fs.readFileString(pending);
87
+ const decoded = yield* option(decodeEffect(PendingText)(text));
88
+ return filter$1(decoded, (snapshot) => snapshot.actor === address.actor && snapshot.key === address.key);
89
+ });
90
+ const started = fn("started")(function* started({ cloud, placing, target }) {
91
+ yield* wipe(placing);
92
+ const snapshot = yield* cloud.request(cloud.client.actorsExportSnapshot(target.organization, target.app, { payload: placing.address }).pipe(retry(snapshotRetries), asSome, catchTags({ ActorsExportSnapshot410: (failure) => goneOrErased(placing)(failure.cause) })), "10 minutes").pipe(mapError(exportFailure(`Snapshot of ${placing.name}`)));
93
+ if (isNone(snapshot)) return snapshot;
94
+ if (snapshot.value.actor !== placing.address.actor || snapshot.value.key !== placing.address.key) return yield* ExportFailedError.make({ message: `The API returned a snapshot of another actor for ${placing.name}.` });
95
+ const fs = yield* FileSystem;
96
+ const text = yield* encodeEffect(PendingText)(snapshot.value);
97
+ yield* fs.writeFileString(placing.pending, text, { mode: ownerOnlyFile });
98
+ return snapshot;
99
+ });
100
+ const snapshotOf = fn("snapshotOf")(function* snapshotOf(job) {
101
+ const resumed = yield* pendingOf(job.placing);
102
+ if (isSome(resumed)) {
103
+ yield* job.say(`${job.placing.name}: resuming`);
104
+ return resumed;
105
+ }
106
+ return yield* started(job);
107
+ });
108
+ const databaseRange = ({ cloud, placing, target }, snapshot) => (range) => {
109
+ const params = {
110
+ ...placing.address,
111
+ length: range.length,
112
+ offset: range.offset
113
+ };
114
+ return cloud.request(cloud.client.actorsExportRead(target.organization, target.app, snapshot.snapshot, { params }).pipe(retry(snapshotRetries), asSome, catchTag("ActorsExportRead410", (failure) => goneOrErased(placing)(failure.cause))), "2 minutes").pipe(mapError(exportFailure(`Download of ${placing.name}`)));
115
+ };
116
+ const skipErased = fn("skipErased")(function* skipErased({ placing, say }) {
117
+ yield* wipe(placing);
118
+ yield* FileSystem.use((fs) => fs.remove(placing.files, {
119
+ force: true,
120
+ recursive: true
121
+ }));
122
+ yield* say(`${placing.name}: erased, nothing to export`);
123
+ return "erased";
124
+ });
125
+ const erasedOr = (placing) => (failure) => goneOrErased(placing)(failure.cause);
126
+ const filePage = ({ cloud, placing, target }, snapshot) => (after) => {
127
+ const from = match(after, {
128
+ onNone: () => ({}),
129
+ onSome: (sha256) => ({ after: sha256 })
130
+ });
131
+ const params = {
132
+ ...placing.address,
133
+ ...from
134
+ };
135
+ const listing = cloud.client.actorsExportFiles(target.organization, target.app, snapshot.snapshot, { params }).pipe(retry(snapshotRetries), asSome, catchTag("ActorsExportFiles410", erasedOr(placing)));
136
+ return cloud.request(listing, "30 seconds").pipe(mapError(exportFailure(`Listing the files of ${placing.name}`)));
137
+ };
138
+ const fileRange = ({ cloud, placing, target }, snapshot) => (file, range) => {
139
+ const params = {
140
+ ...placing.address,
141
+ length: range.length,
142
+ offset: range.offset
143
+ };
144
+ const reading = cloud.client.actorsExportFileRead(target.organization, target.app, snapshot.snapshot, file.sha256, { params }).pipe(retry(snapshotRetries), asSome, catchTag("ActorsExportFileRead410", erasedOr(placing)));
145
+ return cloud.request(reading, "2 minutes").pipe(mapError(exportFailure(`Download of a file of ${placing.name}`)));
146
+ };
147
+ /** The verified database becomes the actor's `actor.db` only once it is stripped, which marks the actor done. */
148
+ const stripped = fn("stripped")(function* stripped(placing) {
149
+ const fs = yield* FileSystem;
150
+ yield* ArchivedActors.strip(placing.stripping).pipe(tapCause(logWarning), catch_(() => refused(placing, `${placing.name}'s database could not be prepared for the archive.`)));
151
+ yield* fs.rename(placing.stripping, placing.database);
152
+ yield* fs.remove(placing.pending, { force: true });
153
+ });
154
+ const snapshotDownload = (job, snapshot, hashing) => {
155
+ const { name } = job.placing;
156
+ const refuse = (message) => refused(job.placing, message);
157
+ return {
158
+ database: {
159
+ ...snapshot,
160
+ hashing,
161
+ name,
162
+ partial: job.placing.partial,
163
+ read: databaseRange(job, snapshot),
164
+ refuse,
165
+ say: job.say,
166
+ target: job.placing.stripping
167
+ },
168
+ files: {
169
+ folder: job.placing.files,
170
+ hashing,
171
+ name,
172
+ page: filePage(job, snapshot),
173
+ read: fileRange(job, snapshot),
174
+ refuse,
175
+ say: job.say
176
+ }
177
+ };
178
+ };
179
+ const fetched = fn("fetched")(function* fetched(job) {
180
+ yield* FileSystem.use((fs) => fs.makeDirectory(job.placing.home, {
181
+ mode: ownerOnlyFolder,
182
+ recursive: true
183
+ }));
184
+ const snapshot = yield* snapshotOf(job);
185
+ if (isNone(snapshot)) return yield* skipErased(job);
186
+ const hashing = yield* BlobHashing.use((hasher) => succeed(hasher.sha256));
187
+ const database = yield* downloadedSnapshot(snapshotDownload(job, snapshot.value, hashing));
188
+ if (isNone(database)) return yield* skipErased(job);
189
+ yield* stripped(job.placing);
190
+ return "exported";
191
+ });
192
+ const exportActor = fn("exportActor")(function* exportActor(job) {
193
+ if (yield* (yield* FileSystem).exists(job.placing.database)) {
194
+ yield* wipe(job.placing);
195
+ yield* job.say(`${job.placing.name}: already exported`);
196
+ return "kept";
197
+ }
198
+ return yield* fetched(job);
199
+ });
200
+ /** Shards listed at once: a sparse app's 300 listings take about 300 × 330 ms one at a time, about 3 s 32 at a time. */
201
+ const shardsListedAtOnce = 32;
202
+ const listShard = (run, shard) => run.cloud.request(run.cloud.client.actorsExportShard(run.target.organization, run.target.app, String(shard), {}), "30 seconds").pipe(mapBoth({
203
+ onFailure: exportFailure(`Listing shard ${shard}`),
204
+ onSuccess: (page) => ({
205
+ page,
206
+ shard
207
+ })
208
+ }));
209
+ /** Actors exported at once: a few, so a dense app goes faster while each snapshot still streams to disk. */
210
+ const actorsAtOnce = 4;
211
+ /**
212
+ * Lists every shard first, 32 at a time, then exports their actors 4 at a time. The manifest, the archive and the
213
+ * console lines keep shard order, as if each actor were exported in turn.
214
+ */
215
+ const exportShards = fn("exportShards")(function* exportShards(run) {
216
+ const listed = yield* forEach(shards, (shard) => listShard(run, shard), { concurrency: shardsListedAtOnce });
217
+ const actors = flatMap(listed, ({ page, shard }) => map(page.actors, (address) => ({
218
+ address,
219
+ shard
220
+ })));
221
+ const lines = yield* orderedLines;
222
+ const outcomes = yield* forEach(actors, ({ address, shard }, index) => flatMap$1(placed(run.out, shard, address), (placing) => exportActor({
223
+ ...run,
224
+ placing,
225
+ say: lines.say(index)
226
+ })).pipe(ensuring(lines.done(index))), { concurrency: actorsAtOnce });
227
+ return {
228
+ computers: flatMap(listed, ({ page }) => map(page.computers ?? [], archivedComputer)),
229
+ exported: filter(outcomes, (outcome) => outcome !== "erased").length
230
+ };
231
+ });
232
+ /** Writes the computers every shard listed into the staging folder, where packing reads them into the manifest. */
233
+ const keepComputers = fn("keepComputers")(function* keepComputers(out, computers) {
234
+ const fs = yield* FileSystem;
235
+ const path = yield* Path;
236
+ const text = yield* orDie(encodeEffect(ArchivedComputersJson)(computers));
237
+ yield* fs.makeDirectory(out, {
238
+ mode: ownerOnlyFolder,
239
+ recursive: true
240
+ });
241
+ yield* fs.writeFileString(path.join(out, computersPath), text, { mode: ownerOnlyFile });
242
+ });
243
+ const runExport = fn("runExport")(function* runExport(options) {
244
+ const target = yield* decodeEffect(Target)(options).pipe(mapError(() => ExportFailedError.make({ message: "App and organization must be valid Cloud names." })));
245
+ const url = yield* cloudUrl(options.url);
246
+ const { apiKey } = options;
247
+ const out = yield* Path.use((path) => succeed(path.resolve(options.out)));
248
+ const cloud = yield* CloudClient.use((factory) => factory.connect({
249
+ apiKey,
250
+ url
251
+ })).pipe(tap((connected) => connected.signIn));
252
+ const { computers, exported } = yield* exportShards({
253
+ cloud,
254
+ out,
255
+ target
256
+ });
257
+ yield* keepComputers(out, computers);
258
+ yield* log(`Exported ${exported} actors to ${out}`);
259
+ return exported;
260
+ });
261
+ var ActorExport = class extends Service()("@akterjs/cli/export/actor-export.service/ActorExport", { make: gen(function* makeActorExport() {
262
+ const context$1 = yield* context().pipe(map$1(pick(BlobHashing, CloudClient, FileSystem, Path)));
263
+ return { run: (options) => provideContext(runExport(options), context$1) };
264
+ }) }) {
265
+ static layer = effect(this, this.make).pipe(provide(mergeAll(CloudClient.layer, blobHashing)));
266
+ };
267
+ export { ActorExport };
@@ -0,0 +1,4 @@
1
+ import { F as String, z as TaggedError } from "../Schema-MiKt5K1v.mjs";
2
+ // src/export/export-failed.errors.ts
3
+ var ExportFailedError = class extends TaggedError()("ExportFailedError", { message: String }) {};
4
+ export { ExportFailedError };
@@ -0,0 +1,18 @@
1
+ import { An as spaced } from "../Effect-CKFpqbmw.mjs";
2
+ import { mt as is } from "../Schema-MiKt5K1v.mjs";
3
+ import { DeployFailedError } from "../deploy/deploy-failed.errors.mjs";
4
+ import { retryable, statusOf } from "../deploy/request-failure.pure.mjs";
5
+ import { ExportFailedError } from "./export-failed.errors.mjs";
6
+ // src/export/export-failure.pure.ts
7
+ const snapshotRetries = {
8
+ schedule: spaced("250 millis"),
9
+ times: 3,
10
+ while: retryable
11
+ };
12
+ /** A failed Cloud request of an export step as the export's failure, naming the step and the HTTP status. */
13
+ const exportFailure = (operation) => (failure) => {
14
+ if (is(ExportFailedError)(failure)) return failure;
15
+ if (is(DeployFailedError)(failure)) return ExportFailedError.make({ message: failure.message });
16
+ return ExportFailedError.make({ message: `${operation} failed (${statusOf(failure)}).` });
17
+ };
18
+ export { exportFailure, snapshotRetries };
@@ -0,0 +1,43 @@
1
+ import { o as empty } from "../Array-B4kuTQeW.mjs";
2
+ import { n as log } from "../Console-BKHUkH6a.mjs";
3
+ import { $t as suspend, K as forEach, dt as map, gn as void_ } from "../Effect-CKFpqbmw.mjs";
4
+ import { a as remove, n as empty$1, o as set, r as get } from "../MutableHashMap-f8nW8AJR.mjs";
5
+ import { n as empty$2, r as has, t as add } from "../MutableHashSet-_HI572Ya.mjs";
6
+ import { f as getOrElse } from "../Option-DkxSnBhp.mjs";
7
+ import { t as make } from "../Semaphore-DOVJ4ZGS.mjs";
8
+ // src/export/ordered-lines.pure.ts
9
+ const firstJob = 0;
10
+ const one = 1;
11
+ const printed = (lines) => forEach(lines, (line) => log(line), { discard: true });
12
+ const sayWith = (printing) => (index) => (line) => suspend(() => {
13
+ if (index === printing.next) return log(line);
14
+ const kept = getOrElse(get(printing.kept, index), empty);
15
+ set(printing.kept, index, [...kept, line]);
16
+ return void_;
17
+ });
18
+ /** Moves past every finished job at the front, gathering what each job that becomes first has kept. */
19
+ const advanced = (printing) => {
20
+ const lines = [];
21
+ while (has(printing.finished, printing.next)) {
22
+ printing.next += one;
23
+ lines.push(...getOrElse(get(printing.kept, printing.next), empty));
24
+ remove(printing.kept, printing.next);
25
+ }
26
+ return lines;
27
+ };
28
+ const orderedLines = map(make(one), (lock) => {
29
+ const printing = {
30
+ finished: empty$2(),
31
+ kept: empty$1(),
32
+ next: firstJob
33
+ };
34
+ const say = sayWith(printing);
35
+ return {
36
+ done: (index) => lock.withPermit(suspend(() => {
37
+ add(printing.finished, index);
38
+ return printed(advanced(printing));
39
+ })),
40
+ say: (index) => (line) => lock.withPermit(say(index)(line))
41
+ };
42
+ });
43
+ export { orderedLines };
@@ -0,0 +1,66 @@
1
+ import { Gt as scoped, U as fn } from "../Effect-CKFpqbmw.mjs";
2
+ import { t as FileSystem } from "../FileSystem-4pPcEb_i.mjs";
3
+ import { S as none, _ as isSome, h as isNone, w as some } from "../Option-DkxSnBhp.mjs";
4
+ import { W as runForEach } from "../Stream-bK9QIETN.mjs";
5
+ // src/export/ranged-download.pure.ts
6
+ const largestRead = 65536;
7
+ const progressStep = 1048576;
8
+ const ownerOnlyFile = 384;
9
+ const checkedRange = fn("checkedRange")(function* checkedRange(download, offset) {
10
+ const length = Math.min(largestRead, download.bytes - offset);
11
+ const chunk = yield* download.read({
12
+ length,
13
+ offset
14
+ });
15
+ if (isSome(chunk) && chunk.value.byteLength !== length) return yield* download.refuse(`The API returned ${chunk.value.byteLength} bytes of ${download.name} where ${length} were expected.`);
16
+ return chunk;
17
+ });
18
+ const stepped = fn("stepped")(function* stepped({ download, file, hash }, offset) {
19
+ const chunk = yield* checkedRange(download, offset);
20
+ if (isNone(chunk)) return none();
21
+ yield* file.writeAll(chunk.value);
22
+ yield* hash.update(chunk.value);
23
+ const next = offset + chunk.value.byteLength;
24
+ if (Math.floor(next / progressStep) > Math.floor(offset / progressStep) || next === download.bytes) yield* download.say(`${download.name}: ${next} of ${download.bytes} bytes`);
25
+ return some(next);
26
+ });
27
+ const resumedHash = fn("resumedHash")(function* resumedHash(download) {
28
+ const fs = yield* FileSystem;
29
+ const hash = yield* download.hashing;
30
+ yield* runForEach(fs.stream(download.partial, { chunkSize: largestRead }), hash.update);
31
+ return hash;
32
+ });
33
+ const streamed = fn("streamed")(function* streamed(writing, from) {
34
+ let offset = some(from);
35
+ while (isSome(offset) && offset.value < writing.download.bytes) offset = yield* stepped(writing, offset.value);
36
+ yield* writing.file.sync;
37
+ if (isNone(offset)) return none();
38
+ const sha256 = yield* writing.hash.digest;
39
+ return some(sha256);
40
+ });
41
+ const written = fn("written")(function* written(download) {
42
+ const file = yield* (yield* FileSystem).open(download.partial, {
43
+ flag: "a",
44
+ mode: ownerOnlyFile
45
+ });
46
+ const { size } = yield* file.stat;
47
+ if (Number(size) > download.bytes) return yield* download.refuse(`${download.name}: the partial download is longer than its snapshot.`);
48
+ const hash = yield* resumedHash(download);
49
+ return yield* streamed({
50
+ download,
51
+ file,
52
+ hash
53
+ }, Number(size));
54
+ });
55
+ /**
56
+ * Downloads the rest of the file, checks its SHA-256 and moves it to its target. None when the actor was erased while
57
+ * it downloaded; a file that doesn't match is refused, so the next run starts the actor over.
58
+ */
59
+ const rangedDownload = fn("rangedDownload")(function* rangedDownload(download) {
60
+ const sha256 = yield* scoped(written(download));
61
+ if (isNone(sha256)) return none();
62
+ if (sha256.value !== download.sha256) return yield* download.refuse(`${download.name} did not match its SHA-256. Run akter export again to start that actor over.`);
63
+ yield* FileSystem.use((fs) => fs.rename(download.partial, download.target));
64
+ return some(download.target);
65
+ });
66
+ export { rangedDownload };
@@ -0,0 +1,86 @@
1
+ import { C as isReadonlyArrayEmpty, j as map, l as filter, n as appendAll, r as contains, w as isReadonlyArrayNonEmpty } from "../Array-B4kuTQeW.mjs";
2
+ import { K as forEach, U as fn, o as andThen } from "../Effect-CKFpqbmw.mjs";
3
+ import { t as FileSystem } from "../FileSystem-4pPcEb_i.mjs";
4
+ import { S as none, _ as isSome, d as fromUndefinedOr, h as isNone, w as some } from "../Option-DkxSnBhp.mjs";
5
+ import { t as Path } from "../Path-Djdv_eF5.mjs";
6
+ import { rangedDownload } from "./ranged-download.pure.mjs";
7
+ // src/export/snapshot-download.pure.ts
8
+ const ownerOnlyFolder = 448;
9
+ /** Every page of the snapshot's files, in SHA-256 order; none when the actor was erased while they were listed. */
10
+ const listed = fn("listed")(function* listed(files) {
11
+ let found = [];
12
+ let page = yield* files.page(none());
13
+ while (isSome(page)) {
14
+ found = appendAll(found, page.value.files);
15
+ const after = fromUndefinedOr(page.value.next);
16
+ if (isNone(after)) return some(found);
17
+ page = yield* files.page(after);
18
+ }
19
+ return none();
20
+ });
21
+ /** A file already in the folder was verified when it was written, since its name is its SHA-256. */
22
+ const fetchedFile = fn("fetchedFile")(function* fetchedFile(files, file) {
23
+ const fs = yield* FileSystem;
24
+ const target = (yield* Path).join(files.folder, file.sha256);
25
+ if (yield* fs.exists(target)) return some(target);
26
+ const partial = `${target}.export`;
27
+ return yield* rangedDownload({
28
+ ...file,
29
+ hashing: files.hashing,
30
+ name: `${files.name} file ${file.sha256}`,
31
+ partial,
32
+ read: (range) => files.read(file, range),
33
+ refuse: (message) => andThen(fs.remove(partial, { force: true }), files.refuse(message)),
34
+ say: files.say,
35
+ target
36
+ });
37
+ });
38
+ /**
39
+ * Removes what the snapshot doesn't hold, such as an earlier snapshot's files or a partial download left by one, so
40
+ * the archive carries only these.
41
+ */
42
+ const pruned = fn("pruned")(function* pruned(files, kept) {
43
+ const fs = yield* FileSystem;
44
+ const path = yield* Path;
45
+ if (isReadonlyArrayEmpty(kept)) return yield* fs.remove(files.folder, {
46
+ force: true,
47
+ recursive: true
48
+ });
49
+ const names = map(kept, (file) => file.sha256);
50
+ const present = yield* fs.readDirectory(files.folder);
51
+ const stale = filter(present, (name) => !contains(names, name));
52
+ return yield* forEach(stale, (name) => fs.remove(path.join(files.folder, name), { force: true }), { discard: true });
53
+ });
54
+ /** Downloads every listed file, in order; false when the actor was erased meanwhile. */
55
+ const downloadedAll = fn("downloadedAll")(function* downloadedAll(files, listing) {
56
+ if (isReadonlyArrayNonEmpty(listing)) yield* FileSystem.use((fs) => fs.makeDirectory(files.folder, {
57
+ mode: ownerOnlyFolder,
58
+ recursive: true
59
+ }));
60
+ for (const file of listing) {
61
+ const target = yield* fetchedFile(files, file);
62
+ if (isNone(target)) return false;
63
+ }
64
+ return true;
65
+ });
66
+ /**
67
+ * Downloads each file the snapshot holds into `<folder>/<sha256>`, resuming and verifying each like the database,
68
+ * and leaves nothing else there. Returns how many files the actor has; none when it was erased meanwhile.
69
+ */
70
+ const actorFiles = fn("actorFiles")(function* actorFiles(files) {
71
+ const listing = yield* listed(files);
72
+ if (isNone(listing)) return none();
73
+ if (!(yield* downloadedAll(files, listing.value))) return none();
74
+ yield* pruned(files, listing.value);
75
+ return some(listing.value.length);
76
+ });
77
+ /**
78
+ * Downloads an actor's export snapshot: its files first, then its database, which comes last because the caller
79
+ * takes a written database as the actor done. None when the actor was erased meanwhile.
80
+ */
81
+ const downloadedSnapshot = fn("downloadedSnapshot")(function* downloadedSnapshot({ database, files }) {
82
+ const counted = yield* actorFiles(files);
83
+ if (isNone(counted)) return none();
84
+ return yield* rangedDownload(database);
85
+ });
86
+ export { downloadedSnapshot };
@@ -0,0 +1,186 @@
1
+ import { T as join } from "./Array-B4kuTQeW.mjs";
2
+ import { appEntry } from "./app-entry.pure.mjs";
3
+ // src/history-runner.pure.ts
4
+ const recordLines = [
5
+ "const toJson = (schema, value) => Schema.encodeSync(Schema.toCodecJson(schema))(value);",
6
+ "const failureOf = (declaration, cause) =>",
7
+ " Option.match(Cause.findErrorOption(cause), {",
8
+ " onNone: () => ({ outcome: \"died\", defect: Cause.pretty(cause) }),",
9
+ " onSome: (error) => ({ outcome: \"failed\", error: toJson(Schema.Union([declaration.error, HandlerFailedError]), error) }),",
10
+ " });",
11
+ "const outcomeOf = (declaration, exit) =>",
12
+ " Exit.match(exit, {",
13
+ " onFailure: (cause) => failureOf(declaration, cause),",
14
+ " onSuccess: (value) => ({ outcome: \"ok\", value: toJson(declaration.success, value) }),",
15
+ " });",
16
+ "const declarationOf = (entry) => {",
17
+ " if (entry.kind === \"created\") return { error: ActorAlreadyExistsError, payload: actor.create, success: Schema.Void };",
18
+ " if (entry.kind !== \"result\") return actor.commands[entry.command];",
19
+ " const job = actor.jobs[entry.job];",
20
+ " return { error: Schema.Never, payload: Schema.Exit(job.successSchema, job.errorSchema, Schema.Defect()), success: Schema.Void };",
21
+ "};",
22
+ "const recordOf = (entry) => {",
23
+ " const base = { seq: entry.seq, at: new Date(entry.at).toISOString(), command: entry.command, commandId: entry.commandId };",
24
+ " if (entry.kind === \"unreadable\") return { ...base, unreadable: entry.reason };",
25
+ " const declaration = declarationOf(entry);",
26
+ " const payload = entry.kind === \"result\" ? entry.result : entry.payload;",
27
+ " return { ...base, principal: entry.principal, payload: toJson(declaration.payload, payload), ...outcomeOf(declaration, entry.exit) };",
28
+ "};",
29
+ "const shown = (value) => (value === undefined ? \"\" : \" \" + JSON.stringify(value));",
30
+ "const textOf = (record) => {",
31
+ " const head = record.at + \" \" + record.command;",
32
+ " if (\"unreadable\" in record) return head + \" unreadable: \" + record.unreadable;",
33
+ " const said = head + \" \" + shown(record.principal) + \" \" + shown(record.payload);",
34
+ " if (record.outcome === \"ok\") return said + \" -> ok\" + shown(record.value);",
35
+ " if (record.outcome === \"died\") return said + \" -> died \" + record.defect;",
36
+ " const { _tag, ...fields } = record.error;",
37
+ " return said + \" -> failed \" + _tag + (Object.keys(fields).length === 0 ? \"\" : shown(fields));",
38
+ "};"
39
+ ];
40
+ const pageLines = (json) => [
41
+ `const render = ${json} ? JSON.stringify : textOf;`,
42
+ "const coverageOf = (coveredSince) => {",
43
+ " const iso = new Date(coveredSince).toISOString();",
44
+ ` return ${json} ? JSON.stringify({ coveredSince: iso }) : "complete since " + iso;`,
45
+ "};",
46
+ "const printed = { coveredSince: 0 };",
47
+ "const printPage = (page) =>",
48
+ " Effect.sync(() => {",
49
+ " if (page.coveredSince !== printed.coveredSince) {",
50
+ " printed.coveredSince = page.coveredSince;",
51
+ " console.log(coverageOf(page.coveredSince));",
52
+ " }",
53
+ " for (const entry of page.entries) console.log(render(recordOf(entry)));",
54
+ " });"
55
+ ];
56
+ const verifyLines = [
57
+ "const timeOf = (point) => new Date(point.at).toISOString();",
58
+ "const windowText = (window) =>",
59
+ " Option.match(Option.all({ first: window.first, last: window.last }), {",
60
+ " onNone: () => \"no commands\",",
61
+ " onSome: ({ first, last }) => window.commands + \" commands, seq \" + first.seq + \" to \" + last.seq + \", \" + timeOf(first) + \" to \" + timeOf(last),",
62
+ " });",
63
+ "const actualText = (actual) =>",
64
+ " actual.kind === \"journaled\" ? actual.exit : \"refused \" + actual.error._tag + \": \" + actual.error.message;",
65
+ "const reportText = (report) => {",
66
+ " const replayed = \"replayed \" + windowText(report.window) + \" against the current handlers\";",
67
+ " return Option.match(report.difference, {",
68
+ " onNone: () => replayed + \": no difference\",",
69
+ " onSome: (difference) =>",
70
+ " [",
71
+ " replayed,",
72
+ " \"first difference at seq \" + difference.seq + \": \" + difference.command + \" \" + difference.commandId + \" at \" + timeOf(difference),",
73
+ " \" expected \" + difference.expected,",
74
+ " \" actual \" + actualText(difference.actual),",
75
+ String.raw` ].join("\n"),`,
76
+ " });",
77
+ "};",
78
+ "const pointJson = (point) => Option.getOrNull(Option.map(point, (found) => ({ seq: found.seq, at: timeOf(found) })));",
79
+ "const actualJson = (actual) =>",
80
+ " actual.kind === \"journaled\"",
81
+ " ? { kind: \"journaled\", exit: JSON.parse(actual.exit) }",
82
+ " : { kind: \"refused\", error: { _tag: actual.error._tag, message: actual.error.message } };",
83
+ "const differenceJson = (difference) => ({",
84
+ " seq: difference.seq,",
85
+ " at: timeOf(difference),",
86
+ " command: difference.command,",
87
+ " commandId: difference.commandId,",
88
+ " expected: JSON.parse(difference.expected),",
89
+ " actual: actualJson(difference.actual),",
90
+ "});",
91
+ "const reportJson = (report) =>",
92
+ " JSON.stringify({",
93
+ " window: { commands: report.window.commands, first: pointJson(report.window.first), last: pointJson(report.window.last) },",
94
+ " difference: Option.getOrNull(Option.map(report.difference, differenceJson)),",
95
+ " });",
96
+ "const notHosted = (error) =>",
97
+ " Effect.sync(() => {",
98
+ " console.error(\"akter history: the app layer does not host \" + error.actor);",
99
+ " process.exitCode = 1;",
100
+ " });",
101
+ "const pruned = (error) =>",
102
+ " Effect.sync(() => {",
103
+ " console.error(\"akter history: --verify replays from the first command, but the history of \" + address + \" is complete only since \" + new Date(error.coveredSince).toISOString() + \", because a retention pruned what came before.\");",
104
+ " process.exitCode = 1;",
105
+ " });",
106
+ "const printed = (report) =>",
107
+ " Effect.sync(() => {",
108
+ " console.log(render(report));",
109
+ " if (Option.isSome(report.difference)) process.exitCode = 1;",
110
+ " });"
111
+ ];
112
+ const streamLines = ({ directory, json, since }) => [
113
+ ...recordLines,
114
+ ...pageLines(json),
115
+ `const options = ${since} === null ? {} : { since: ${since} };`,
116
+ "",
117
+ "await Effect.runPromise(",
118
+ " decodedKey.pipe(",
119
+ " Effect.flatMap((key) => Stream.runForEach(actor.historyStream(key, options), printPage)),",
120
+ " Effect.catchTags({ ActorUnavailableError: unreadableStore, InvalidRequestError: unreadableStore }),",
121
+ ` Effect.provide(Akter.local(${directory}).pipe(Layer.provide(app.layer), Layer.provide(Akter.jobs))),`,
122
+ " ),",
123
+ ");"
124
+ ];
125
+ const replayLines = ({ directory, json }) => [
126
+ ...verifyLines,
127
+ `const render = ${json} ? reportJson : reportText;`,
128
+ "",
129
+ "await Effect.runPromise(",
130
+ " decodedKey.pipe(",
131
+ ` Effect.flatMap((key) => Verify.replay(app.layer.pipe(Layer.provide(Akter.jobs)), { actor, directory: ${directory}, key })),`,
132
+ " Effect.flatMap(printed),",
133
+ " Effect.catchTags({ ActorNotRegisteredError: notHosted, ActorUnavailableError: unreadableStore, HistoryPrunedError: pruned, InvalidRequestError: unreadableStore }),",
134
+ " ),",
135
+ ");"
136
+ ];
137
+ const bodyOf = ({ directory, json, since, verify }) => {
138
+ if (verify) return replayLines({
139
+ directory,
140
+ json
141
+ });
142
+ return streamLines({
143
+ directory,
144
+ json,
145
+ since
146
+ });
147
+ };
148
+ const historyRunner = ({ address, directory, entry, json, since, verify }) => join([
149
+ "import { Cause, Effect, Exit, Layer, Option, Schema, Stream } from \"effect\";",
150
+ "import { ActorAlreadyExistsError, Akter, HandlerFailedError, Verify } from \"@akterjs/effect\";",
151
+ ...appEntry({
152
+ command: "history",
153
+ entry
154
+ }),
155
+ "",
156
+ `const address = ${address};`,
157
+ "const slash = address.indexOf(\"/\");",
158
+ "const actor = app.actors.find((each) => each.name === address.slice(0, slash));",
159
+ "const keyText = address.slice(slash + 1);",
160
+ "const usage = (line) => {",
161
+ " console.error(\"akter history: \" + line);",
162
+ " process.exit(2);",
163
+ "};",
164
+ "",
165
+ "if (slash < 1 || actor === undefined) {",
166
+ " usage(address + \" names no actor of the app. Name one as <Actor>/<key>, where <Actor> is one of: \" + app.actors.map((each) => each.name).join(\", \"));",
167
+ "}",
168
+ "",
169
+ "const unreadableStore = (error) =>",
170
+ " Effect.sync(() => {",
171
+ " console.error(\"akter history: could not read \" + address + \": \" + error.message);",
172
+ " process.exitCode = 1;",
173
+ " });",
174
+ "const decodedKey = Schema.decodeUnknownEffect(actor.key)(keyText).pipe(",
175
+ " Effect.catchTag(\"SchemaError\", (issue) => Effect.sync(() => usage(keyText + \" is not a key of \" + actor.name + \": \" + issue.message))),",
176
+ ");",
177
+ "",
178
+ ...bodyOf({
179
+ directory,
180
+ json,
181
+ since,
182
+ verify
183
+ }),
184
+ ""
185
+ ], "\n");
186
+ export { historyRunner };