@amalgm/shell 0.1.125 → 0.1.126
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 +4 -0
- package/dist/content-direct.d.ts +13 -0
- package/dist/content-direct.js +44 -13
- package/dist/content-direct.js.map +1 -1
- package/dist/registry-graph.d.ts +73 -31
- package/dist/registry-graph.js +283 -80
- package/dist/registry-graph.js.map +1 -1
- package/dist/registry-page-cache.d.ts +14 -0
- package/dist/registry-page-cache.js +32 -0
- package/dist/registry-page-cache.js.map +1 -0
- package/dist/user-ground-host.d.ts +28 -7
- package/dist/user-ground-host.js +196 -80
- package/dist/user-ground-host.js.map +1 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlin
|
|
|
3
3
|
import { open, readFile } 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, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } from "@amalgm/live";
|
|
6
|
+
import { CHUNK_BYTES, CONTENT_CONTRACT, INLINE_CARGO_MAX_BYTES, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, containedRecords, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, decodeAcceptedEntityRecord, deriveUploadManifest, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, downloadAll as downloadAllArtifacts, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, pathIsSuspect, CONTENT_REFS_BATCH_OBJECTS, privateEntityResourceId, repositoryIdentityHash, sameRecords, snapshotFromRecords, stableJson, streamCargo, travelingRecords, upload as uploadArtifact, userGroundRecords, GRAPH_HEAD_OPERATION_KIND, } 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";
|
|
@@ -23,6 +23,7 @@ import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile
|
|
|
23
23
|
import { gitEvidenceFingerprint, inspectGitRegistration, sameGitIdentity, } from "./git-registration-host.js";
|
|
24
24
|
import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
25
25
|
import { graphPageIdentities, registryGraphFromSnapshot, } from "./registry-graph.js";
|
|
26
|
+
import { readVerifiedRegistryPage, writeVerifiedRegistryPage, } from "./registry-page-cache.js";
|
|
26
27
|
import { NodeWatchHost, } from "./watching/index.js";
|
|
27
28
|
import { GroundCoordinator } from "./ground-coordination.js";
|
|
28
29
|
import { adoptSnapshotDeliveryBaseline } from "./snapshot-delivery-baseline.js";
|
|
@@ -34,17 +35,17 @@ const CARGO_SEGMENT_LOAD_CONCURRENCY = 8;
|
|
|
34
35
|
/** Storage-object lanes for one download set. Tiny objects use the wide
|
|
35
36
|
* request window, while the separate byte budget narrows large chunks so
|
|
36
37
|
* request concurrency can never silently become an unbounded memory claim. */
|
|
37
|
-
|
|
38
|
+
/** Admission is sized to the refs batch law so one same-turn wave coalesces
|
|
39
|
+
* into one refs frame and the span fetcher merges each pack's members into a
|
|
40
|
+
* few broad ranged reads. Memory is bounded by the byte laws below, never by
|
|
41
|
+
* this count. */
|
|
42
|
+
const DIRECT_OBJECT_DOWNLOAD_CONCURRENCY = CONTENT_REFS_BATCH_OBJECTS;
|
|
38
43
|
// Opening one protected object transiently retains the fetched envelope,
|
|
39
44
|
// WebCrypto's ciphertext-with-tag input, and the plaintext. Bound plaintext
|
|
40
45
|
// to two full blocks so those three representations stay below 96 MiB even
|
|
41
46
|
// on the 1 GiB runtimes used for ordinary Files materialization. Small
|
|
42
47
|
// objects still use the full request-concurrency window.
|
|
43
48
|
const DIRECT_OBJECT_DOWNLOAD_MAX_IN_FLIGHT_BYTES = 2 * CHUNK_BYTES;
|
|
44
|
-
/** Graph pages are at most a few megabytes each and a whole account is a
|
|
45
|
-
* handful of them, so a narrow lane keeps the cold read fast without holding
|
|
46
|
-
* the whole graph twice while it assembles. */
|
|
47
|
-
const GRAPH_PAGE_READ_CONCURRENCY = 8;
|
|
48
49
|
const DETECT_QUIET_MS = 12;
|
|
49
50
|
const DETECT_MAX_DEFERRAL_MS = 75;
|
|
50
51
|
const DETECT_RETRY_MS = 250;
|
|
@@ -94,6 +95,9 @@ export class UserGroundHost {
|
|
|
94
95
|
watchDirty = false;
|
|
95
96
|
cloudState = null;
|
|
96
97
|
converged = false;
|
|
98
|
+
/** Cumulative graph-page reads, reported as diffs on the stages that
|
|
99
|
+
* materialize roots so the ledger shows what each operation actually read. */
|
|
100
|
+
graphPageReads = { fetchedPages: 0, fetchedBytes: 0, cachedPages: 0 };
|
|
97
101
|
syncing = null;
|
|
98
102
|
recordRail = null;
|
|
99
103
|
recordStore = null;
|
|
@@ -159,7 +163,7 @@ export class UserGroundHost {
|
|
|
159
163
|
const local = this.localPort(this.activeIdentity);
|
|
160
164
|
const result = await convergeUserGround(this.activeIdentity, {
|
|
161
165
|
sha256Hex,
|
|
162
|
-
cloud: this.cloudPort(this.activeIdentity),
|
|
166
|
+
cloud: this.cloudPort(this.activeIdentity, { kind: "core" }),
|
|
163
167
|
local,
|
|
164
168
|
});
|
|
165
169
|
await this.resumeWorkspaceAdds(this.activeIdentity);
|
|
@@ -302,13 +306,16 @@ export class UserGroundHost {
|
|
|
302
306
|
});
|
|
303
307
|
const add = {
|
|
304
308
|
sha256Hex,
|
|
305
|
-
lookupRegistry: async () => {
|
|
309
|
+
lookupRegistry: async (workspaceId) => {
|
|
306
310
|
const started = performance.now();
|
|
307
311
|
this.stage({
|
|
308
312
|
primitive: "download", stage: "registry-lookup", status: "started",
|
|
309
313
|
});
|
|
314
|
+
const pagesBefore = { ...this.graphPageReads };
|
|
315
|
+
const planeBefore = this.direct.metrics();
|
|
310
316
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
311
|
-
const cloud = await this.cloudPort(identity)
|
|
317
|
+
const cloud = await this.cloudPort(identity, { kind: "workspace", workspaceId })
|
|
318
|
+
.lookup(resourceId);
|
|
312
319
|
if (!cloud)
|
|
313
320
|
throw new Error("this user has no cloud entity registry");
|
|
314
321
|
// The authority's active snapshot is the compact base; Add's active
|
|
@@ -322,7 +329,7 @@ export class UserGroundHost {
|
|
|
322
329
|
if (deliveryWatermark < cloud.deliveryWatermark) {
|
|
323
330
|
throw new Error("local delivery cursor is behind the active snapshot watermark");
|
|
324
331
|
}
|
|
325
|
-
const snapshot = snapshotFromRecords(
|
|
332
|
+
const snapshot = snapshotFromRecords(await this.foldScopedPrefix(cloud.snapshot.records, readAcceptedResults(this.databasePath(identity), resourceId, cloud.deliveryWatermark, deliveryWatermark), workspaceId));
|
|
326
333
|
const current = {
|
|
327
334
|
...cloud,
|
|
328
335
|
deliveryWatermark,
|
|
@@ -334,6 +341,13 @@ export class UserGroundHost {
|
|
|
334
341
|
stage: "registry-lookup",
|
|
335
342
|
status: "completed",
|
|
336
343
|
durationMs: performance.now() - started,
|
|
344
|
+
measurements: {
|
|
345
|
+
records: snapshot.records.length,
|
|
346
|
+
fetchedPages: this.graphPageReads.fetchedPages - pagesBefore.fetchedPages,
|
|
347
|
+
fetchedPageBytes: this.graphPageReads.fetchedBytes - pagesBefore.fetchedBytes,
|
|
348
|
+
cachedPages: this.graphPageReads.cachedPages - pagesBefore.cachedPages,
|
|
349
|
+
...physicalPlaneDelta(planeBefore, this.direct.metrics()),
|
|
350
|
+
},
|
|
337
351
|
});
|
|
338
352
|
return current;
|
|
339
353
|
},
|
|
@@ -564,12 +578,29 @@ export class UserGroundHost {
|
|
|
564
578
|
throw stageError("Watch coverage", error);
|
|
565
579
|
}
|
|
566
580
|
const database = this.databasePath(identity);
|
|
567
|
-
|
|
568
|
-
|
|
581
|
+
// Both proofs are root-scoped reads: the boundary's own record from
|
|
582
|
+
// whichever root holds it, and its durable reference from the core root
|
|
583
|
+
// — the only root references lawfully live in.
|
|
584
|
+
const hasRegisteredBoundary = async () => {
|
|
585
|
+
const graph = this.cloudState?.graph;
|
|
586
|
+
if (!graph)
|
|
587
|
+
return false;
|
|
588
|
+
const boundaryRoot = await graph.locate(workspaceId);
|
|
589
|
+
if (boundaryRoot === null)
|
|
590
|
+
return false;
|
|
591
|
+
const boundary = (await graph.recordsFor(boundaryRoot))
|
|
592
|
+
.find((record) => record.uuid === workspaceId);
|
|
593
|
+
if (!boundary || boundary.status !== "active")
|
|
594
|
+
return false;
|
|
595
|
+
const coreRoot = await this.coreRootId(identity, graph);
|
|
596
|
+
if (coreRoot === null)
|
|
597
|
+
return false;
|
|
598
|
+
return (await graph.recordsFor(coreRoot)).some((record) => record.type === "reference"
|
|
569
599
|
&& record.status === "active"
|
|
570
600
|
&& record.payloadVersion === workspaceId);
|
|
601
|
+
};
|
|
571
602
|
if (hasRegisteredBoundaryInDatabase(database, workspaceId)
|
|
572
|
-
&& hasRegisteredBoundary(
|
|
603
|
+
&& await hasRegisteredBoundary()) {
|
|
573
604
|
// Already declared — but Register may only return once cargo closure
|
|
574
605
|
// holds under the current content contract. The graph head can be
|
|
575
606
|
// unchanged while a contract cutover moved its cargo namespace, so
|
|
@@ -751,7 +782,7 @@ export class UserGroundHost {
|
|
|
751
782
|
if (!hasRegisteredBoundaryInDatabase(database, workspaceId)) {
|
|
752
783
|
throw stageError("entity registration", new Error(`workspace ${workspaceId} and its durable reference were not recorded`));
|
|
753
784
|
}
|
|
754
|
-
if (!hasRegisteredBoundary(
|
|
785
|
+
if (!(await hasRegisteredBoundary())) {
|
|
755
786
|
throw stageError("cloud graph commit", new Error(`workspace ${workspaceId} or its durable reference is absent from the committed head`));
|
|
756
787
|
}
|
|
757
788
|
return {
|
|
@@ -1128,7 +1159,7 @@ export class UserGroundHost {
|
|
|
1128
1159
|
cacheDir(identity) {
|
|
1129
1160
|
return join(liveMachineStateDir(this.userRoot(identity), identity.deviceId), "content");
|
|
1130
1161
|
}
|
|
1131
|
-
cloudPort(identity) {
|
|
1162
|
+
cloudPort(identity, scope) {
|
|
1132
1163
|
return {
|
|
1133
1164
|
lookup: async (resourceId) => {
|
|
1134
1165
|
await this.publishPendingSnapshotsBeforeLookup(identity, resourceId);
|
|
@@ -1139,7 +1170,8 @@ export class UserGroundHost {
|
|
|
1139
1170
|
}, ["private.resource.ready", "private.resource.missing"]);
|
|
1140
1171
|
if (frame.type === "private.resource.missing")
|
|
1141
1172
|
return null;
|
|
1142
|
-
const
|
|
1173
|
+
const graph = this.materializeRegistry(identity, frame);
|
|
1174
|
+
const ready = readyFromFrame(frame, graph, await this.selectScope(identity, graph, scope));
|
|
1143
1175
|
this.cloudState = ready.state;
|
|
1144
1176
|
return ready.cloud;
|
|
1145
1177
|
},
|
|
@@ -1154,7 +1186,8 @@ export class UserGroundHost {
|
|
|
1154
1186
|
snapshot_checksum: input.checksum,
|
|
1155
1187
|
snapshot_bytes: bytes.length,
|
|
1156
1188
|
}, ["private.resource.ready"]);
|
|
1157
|
-
const
|
|
1189
|
+
const graph = this.materializeRegistry(identity, frame);
|
|
1190
|
+
const ready = readyFromFrame(frame, graph, await this.selectScope(identity, graph, scope));
|
|
1158
1191
|
const current = ready.cloud;
|
|
1159
1192
|
this.cloudState = ready.state;
|
|
1160
1193
|
const created = frame.duplicate === false;
|
|
@@ -1164,60 +1197,103 @@ export class UserGroundHost {
|
|
|
1164
1197
|
},
|
|
1165
1198
|
};
|
|
1166
1199
|
}
|
|
1167
|
-
/** The authority's stored snapshot as this machine's graph. A head
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
|
|
1200
|
+
/** The authority's stored snapshot as this machine's graph. A head is held
|
|
1201
|
+
* lazily — no page is read until an operation asks for a root — and every
|
|
1202
|
+
* page this machine already cached is a local read, so an unchanged root
|
|
1203
|
+
* never crosses the network twice. */
|
|
1204
|
+
materializeRegistry(identity, frame) {
|
|
1171
1205
|
const resourceId = String(frame.resource_id || "");
|
|
1172
|
-
return registryGraphFromSnapshot(Buffer.from(String(frame.snapshot_b64 || ""), "base64"), (
|
|
1173
|
-
}
|
|
1174
|
-
/**
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1206
|
+
return registryGraphFromSnapshot(Buffer.from(String(frame.snapshot_b64 || ""), "base64"), (ref) => this.readGraphPage(identity, resourceId, ref), sha256Hex);
|
|
1207
|
+
}
|
|
1208
|
+
/** The records a lookup hands to Live: every root's own record — the
|
|
1209
|
+
* discovery set that proves root uniqueness — plus the full records of
|
|
1210
|
+
* exactly the roots the operation acts on. A records-shaped snapshot is a
|
|
1211
|
+
* first publication's own small graph and travels whole, keeping its
|
|
1212
|
+
* records-shape checksum law intact. */
|
|
1213
|
+
async selectScope(identity, graph, scope) {
|
|
1214
|
+
if (graph.head === null)
|
|
1215
|
+
return graph.recordsShapedSnapshot().records;
|
|
1216
|
+
if (scope.kind === "none")
|
|
1217
|
+
return [];
|
|
1218
|
+
const selected = new Set();
|
|
1219
|
+
const core = await this.coreRootId(identity, graph);
|
|
1220
|
+
if (core)
|
|
1221
|
+
selected.add(core);
|
|
1222
|
+
if (scope.kind === "workspace") {
|
|
1223
|
+
const boundaryRoot = await graph.locate(scope.workspaceId);
|
|
1224
|
+
if (boundaryRoot)
|
|
1225
|
+
selected.add(boundaryRoot);
|
|
1226
|
+
}
|
|
1227
|
+
const records = new Map((await graph.rootRecords())
|
|
1228
|
+
.map((record) => [record.uuid, record]));
|
|
1229
|
+
for (const rootId of selected) {
|
|
1230
|
+
for (const record of await graph.recordsFor(rootId))
|
|
1231
|
+
records.set(record.uuid, record);
|
|
1232
|
+
}
|
|
1233
|
+
return [...records.values()];
|
|
1234
|
+
}
|
|
1235
|
+
/** The account's core root: the one active top-level workspace named by the
|
|
1236
|
+
* account email. Registered-boundary references live only there. */
|
|
1237
|
+
async coreRootId(identity, graph) {
|
|
1238
|
+
const email = identity.userEmail.toLowerCase();
|
|
1239
|
+
const core = (await graph.rootRecords()).find((record) => record.status === "active"
|
|
1240
|
+
&& record.parentUUID === null
|
|
1241
|
+
&& record.type === "workspace"
|
|
1242
|
+
&& record.name.toLowerCase() === email);
|
|
1243
|
+
return core?.uuid ?? null;
|
|
1244
|
+
}
|
|
1245
|
+
/** Fold the committed delivery tail over a root-scoped base without losing
|
|
1246
|
+
* a subtree the tail moved across roots. A container the folded boundary
|
|
1247
|
+
* contains only through delivered results may still file unchanged
|
|
1248
|
+
* descendants under the root that used to hold it, so that root's records
|
|
1249
|
+
* join the base and the fold reruns until no probe widens it. Leaf results
|
|
1250
|
+
* travel whole and never probe. */
|
|
1251
|
+
async foldScopedPrefix(base, accepted, boundaryId) {
|
|
1252
|
+
if (accepted.length === 0)
|
|
1253
|
+
return base;
|
|
1254
|
+
const graph = this.cloudState?.graph;
|
|
1255
|
+
if (!graph)
|
|
1256
|
+
throw new Error("cloud state is unavailable for a registry lookup");
|
|
1257
|
+
const probed = new Set(base.map((record) => record.uuid));
|
|
1258
|
+
let widened = base;
|
|
1259
|
+
for (;;) {
|
|
1260
|
+
const folded = recordsAtDeliveryPrefix(widened, accepted);
|
|
1261
|
+
const probes = containedRecords(folded, boundaryId)
|
|
1262
|
+
.filter((record) => !probed.has(record.uuid)
|
|
1263
|
+
&& (record.type === "workspace" || record.type === "folder" || record.type === "repo.git"))
|
|
1264
|
+
.map((record) => record.uuid);
|
|
1265
|
+
if (probes.length === 0)
|
|
1266
|
+
return folded;
|
|
1267
|
+
for (const uuid of probes)
|
|
1268
|
+
probed.add(uuid);
|
|
1269
|
+
const located = await graph.membership(new Set(probes));
|
|
1270
|
+
const merged = new Map(widened.map((record) => [record.uuid, record]));
|
|
1271
|
+
for (const rootId of new Set(located.values())) {
|
|
1272
|
+
for (const record of await graph.recordsFor(rootId)) {
|
|
1273
|
+
merged.set(record.uuid, record);
|
|
1274
|
+
}
|
|
1204
1275
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1276
|
+
if (merged.size === widened.length)
|
|
1277
|
+
return folded;
|
|
1278
|
+
widened = [...merged.values()];
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/** Read one graph page by identity: cached pages come off local disk, the
|
|
1282
|
+
* rest are fetched straight from content storage and cached on the way in.
|
|
1283
|
+
* The graph proves every page against its declared identity as it parses,
|
|
1284
|
+
* so this reader only guards the cache against torn copies. */
|
|
1285
|
+
async readGraphPage(identity, resourceId, ref) {
|
|
1286
|
+
const path = join(graphPageCacheDir(this.cacheDir(identity)), `${ref.sha256}.bin`);
|
|
1287
|
+
const cached = readVerifiedRegistryPage(path, ref, sha256Hex);
|
|
1288
|
+
if (cached) {
|
|
1289
|
+
this.graphPageReads.cachedPages += 1;
|
|
1290
|
+
return cached;
|
|
1291
|
+
}
|
|
1292
|
+
const bytes = await retryContent(() => this.direct.fetchObject(resourceId, { kind: "block", sha256: ref.sha256 }), true);
|
|
1293
|
+
writeVerifiedRegistryPage(path, ref, bytes, sha256Hex);
|
|
1294
|
+
this.graphPageReads.fetchedPages += 1;
|
|
1295
|
+
this.graphPageReads.fetchedBytes += bytes.byteLength;
|
|
1296
|
+
return bytes;
|
|
1221
1297
|
}
|
|
1222
1298
|
localPort(identity) {
|
|
1223
1299
|
const userRoot = this.userRoot(identity);
|
|
@@ -1604,11 +1680,13 @@ export class UserGroundHost {
|
|
|
1604
1680
|
const unique = new Map(wants.map((want) => [want.artifact.contentHash, want]));
|
|
1605
1681
|
const sole = unique.size === 1 ? [...unique.values()][0]?.artifact : undefined;
|
|
1606
1682
|
const started = performance.now();
|
|
1683
|
+
const planeBefore = this.direct.metrics();
|
|
1607
1684
|
let declaredBytes = 0;
|
|
1608
1685
|
let downloadedBytes = 0;
|
|
1609
1686
|
let downloadedChunks = 0;
|
|
1610
1687
|
let reusedChunks = 0;
|
|
1611
1688
|
let requests = 0;
|
|
1689
|
+
let cacheWriteMs = 0;
|
|
1612
1690
|
const entityId = trace?.entityId ?? sole?.entityId;
|
|
1613
1691
|
const identifiers = {
|
|
1614
1692
|
...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
@@ -1673,10 +1751,14 @@ export class UserGroundHost {
|
|
|
1673
1751
|
return fetchObject({ kind: "block", sha256: chunk.sha256 });
|
|
1674
1752
|
},
|
|
1675
1753
|
putChunk: async (artifact, _manifest, index, bytes) => {
|
|
1754
|
+
const writeStarted = performance.now();
|
|
1676
1755
|
await target(artifact).putChunk(index, bytes);
|
|
1756
|
+
cacheWriteMs += performance.now() - writeStarted;
|
|
1677
1757
|
},
|
|
1678
1758
|
sealArtifact: async (artifact) => {
|
|
1759
|
+
const sealStarted = performance.now();
|
|
1679
1760
|
const sealed = await target(artifact).seal();
|
|
1761
|
+
cacheWriteMs += performance.now() - sealStarted;
|
|
1680
1762
|
return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
|
|
1681
1763
|
},
|
|
1682
1764
|
}, {
|
|
@@ -1704,7 +1786,12 @@ export class UserGroundHost {
|
|
|
1704
1786
|
downloadedBytes,
|
|
1705
1787
|
downloadedChunks,
|
|
1706
1788
|
reusedChunks,
|
|
1789
|
+
// `requests` is logical object reads; the plane deltas below are
|
|
1790
|
+
// the physical facts — actual storage GETs, sealed bytes on the
|
|
1791
|
+
// wire, refs round-trips, decrypt and cache-write CPU.
|
|
1707
1792
|
requests,
|
|
1793
|
+
...physicalPlaneDelta(planeBefore, this.direct.metrics()),
|
|
1794
|
+
cacheWriteMs: Math.round(cacheWriteMs),
|
|
1708
1795
|
},
|
|
1709
1796
|
});
|
|
1710
1797
|
return content;
|
|
@@ -2124,7 +2211,8 @@ export class UserGroundHost {
|
|
|
2124
2211
|
// the roots it rebuilt — so the machine re-reads the authority's head
|
|
2125
2212
|
// before deriving the next boundary on top of it.
|
|
2126
2213
|
if (!this.cloudState) {
|
|
2127
|
-
await this.cloudPort(identity
|
|
2214
|
+
await this.cloudPort(identity, { kind: "none" })
|
|
2215
|
+
.lookup(privateEntityResourceId(identity.userId, sha256Hex));
|
|
2128
2216
|
}
|
|
2129
2217
|
const state = this.cloudState;
|
|
2130
2218
|
if (!state)
|
|
@@ -2155,6 +2243,12 @@ export class UserGroundHost {
|
|
|
2155
2243
|
leg[`${name}Ms`] = performance.now() - started;
|
|
2156
2244
|
return value;
|
|
2157
2245
|
};
|
|
2246
|
+
const timedAsync = async (name, work) => {
|
|
2247
|
+
const started = performance.now();
|
|
2248
|
+
const value = await work();
|
|
2249
|
+
leg[`${name}Ms`] = performance.now() - started;
|
|
2250
|
+
return value;
|
|
2251
|
+
};
|
|
2158
2252
|
if (trace?.journey === "register") {
|
|
2159
2253
|
this.stage({
|
|
2160
2254
|
primitive: "register",
|
|
@@ -2167,7 +2261,7 @@ export class UserGroundHost {
|
|
|
2167
2261
|
.map(portableRecord), readGroundRowsForRoots(this.databasePath(identity), "detection_notebook", publicationRootIds)
|
|
2168
2262
|
.map(portableRecord)));
|
|
2169
2263
|
const accepted = timed("officialRows", () => readAcceptedResults(this.databasePath(identity), state.resourceId, state.deliveryWatermark, deliveryWatermark));
|
|
2170
|
-
const rebuilt =
|
|
2264
|
+
const rebuilt = await timedAsync("merge", () => publicationRoots(state.graph, localRecords, accepted));
|
|
2171
2265
|
const projected = timed("project", () => new Map([...rebuilt].map(([rootId, records]) => [rootId, records.length === 0 ? records : travelingRecords(records, sha256Hex)])));
|
|
2172
2266
|
const traveling = [...projected.values()].flat();
|
|
2173
2267
|
const impossibleGitLeaf = traveling.find((record) => (record.type === "file.text" || record.type === "file.binary" || record.type === "link")
|
|
@@ -2179,7 +2273,7 @@ export class UserGroundHost {
|
|
|
2179
2273
|
// ONE canonical serialization per rebuilt root: the page bytes are the
|
|
2180
2274
|
// journal's payload, the upload's payload, and the head's identities.
|
|
2181
2275
|
// Untouched roots are not serialized, hashed, or journaled at all.
|
|
2182
|
-
const publication = timed("pages", () => state.graph.rebuild(projected
|
|
2276
|
+
const publication = timed("pages", () => state.graph.rebuild(projected));
|
|
2183
2277
|
const checksum = publication.checksum;
|
|
2184
2278
|
const localIds = new Set(localRecords.map((record) => record.uuid));
|
|
2185
2279
|
// Graph membership is never proof of sealed cargo: a record in the cloud
|
|
@@ -5177,7 +5271,7 @@ function recordRootResolver(records) {
|
|
|
5177
5271
|
* touched is rebuilt from the committed graph with those results folded in, so
|
|
5178
5272
|
* the published graph never declares a watermark it has not actually applied.
|
|
5179
5273
|
* Every root outside this map keeps its committed pages untouched. */
|
|
5180
|
-
function publicationRoots(graph, localRecords, accepted) {
|
|
5274
|
+
async function publicationRoots(graph, localRecords, accepted) {
|
|
5181
5275
|
const rebuilt = new Map();
|
|
5182
5276
|
const walked = new Map(localRecords
|
|
5183
5277
|
.filter((record) => record.parentUUID === null)
|
|
@@ -5194,9 +5288,16 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5194
5288
|
for (const [rootId, records] of walked)
|
|
5195
5289
|
rebuilt.set(rootId, records);
|
|
5196
5290
|
}
|
|
5291
|
+
// One page-bounded sweep answers every committed-root question this merge
|
|
5292
|
+
// asks: which root holds each accepted result, and which root still files
|
|
5293
|
+
// each record the walk re-rooted.
|
|
5294
|
+
const located = await graph.membership(new Set([
|
|
5295
|
+
...accepted.map((result) => result.uuid),
|
|
5296
|
+
...localRecords.map((record) => record.uuid),
|
|
5297
|
+
]));
|
|
5197
5298
|
const overlays = new Map();
|
|
5198
5299
|
for (const result of accepted) {
|
|
5199
|
-
const rootId =
|
|
5300
|
+
const rootId = located.get(result.uuid) ?? null;
|
|
5200
5301
|
if (rootId === null || walked.has(rootId))
|
|
5201
5302
|
continue;
|
|
5202
5303
|
const overlay = overlays.get(rootId);
|
|
@@ -5206,7 +5307,8 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5206
5307
|
overlays.set(rootId, [result]);
|
|
5207
5308
|
}
|
|
5208
5309
|
for (const [rootId, results] of overlays) {
|
|
5209
|
-
const byUuid = new Map(graph.recordsFor(rootId)
|
|
5310
|
+
const byUuid = new Map((await graph.recordsFor(rootId))
|
|
5311
|
+
.map((record) => [record.uuid, record]));
|
|
5210
5312
|
for (const result of results)
|
|
5211
5313
|
byUuid.set(result.uuid, result);
|
|
5212
5314
|
// Entity channels are independently ordered, so container membership is
|
|
@@ -5219,7 +5321,7 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5219
5321
|
if (graph.head === null) {
|
|
5220
5322
|
for (const rootId of graph.rootIds()) {
|
|
5221
5323
|
if (!rebuilt.has(rootId))
|
|
5222
|
-
rebuilt.set(rootId, graph.recordsFor(rootId));
|
|
5324
|
+
rebuilt.set(rootId, await graph.recordsFor(rootId));
|
|
5223
5325
|
}
|
|
5224
5326
|
}
|
|
5225
5327
|
// Re-rooting is a move, not a copy: an enclosing Register walks records the
|
|
@@ -5228,7 +5330,7 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5228
5330
|
// with no records at all — the rebuild then drops it from the graph.
|
|
5229
5331
|
const vacated = new Map();
|
|
5230
5332
|
for (const record of localRecords) {
|
|
5231
|
-
const rootId =
|
|
5333
|
+
const rootId = located.get(record.uuid) ?? null;
|
|
5232
5334
|
if (rootId === null || walked.has(rootId))
|
|
5233
5335
|
continue;
|
|
5234
5336
|
const gone = vacated.get(rootId);
|
|
@@ -5238,7 +5340,7 @@ function publicationRoots(graph, localRecords, accepted) {
|
|
|
5238
5340
|
vacated.set(rootId, new Set([record.uuid]));
|
|
5239
5341
|
}
|
|
5240
5342
|
for (const [rootId, gone] of vacated) {
|
|
5241
|
-
const held = rebuilt.get(rootId) ?? graph.recordsFor(rootId);
|
|
5343
|
+
const held = rebuilt.get(rootId) ?? await graph.recordsFor(rootId);
|
|
5242
5344
|
rebuilt.set(rootId, held.filter((record) => !gone.has(record.uuid)));
|
|
5243
5345
|
}
|
|
5244
5346
|
return rebuilt;
|
|
@@ -5814,13 +5916,15 @@ async function installCloudWorkspace(input) {
|
|
|
5814
5916
|
alreadyMaterialized: false,
|
|
5815
5917
|
};
|
|
5816
5918
|
}
|
|
5817
|
-
|
|
5919
|
+
/** The authority's ready frame as Live sees it. The snapshot carries only the
|
|
5920
|
+
* records the caller's scope selected — never a flattened whole account. */
|
|
5921
|
+
function readyFromFrame(frame, graph, records) {
|
|
5818
5922
|
const cloud = {
|
|
5819
5923
|
resourceId: String(frame.resource_id || ""),
|
|
5820
5924
|
version: Number(frame.head_version),
|
|
5821
5925
|
deliveryWatermark: Number(frame.delivery_watermark),
|
|
5822
5926
|
checksum: String(frame.snapshot_checksum || ""),
|
|
5823
|
-
snapshot:
|
|
5927
|
+
snapshot: snapshotFromRecords(records),
|
|
5824
5928
|
...(graph.head ? { head: graph.head } : {}),
|
|
5825
5929
|
};
|
|
5826
5930
|
return {
|
|
@@ -5835,6 +5939,18 @@ function readyFromFrame(frame, graph) {
|
|
|
5835
5939
|
},
|
|
5836
5940
|
};
|
|
5837
5941
|
}
|
|
5942
|
+
/** What one leg physically did on the content plane: the counter movement
|
|
5943
|
+
* between its start and end, ms rounded for the ledger. */
|
|
5944
|
+
function physicalPlaneDelta(before, after) {
|
|
5945
|
+
return {
|
|
5946
|
+
rangedGets: after.rangedGets - before.rangedGets,
|
|
5947
|
+
wholeGets: after.wholeGets - before.wholeGets,
|
|
5948
|
+
fetchedSealedBytes: after.fetchedBytes - before.fetchedBytes,
|
|
5949
|
+
refsFrames: after.refsFrames - before.refsFrames,
|
|
5950
|
+
refsMs: Math.round(after.refsMs - before.refsMs),
|
|
5951
|
+
decryptMs: Math.round(after.decryptMs - before.decryptMs),
|
|
5952
|
+
};
|
|
5953
|
+
}
|
|
5838
5954
|
/** Immutable content reads and writes are idempotent. Retry transient wire or
|
|
5839
5955
|
* authority failures; reads additionally tolerate the brief not-found window
|
|
5840
5956
|
* between an immutable object acknowledgement and globally visible R2 state. */
|