@amalgm/shell 0.1.100 → 0.1.102
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/PURPOSE.md +7 -4
- package/dist/user-ground-host.d.ts +6 -0
- package/dist/user-ground-host.js +148 -89
- package/dist/user-ground-host.js.map +1 -1
- package/package.json +2 -2
package/PURPOSE.md
CHANGED
|
@@ -284,10 +284,13 @@ shell only assembles.
|
|
|
284
284
|
baseline: the newest retained Inbox result that exactly equals each
|
|
285
285
|
installed row advances that entity's resolved and materialized sequence
|
|
286
286
|
without replaying historical cargo, while every later different result
|
|
287
|
-
remains ordinary Apply work.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
only after
|
|
287
|
+
remains ordinary Apply work. Watch may establish native coverage before
|
|
288
|
+
startup Apply is ready, but its suspicions remain unconsumed until Apply
|
|
289
|
+
has adopted or resumed durable materialization. Startup adopts that
|
|
290
|
+
baseline while holding every watched ground cone; only after Apply starts
|
|
291
|
+
does one Detect generation consume the rings accumulated during boot. An
|
|
292
|
+
older Detect transaction settles first and any newer Watch generation
|
|
293
|
+
waits until adoption finishes. Register
|
|
291
294
|
and Watch enumerate the same durable binding inventory, so a missed
|
|
292
295
|
notification for rendering the portable reference cannot make either the
|
|
293
296
|
reference row or its declared external
|
|
@@ -154,6 +154,12 @@ export declare class UserGroundHost {
|
|
|
154
154
|
* runs unchanged. */
|
|
155
155
|
private readInlineCargo;
|
|
156
156
|
private uploadSnapshotContent;
|
|
157
|
+
/** Download a set of immutable artifacts straight from content storage.
|
|
158
|
+
* The set is the primitive: every storage object the set still needs —
|
|
159
|
+
* manifests and blocks alike — shares one wide lane bound, so no artifact
|
|
160
|
+
* serializes behind another's plan or seal. Each object is still its own
|
|
161
|
+
* storage request. A lone artifact is a set of one. */
|
|
162
|
+
private downloadContentSet;
|
|
157
163
|
private downloadContent;
|
|
158
164
|
private ensureWatchers;
|
|
159
165
|
private scheduleRescan;
|
package/dist/user-ground-host.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
|
|
|
3
3
|
import { open } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
|
|
6
|
-
import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory,
|
|
6
|
+
import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, PRESIGN_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, rootReplacementFromRecords, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
|
|
7
7
|
import Database from "better-sqlite3";
|
|
8
8
|
import { atomicCopy, atomicWrite, ensurePrivateDir, ensureUserDir } from "./filesystem.js";
|
|
9
9
|
import { emitFilesStage, } from "./files-observability.js";
|
|
@@ -27,13 +27,12 @@ import { GroundCoordinator } from "./ground-coordination.js";
|
|
|
27
27
|
import { submitMutationOperation } from "./paged-mutation-submit.js";
|
|
28
28
|
import { WireClient, WireRequestError } from "./wire-client.js";
|
|
29
29
|
const SMALL_CONTENT_UPLOAD_CONCURRENCY = 16;
|
|
30
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const DIRECT_BLOCK_DOWNLOAD_CONCURRENCY = 8;
|
|
30
|
+
/** Storage-object lanes for one download set. Manifests and blocks are all
|
|
31
|
+
* independent content-addressed objects fetched straight from storage, so
|
|
32
|
+
* twelve thousand small files and one large pack share the same wide bound;
|
|
33
|
+
* direct fetches cost the gateway nothing, and memory stays bounded by
|
|
34
|
+
* lanes × one block. */
|
|
35
|
+
const DIRECT_OBJECT_DOWNLOAD_CONCURRENCY = 128;
|
|
37
36
|
const DETECT_QUIET_MS = 12;
|
|
38
37
|
const DETECT_MAX_DEFERRAL_MS = 75;
|
|
39
38
|
const DETECT_RETRY_MS = 250;
|
|
@@ -87,7 +86,14 @@ export class UserGroundHost {
|
|
|
87
86
|
});
|
|
88
87
|
this.watchHost = new NodeWatchHost({
|
|
89
88
|
open: options.openWatch,
|
|
90
|
-
|
|
89
|
+
// Watch establishes the native evidence boundary during convergence,
|
|
90
|
+
// before the durable Apply baseline has been adopted. Keep every ring
|
|
91
|
+
// in Watch, but do not let Detect consume that evidence until Apply has
|
|
92
|
+
// resumed any revealed intent and owns materialization truth.
|
|
93
|
+
onSignal: () => {
|
|
94
|
+
if (this.converged)
|
|
95
|
+
this.scheduleRescan();
|
|
96
|
+
},
|
|
91
97
|
onEvent: (evidence) => this.stage({
|
|
92
98
|
primitive: "watch",
|
|
93
99
|
stage: "native-suspicion",
|
|
@@ -164,6 +170,11 @@ export class UserGroundHost {
|
|
|
164
170
|
await this.startRecordRail(this.activeIdentity);
|
|
165
171
|
await this.startApplyRail(this.activeIdentity);
|
|
166
172
|
this.converged = true;
|
|
173
|
+
// Native rings collected while convergence was establishing Watch
|
|
174
|
+
// coverage are ordinary evidence, not a startup exception. They get
|
|
175
|
+
// one Detect generation now that Apply recovery is authoritative.
|
|
176
|
+
if (this.watchHost.hasPending)
|
|
177
|
+
this.scheduleRescan();
|
|
167
178
|
return evidence;
|
|
168
179
|
},
|
|
169
180
|
};
|
|
@@ -288,7 +299,7 @@ export class UserGroundHost {
|
|
|
288
299
|
records: input.records,
|
|
289
300
|
destinationParent: input.destinationParent,
|
|
290
301
|
cloudHead: input.cloudHead,
|
|
291
|
-
readContent: (
|
|
302
|
+
readContent: (artifacts) => this.downloadContentSet(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: input.workspace.uuid }),
|
|
292
303
|
adoptMaterialization: (rootUUID) => {
|
|
293
304
|
const settled = this.applyHost?.adoptVerifiedMaterialization(rootUUID) ?? 0;
|
|
294
305
|
this.applyRail?.recordsAvailable();
|
|
@@ -414,7 +425,7 @@ export class UserGroundHost {
|
|
|
414
425
|
records: intent.records,
|
|
415
426
|
destinationParent: dirname(intent.destinationPath),
|
|
416
427
|
cloudHead: intent.cloudHead,
|
|
417
|
-
readContent: (
|
|
428
|
+
readContent: (artifacts) => this.downloadContentSet(intent.resourceId, this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: intent.workspaceId }),
|
|
418
429
|
adoptMaterialization: (rootUUID) => {
|
|
419
430
|
this.applyHost?.adoptVerifiedMaterialization(rootUUID);
|
|
420
431
|
this.applyRail?.recordsAvailable();
|
|
@@ -1050,7 +1061,7 @@ export class UserGroundHost {
|
|
|
1050
1061
|
database,
|
|
1051
1062
|
cacheDir,
|
|
1052
1063
|
cloud,
|
|
1053
|
-
readContent: (
|
|
1064
|
+
readContent: (artifacts) => this.downloadContentSet(cloud.resourceId, cacheDir, artifacts.map((artifact) => ({ artifact }))),
|
|
1054
1065
|
});
|
|
1055
1066
|
return localValue(identity, userRoot, database);
|
|
1056
1067
|
},
|
|
@@ -1268,13 +1279,24 @@ export class UserGroundHost {
|
|
|
1268
1279
|
throw error;
|
|
1269
1280
|
}
|
|
1270
1281
|
}
|
|
1271
|
-
|
|
1282
|
+
/** Download a set of immutable artifacts straight from content storage.
|
|
1283
|
+
* The set is the primitive: every storage object the set still needs —
|
|
1284
|
+
* manifests and blocks alike — shares one wide lane bound, so no artifact
|
|
1285
|
+
* serializes behind another's plan or seal. Each object is still its own
|
|
1286
|
+
* storage request. A lone artifact is a set of one. */
|
|
1287
|
+
async downloadContentSet(resourceId, cacheDir, wants, retryAttempts = 10, trace) {
|
|
1288
|
+
const unique = new Map(wants.map((want) => [want.artifact.contentHash, want]));
|
|
1289
|
+
const sole = unique.size === 1 ? [...unique.values()][0]?.artifact : undefined;
|
|
1272
1290
|
const started = performance.now();
|
|
1291
|
+
let declaredBytes = 0;
|
|
1273
1292
|
let downloadedBytes = 0;
|
|
1293
|
+
let downloadedChunks = 0;
|
|
1294
|
+
let reusedChunks = 0;
|
|
1274
1295
|
let requests = 0;
|
|
1296
|
+
const entityId = trace?.entityId ?? sole?.entityId;
|
|
1275
1297
|
const identifiers = {
|
|
1276
1298
|
...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
1277
|
-
entityId
|
|
1299
|
+
...(entityId ? { entityId } : {}),
|
|
1278
1300
|
...(trace?.mutationId ? { mutationId: trace.mutationId } : {}),
|
|
1279
1301
|
...(trace?.globalSequence ? { globalSequence: trace.globalSequence } : {}),
|
|
1280
1302
|
};
|
|
@@ -1283,9 +1305,19 @@ export class UserGroundHost {
|
|
|
1283
1305
|
stage: "immutable-content",
|
|
1284
1306
|
status: "started",
|
|
1285
1307
|
...identifiers,
|
|
1286
|
-
measurements: {
|
|
1308
|
+
measurements: {
|
|
1309
|
+
journey: trace?.journey ?? "bootstrap",
|
|
1310
|
+
artifacts: unique.size,
|
|
1311
|
+
...(sole ? { kind: sole.kind } : {}),
|
|
1312
|
+
},
|
|
1287
1313
|
});
|
|
1288
|
-
const
|
|
1314
|
+
const targets = new Map();
|
|
1315
|
+
const target = (artifact) => {
|
|
1316
|
+
const found = targets.get(artifact.contentHash);
|
|
1317
|
+
if (!found)
|
|
1318
|
+
throw new Error("download cache has no content manifest");
|
|
1319
|
+
return found;
|
|
1320
|
+
};
|
|
1289
1321
|
// Every storage object — the manifest and each block — is fetched
|
|
1290
1322
|
// straight from content storage over a presigned URL and opened locally;
|
|
1291
1323
|
// the wire carries only the key handoff and signing requests. A retry
|
|
@@ -1297,13 +1329,14 @@ export class UserGroundHost {
|
|
|
1297
1329
|
return bytes;
|
|
1298
1330
|
};
|
|
1299
1331
|
try {
|
|
1300
|
-
const
|
|
1332
|
+
const receipts = await downloadAllArtifacts([...unique.values()].map((want) => want.artifact), {
|
|
1301
1333
|
sha256Hex,
|
|
1302
|
-
getManifest: async () => {
|
|
1334
|
+
getManifest: async (artifact) => {
|
|
1303
1335
|
// The record's plan or this machine's own manifest cache already
|
|
1304
1336
|
// proves chunk identity; downloaded bytes are still hash-verified,
|
|
1305
1337
|
// so a stale local manifest can only fail loudly, never corrupt.
|
|
1306
|
-
const local =
|
|
1338
|
+
const local = unique.get(artifact.contentHash)?.manifest
|
|
1339
|
+
?? readCachedManifest(cacheDir, artifact.contentHash);
|
|
1307
1340
|
const manifest = local ?? await (async () => {
|
|
1308
1341
|
const bytes = await fetchObject({ kind: "manifest", sha256: artifact.contentHash });
|
|
1309
1342
|
const checked = checkContentManifest(JSON.parse(bytes.toString("utf8")), artifact.contentHash);
|
|
@@ -1311,32 +1344,31 @@ export class UserGroundHost {
|
|
|
1311
1344
|
throw new Error(checked.error);
|
|
1312
1345
|
return checked.value;
|
|
1313
1346
|
})();
|
|
1314
|
-
|
|
1347
|
+
targets.set(artifact.contentHash, new ContentCacheDownload(cacheDir, artifact, manifest));
|
|
1315
1348
|
return manifest;
|
|
1316
1349
|
},
|
|
1317
|
-
hasChunk: async (
|
|
1318
|
-
|
|
1319
|
-
throw new Error("download cache has no content manifest");
|
|
1320
|
-
return cache.target.hasChunk(index);
|
|
1321
|
-
},
|
|
1322
|
-
getChunk: async (_candidate, manifest, index) => {
|
|
1350
|
+
hasChunk: async (artifact, _manifest, index) => target(artifact).hasChunk(index),
|
|
1351
|
+
getChunk: async (_artifact, manifest, index) => {
|
|
1323
1352
|
const chunk = manifest.chunks[index];
|
|
1324
1353
|
if (!chunk)
|
|
1325
1354
|
throw new Error(`content manifest has no chunk ${index}`);
|
|
1326
1355
|
return fetchObject({ kind: "block", sha256: chunk.sha256 });
|
|
1327
1356
|
},
|
|
1328
|
-
putChunk: async (
|
|
1329
|
-
|
|
1330
|
-
throw new Error("download cache has no content manifest");
|
|
1331
|
-
await cache.target.putChunk(index, bytes);
|
|
1357
|
+
putChunk: async (artifact, _manifest, index, bytes) => {
|
|
1358
|
+
await target(artifact).putChunk(index, bytes);
|
|
1332
1359
|
},
|
|
1333
|
-
sealArtifact: async () => {
|
|
1334
|
-
|
|
1335
|
-
throw new Error("download cache has no content manifest");
|
|
1336
|
-
const sealed = await cache.target.seal();
|
|
1360
|
+
sealArtifact: async (artifact) => {
|
|
1361
|
+
const sealed = await target(artifact).seal();
|
|
1337
1362
|
return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
|
|
1338
1363
|
},
|
|
1339
|
-
}, { concurrency:
|
|
1364
|
+
}, { concurrency: DIRECT_OBJECT_DOWNLOAD_CONCURRENCY });
|
|
1365
|
+
const content = new Map();
|
|
1366
|
+
for (const receipt of receipts) {
|
|
1367
|
+
declaredBytes += receipt.manifest.bytes;
|
|
1368
|
+
downloadedChunks += receipt.downloadedChunks;
|
|
1369
|
+
reusedChunks += receipt.reusedChunks;
|
|
1370
|
+
content.set(receipt.artifact.contentHash, receipt.local);
|
|
1371
|
+
}
|
|
1340
1372
|
this.stage({
|
|
1341
1373
|
primitive: "download",
|
|
1342
1374
|
stage: "immutable-content",
|
|
@@ -1345,15 +1377,16 @@ export class UserGroundHost {
|
|
|
1345
1377
|
durationMs: performance.now() - started,
|
|
1346
1378
|
measurements: {
|
|
1347
1379
|
journey: trace?.journey ?? "bootstrap",
|
|
1348
|
-
|
|
1349
|
-
|
|
1380
|
+
artifacts: unique.size,
|
|
1381
|
+
...(sole ? { kind: sole.kind } : {}),
|
|
1382
|
+
declaredBytes,
|
|
1350
1383
|
downloadedBytes,
|
|
1351
|
-
downloadedChunks
|
|
1352
|
-
reusedChunks
|
|
1384
|
+
downloadedChunks,
|
|
1385
|
+
reusedChunks,
|
|
1353
1386
|
requests,
|
|
1354
1387
|
},
|
|
1355
1388
|
});
|
|
1356
|
-
return
|
|
1389
|
+
return content;
|
|
1357
1390
|
}
|
|
1358
1391
|
catch (error) {
|
|
1359
1392
|
this.stage({
|
|
@@ -1362,14 +1395,26 @@ export class UserGroundHost {
|
|
|
1362
1395
|
status: "failed",
|
|
1363
1396
|
...identifiers,
|
|
1364
1397
|
durationMs: performance.now() - started,
|
|
1365
|
-
measurements: {
|
|
1398
|
+
measurements: {
|
|
1399
|
+
journey: trace?.journey ?? "bootstrap",
|
|
1400
|
+
artifacts: unique.size,
|
|
1401
|
+
...(sole ? { kind: sole.kind } : {}),
|
|
1402
|
+
requests,
|
|
1403
|
+
},
|
|
1366
1404
|
});
|
|
1367
1405
|
throw error;
|
|
1368
1406
|
}
|
|
1369
1407
|
finally {
|
|
1370
|
-
await
|
|
1408
|
+
await Promise.all([...targets.values()].map((open) => open.close()));
|
|
1371
1409
|
}
|
|
1372
1410
|
}
|
|
1411
|
+
async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10, trace, knownManifest) {
|
|
1412
|
+
const content = await this.downloadContentSet(resourceId, cacheDir, [{ artifact, ...(knownManifest ? { manifest: knownManifest } : {}) }], retryAttempts, trace);
|
|
1413
|
+
const local = content.get(artifact.contentHash);
|
|
1414
|
+
if (!local)
|
|
1415
|
+
throw new Error(`download set did not seal ${artifact.contentHash}`);
|
|
1416
|
+
return local;
|
|
1417
|
+
}
|
|
1373
1418
|
ensureWatchers(identity, authoritativeBaselineRootIds = []) {
|
|
1374
1419
|
if (this.closing)
|
|
1375
1420
|
return this.watchHost.health();
|
|
@@ -4259,62 +4304,76 @@ async function materializeRecords(input) {
|
|
|
4259
4304
|
const { records, rootPath, cacheDir, bindingDir, readContent } = input;
|
|
4260
4305
|
const repositories = [];
|
|
4261
4306
|
const repositoriesByUuid = new Map();
|
|
4262
|
-
|
|
4307
|
+
// One download set covers everything the wire owes this add up front:
|
|
4308
|
+
// every ordinary file artifact and every repository transport head, side
|
|
4309
|
+
// by side under one wide object bound. Only transport parents — links a
|
|
4310
|
+
// chain reveals inside downloaded history — cost further rounds, and each
|
|
4311
|
+
// round is again one set across every chain. Disk application below keeps
|
|
4312
|
+
// its ordered, validated walk.
|
|
4313
|
+
const wanted = new Map();
|
|
4314
|
+
const repositoryHeads = [];
|
|
4263
4315
|
for (const record of records) {
|
|
4264
|
-
if (record.type
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4316
|
+
if (record.type === "repo.git") {
|
|
4317
|
+
if (!record.payloadVersion || !record.transportVersion) {
|
|
4318
|
+
throw new Error(`cloud repository ${record.uuid} has no state transport`);
|
|
4319
|
+
}
|
|
4320
|
+
const artifact = artifactForRecord(record);
|
|
4321
|
+
if (!artifact)
|
|
4322
|
+
throw new Error(`cloud repository ${record.uuid} has no transfer artifact`);
|
|
4323
|
+
wanted.set(artifact.contentHash, artifact);
|
|
4324
|
+
repositoryHeads.push({ record, artifact });
|
|
4273
4325
|
}
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4326
|
+
else if (["file.text", "file.binary", "link"].includes(record.type)) {
|
|
4327
|
+
const artifact = artifactForRecord(record);
|
|
4328
|
+
if (artifact)
|
|
4329
|
+
wanted.set(artifact.contentHash, artifact);
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
const fileContent = await readContent([...wanted.values()]);
|
|
4333
|
+
const repositoryChains = new Map();
|
|
4334
|
+
const walking = repositoryHeads.map(({ record, artifact }) => {
|
|
4335
|
+
const local = fileContent.get(artifact.contentHash);
|
|
4336
|
+
if (!local)
|
|
4337
|
+
throw new Error(`cloud repository ${record.uuid} transport was not downloaded`);
|
|
4278
4338
|
if (local.contentHash !== record.transportVersion) {
|
|
4279
4339
|
throw new Error(`cloud repository ${record.uuid} transport is corrupt`);
|
|
4280
4340
|
}
|
|
4281
|
-
const chain = [{ file: local.file, transportVersion: local.contentHash }];
|
|
4282
|
-
const versions = new Set([record.transportVersion]);
|
|
4283
|
-
let state = inspectRepositoryTransportFile(local.file);
|
|
4284
|
-
while (state.parentTransportVersion) {
|
|
4285
|
-
if (versions.has(state.parentTransportVersion)) {
|
|
4286
|
-
throw new Error(`cloud repository ${record.uuid} transport chain has a cycle`);
|
|
4287
|
-
}
|
|
4288
|
-
const parentVersion = state.parentTransportVersion;
|
|
4289
|
-
versions.add(parentVersion);
|
|
4290
|
-
const parent = await readContent({ ...artifact, contentHash: parentVersion });
|
|
4291
|
-
if (parent.contentHash !== parentVersion) {
|
|
4292
|
-
throw new Error(`cloud repository ${record.uuid} parent transport is corrupt`);
|
|
4293
|
-
}
|
|
4294
|
-
chain.unshift({ file: parent.file, transportVersion: parent.contentHash });
|
|
4295
|
-
state = inspectRepositoryTransportFile(parent.file);
|
|
4296
|
-
}
|
|
4297
4341
|
const latest = inspectRepositoryTransportFile(local.file);
|
|
4298
4342
|
if (latest.stateId !== record.payloadVersion) {
|
|
4299
4343
|
throw new Error(`cloud repository ${record.uuid} transport does not name its declared state`);
|
|
4300
4344
|
}
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4345
|
+
const chain = [{ file: local.file, transportVersion: local.contentHash }];
|
|
4346
|
+
repositoryChains.set(record.uuid, chain);
|
|
4347
|
+
return {
|
|
4348
|
+
record,
|
|
4349
|
+
artifact,
|
|
4350
|
+
chain,
|
|
4351
|
+
versions: new Set([local.contentHash]),
|
|
4352
|
+
parent: latest.parentTransportVersion,
|
|
4353
|
+
};
|
|
4354
|
+
});
|
|
4355
|
+
for (let frontier = walking.filter((walk) => walk.parent !== null); frontier.length > 0; frontier = frontier.filter((walk) => walk.parent !== null)) {
|
|
4356
|
+
const parents = await readContent(frontier.map((walk) => {
|
|
4357
|
+
const parent = walk.parent;
|
|
4358
|
+
if (walk.versions.has(parent)) {
|
|
4359
|
+
throw new Error(`cloud repository ${walk.record.uuid} transport chain has a cycle`);
|
|
4360
|
+
}
|
|
4361
|
+
return { ...walk.artifact, contentHash: parent };
|
|
4362
|
+
}));
|
|
4363
|
+
for (const walk of frontier) {
|
|
4364
|
+
const wantedParent = walk.parent;
|
|
4365
|
+
const parent = parents.get(wantedParent);
|
|
4366
|
+
if (!parent) {
|
|
4367
|
+
throw new Error(`cloud repository ${walk.record.uuid} parent transport was not downloaded`);
|
|
4368
|
+
}
|
|
4369
|
+
if (parent.contentHash !== wantedParent) {
|
|
4370
|
+
throw new Error(`cloud repository ${walk.record.uuid} parent transport is corrupt`);
|
|
4371
|
+
}
|
|
4372
|
+
walk.versions.add(parent.contentHash);
|
|
4373
|
+
walk.chain.unshift({ file: parent.file, transportVersion: parent.contentHash });
|
|
4374
|
+
walk.parent = inspectRepositoryTransportFile(parent.file).parentTransportVersion;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4318
4377
|
const materializeRepository = async (record, destination) => {
|
|
4319
4378
|
const chain = repositoryChains.get(record.uuid);
|
|
4320
4379
|
if (!chain)
|