@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.
- package/LICENSE +201 -0
- package/README.md +47 -2
- package/dist/Argument-D81dqizB.mjs +57 -0
- package/dist/Array-B4kuTQeW.mjs +2323 -0
- package/dist/Boolean-kILIvmhE.mjs +26 -0
- package/dist/ChildProcess-BCIwxPGc.mjs +577 -0
- package/dist/CliError-Cf5xqkkj.mjs +633 -0
- package/dist/Command-Dzq9KM-h.mjs +5135 -0
- package/dist/Config-B6OND9WR.mjs +854 -0
- package/dist/Console-BKHUkH6a.mjs +141 -0
- package/dist/Data-CeCULr0-.mjs +251 -0
- package/dist/Digest-CBd3uv5B.mjs +348 -0
- package/dist/Duration-EH9Vy4U6.mjs +791 -0
- package/dist/Effect-CKFpqbmw.mjs +12911 -0
- package/dist/FileSystem-4pPcEb_i.mjs +1366 -0
- package/dist/HttpClientError-BUeMWx70.mjs +303 -0
- package/dist/HttpClientResponse-D5ZKICVV.mjs +855 -0
- package/dist/Match-CGPLZYrV.mjs +434 -0
- package/dist/MutableHashMap-f8nW8AJR.mjs +376 -0
- package/dist/MutableHashSet-_HI572Ya.mjs +155 -0
- package/dist/MutableRef-D4Qv592C.mjs +107 -0
- package/dist/Number-BvkRkfc-.mjs +137 -0
- package/dist/Option-DkxSnBhp.mjs +3967 -0
- package/dist/Order-C1JC991I.mjs +481 -0
- package/dist/Path-Djdv_eF5.mjs +130 -0
- package/dist/Queue-CO5UQ6os.mjs +1394 -0
- package/dist/Redacted-RkJrG5Me.mjs +160 -0
- package/dist/Ref-BIPOxJNH.mjs +267 -0
- package/dist/Runtime-BInIAwK3.mjs +297 -0
- package/dist/Schema-MiKt5K1v.mjs +10999 -0
- package/dist/SecureRandom-B_U4ysO3.mjs +279 -0
- package/dist/Semaphore-DOVJ4ZGS.mjs +177 -0
- package/dist/Stdio-KylmOBX_.mjs +74 -0
- package/dist/Stream-bK9QIETN.mjs +4012 -0
- package/dist/String-q5dcsVe4.mjs +351 -0
- package/dist/TerminalCapabilities-w_44MAca.mjs +1769 -0
- package/dist/WebCrypto-ZI_GNVlw.mjs +33 -0
- package/dist/actor-call.service-CGM_vD9u.mjs +1269 -0
- package/dist/actor-client.adapter-Coem7gF1.mjs +3691 -0
- package/dist/akter-program.config-C4cXx4Fr.mjs +7034 -0
- package/dist/akter.cli.mjs +5 -0
- package/dist/app-cli.config.mjs +49 -0
- package/dist/app-entry.pure.mjs +31 -0
- package/dist/archive/akter-archive.schema.mjs +65 -0
- package/dist/archive/akter-archive.service.mjs +309 -0
- package/dist/archive/archive-cli.config.mjs +45 -0
- package/dist/archive/archive-invalid.errors.mjs +5 -0
- package/dist/archive/archived-lineage.pure.mjs +13 -0
- package/dist/archive/tar.pure.mjs +291 -0
- package/dist/archive/zstd.adapter.mjs +46 -0
- package/dist/bench/bench-report.pure.mjs +120 -0
- package/dist/bench/bench-runner.pure.mjs +89 -0
- package/dist/bench/bench.service.mjs +301 -0
- package/dist/cli-runtime.layer-BqK2mmVL.mjs +1191 -0
- package/dist/cli-runtime.layer.mjs +1 -0
- package/dist/cloud-client.adapter-Cy1BjDbG.mjs +3186 -0
- package/dist/cloud-client.adapter-DOVMoYAj.mjs +304 -0
- package/dist/command/akter-commands.config.mjs +32 -0
- package/dist/command/akter-program.config.mjs +1 -0
- package/dist/credentials/credentials.service.mjs +226 -0
- package/dist/credentials/keychain.adapter.mjs +92 -0
- package/dist/credentials/login.config.mjs +251 -0
- package/dist/deploy/auth/api-attempt.service.mjs +30 -0
- package/dist/deploy/auth/token-refresh.errors.mjs +4 -0
- package/dist/deploy/build-bundle.adapter.mjs +133 -0
- package/dist/deploy/build-manifest.pure.mjs +44 -0
- package/dist/deploy/cloud-client.adapter.mjs +1 -0
- package/dist/deploy/cloud-url.pure.mjs +17 -0
- package/dist/deploy/deploy-failed.errors.mjs +17 -0
- package/dist/deploy/deploy.service.mjs +156 -0
- package/dist/deploy/request-failure.pure.mjs +87 -0
- package/dist/dev-runner.pure.mjs +30 -0
- package/dist/docs/doc-topics.pure.mjs +21 -0
- package/dist/docs/docs.config.mjs +1 -0
- package/dist/docs.config-_21Xi6HU.mjs +190 -0
- package/dist/export/actor-export.service.mjs +267 -0
- package/dist/export/export-failed.errors.mjs +4 -0
- package/dist/export/export-failure.pure.mjs +18 -0
- package/dist/export/ordered-lines.pure.mjs +43 -0
- package/dist/export/ranged-download.pure.mjs +66 -0
- package/dist/export/snapshot-download.pure.mjs +86 -0
- package/dist/history-runner.pure.mjs +186 -0
- package/dist/history-since.pure.mjs +17 -0
- package/dist/hosted-runtime.layer-Cx3IPsEd.mjs +16358 -0
- package/dist/local/environment-cli.config.mjs +90 -0
- package/dist/local/environment-runner.pure.mjs +75 -0
- package/dist/local/local-cli.config.mjs +159 -0
- package/dist/logs/log-follow.config.mjs +21 -0
- package/dist/logs/log-follow.service.mjs +87 -0
- package/dist/logs/log-lines.pure.mjs +44 -0
- package/dist/migrate-runner.pure.mjs +28 -0
- package/dist/operation-input.pure-CS9iUhqK.mjs +1383 -0
- package/dist/operation-registry.pure-DVC8EDgj.mjs +283 -0
- package/dist/registry/actor-call.service.mjs +1 -0
- package/dist/registry/actor-commands.config.mjs +127 -0
- package/dist/registry/actor-refusal.pure.mjs +34 -0
- package/dist/registry/approval.service.mjs +80 -0
- package/dist/registry/cloud-commands.config.mjs +151 -0
- package/dist/registry/cloud-served.pure.mjs +31 -0
- package/dist/registry/operation-call.service.mjs +262 -0
- package/dist/registry/operation-input.pure.mjs +1 -0
- package/dist/registry/operation-output.pure.mjs +91 -0
- package/dist/registry/served-command.config.mjs +57 -0
- package/dist/runner-exited.errors.mjs +10 -0
- package/dist/terminal/cli-exit.errors.mjs +38 -0
- package/dist/terminal/cli-failure.pure.mjs +31 -0
- package/dist/terminal/confirm.adapter.mjs +27 -0
- 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,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 };
|