@amalgm/shell 0.1.72 → 0.1.74
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 +9 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/detection/runtime.d.ts +7 -2
- package/dist/detection/runtime.js +19 -4
- package/dist/detection/runtime.js.map +1 -1
- package/dist/entity-record-observability.d.ts +13 -0
- package/dist/entity-record-observability.js +143 -0
- package/dist/entity-record-observability.js.map +1 -0
- package/dist/files-observability.d.ts +26 -0
- package/dist/files-observability.js +11 -0
- package/dist/files-observability.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/machine-host.d.ts +3 -0
- package/dist/machine-host.js +67 -5
- package/dist/machine-host.js.map +1 -1
- package/dist/user-ground-host.d.ts +5 -0
- package/dist/user-ground-host.js +449 -33
- package/dist/user-ground-host.js.map +1 -1
- package/dist/watching/host.d.ts +9 -0
- package/dist/watching/host.js +4 -0
- package/dist/watching/host.js.map +1 -1
- package/dist/watching/index.d.ts +1 -1
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -6,8 +6,10 @@ import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAm
|
|
|
6
6
|
import { CHUNK_BYTES, CONTENT_CONTRACT, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, createLocalEntityRecord, createEntityRecordAuthorityPort, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, download as downloadArtifact, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, privateEntityResourceId, 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 } from "./filesystem.js";
|
|
9
|
+
import { emitFilesStage, } from "./files-observability.js";
|
|
9
10
|
import { EntityApplyHost, } from "./entity-apply-host.js";
|
|
10
11
|
import { EntityRecordSqliteStore } from "./entity-record-store.js";
|
|
12
|
+
import { observeEntityRecordPorts } from "./entity-record-observability.js";
|
|
11
13
|
import { indexRowsByOutermostRoot } from "./ground-root-index.js";
|
|
12
14
|
import { ContentCacheDownload, captureContentFile, hashContentFile, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
13
15
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
@@ -67,7 +69,29 @@ export class UserGroundHost {
|
|
|
67
69
|
this.watchHost = new NodeWatchHost({
|
|
68
70
|
open: options.openWatch,
|
|
69
71
|
onSignal: () => this.scheduleRescan(),
|
|
70
|
-
|
|
72
|
+
onEvent: (evidence) => this.stage({
|
|
73
|
+
primitive: "watch",
|
|
74
|
+
stage: "native-suspicion",
|
|
75
|
+
status: "observed",
|
|
76
|
+
path: evidence.path === null
|
|
77
|
+
? evidence.directory
|
|
78
|
+
: join(evidence.directory, ...evidence.path.split("/")),
|
|
79
|
+
measurements: { kind: evidence.kind, named: evidence.path !== null },
|
|
80
|
+
}),
|
|
81
|
+
onHealth: (health) => {
|
|
82
|
+
options.onWatchHealth?.(health);
|
|
83
|
+
this.stage({
|
|
84
|
+
primitive: "watch",
|
|
85
|
+
stage: "coverage-health",
|
|
86
|
+
status: "observed",
|
|
87
|
+
measurements: {
|
|
88
|
+
state: health.state,
|
|
89
|
+
logicalRoots: health.logicalRoots,
|
|
90
|
+
contentHandles: health.contentHandles,
|
|
91
|
+
addressHandles: health.addressHandles,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
},
|
|
71
95
|
});
|
|
72
96
|
this.port = {
|
|
73
97
|
converge: async (identity) => {
|
|
@@ -80,7 +104,29 @@ export class UserGroundHost {
|
|
|
80
104
|
local,
|
|
81
105
|
});
|
|
82
106
|
await this.resumeWorkspaceAdds(this.activeIdentity);
|
|
83
|
-
this.namedDetect ??= new NamedDetectRuntime(this.databasePath(this.activeIdentity), this.cacheDir(this.activeIdentity), this.userRoot(this.activeIdentity))
|
|
107
|
+
this.namedDetect ??= new NamedDetectRuntime(this.databasePath(this.activeIdentity), this.cacheDir(this.activeIdentity), this.userRoot(this.activeIdentity), ({ durationMs, records }) => {
|
|
108
|
+
if (records.length === 0) {
|
|
109
|
+
this.stage({
|
|
110
|
+
primitive: "record",
|
|
111
|
+
stage: "sqlite-transaction",
|
|
112
|
+
status: "completed",
|
|
113
|
+
durationMs,
|
|
114
|
+
measurements: { records: 0 },
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const record of records) {
|
|
119
|
+
this.stage({
|
|
120
|
+
primitive: "record",
|
|
121
|
+
stage: "outbox-committed",
|
|
122
|
+
status: "completed",
|
|
123
|
+
entityId: record.entityId,
|
|
124
|
+
mutationId: record.mutationId,
|
|
125
|
+
durationMs,
|
|
126
|
+
measurements: { records: records.length },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
84
130
|
const watchHealth = this.watchHost.health();
|
|
85
131
|
if (watchHealth.state !== "healthy") {
|
|
86
132
|
throw new Error(`Watch coverage is degraded: ${watchHealth.reason ?? "unknown failure"}`);
|
|
@@ -103,6 +149,30 @@ export class UserGroundHost {
|
|
|
103
149
|
},
|
|
104
150
|
};
|
|
105
151
|
}
|
|
152
|
+
stage(input) {
|
|
153
|
+
emitFilesStage(this.options.onFilesStage, input);
|
|
154
|
+
}
|
|
155
|
+
reportDetectScan(evidence) {
|
|
156
|
+
this.options.onDetectScan?.(evidence);
|
|
157
|
+
this.stage({
|
|
158
|
+
primitive: "detect",
|
|
159
|
+
stage: "scan",
|
|
160
|
+
status: "completed",
|
|
161
|
+
rootId: evidence.rootId,
|
|
162
|
+
path: evidence.directory,
|
|
163
|
+
durationMs: evidence.durationMs,
|
|
164
|
+
measurements: {
|
|
165
|
+
scope: evidence.scope,
|
|
166
|
+
entries: evidence.entries,
|
|
167
|
+
metadataReused: evidence.metadataReused,
|
|
168
|
+
contentReads: evidence.contentReads,
|
|
169
|
+
gitInspections: evidence.gitInspections,
|
|
170
|
+
repositoryCaptures: evidence.repositoryCaptures,
|
|
171
|
+
records: evidence.records ?? 0,
|
|
172
|
+
recordBytes: evidence.recordBytes ?? 0,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
106
176
|
databasePath(identity) {
|
|
107
177
|
return join(liveMachineStateDir(this.userRoot(identity), identity.deviceId), "identity.db");
|
|
108
178
|
}
|
|
@@ -151,14 +221,39 @@ export class UserGroundHost {
|
|
|
151
221
|
});
|
|
152
222
|
const add = {
|
|
153
223
|
lookupRegistry: async () => {
|
|
224
|
+
const started = performance.now();
|
|
225
|
+
this.stage({
|
|
226
|
+
primitive: "download", stage: "registry-lookup", status: "started",
|
|
227
|
+
});
|
|
154
228
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
155
229
|
const cloud = await this.cloudPort(identity).lookup(resourceId);
|
|
156
230
|
if (!cloud)
|
|
157
231
|
throw new Error("this user has no cloud entity registry");
|
|
232
|
+
this.stage({
|
|
233
|
+
primitive: "download",
|
|
234
|
+
stage: "registry-lookup",
|
|
235
|
+
status: "completed",
|
|
236
|
+
durationMs: performance.now() - started,
|
|
237
|
+
});
|
|
158
238
|
return cloud;
|
|
159
239
|
},
|
|
160
240
|
materializeWorkspace: async (input) => {
|
|
241
|
+
const groundWaitStarted = performance.now();
|
|
242
|
+
this.stage({
|
|
243
|
+
primitive: "download",
|
|
244
|
+
stage: "ground-wait",
|
|
245
|
+
status: "started",
|
|
246
|
+
workspaceId: input.workspace.uuid,
|
|
247
|
+
});
|
|
161
248
|
const release = await this.acquireGroundEffect();
|
|
249
|
+
this.stage({
|
|
250
|
+
primitive: "download",
|
|
251
|
+
stage: "ground-wait",
|
|
252
|
+
status: "completed",
|
|
253
|
+
workspaceId: input.workspace.uuid,
|
|
254
|
+
durationMs: performance.now() - groundWaitStarted,
|
|
255
|
+
});
|
|
256
|
+
let milestoneAt = performance.now();
|
|
162
257
|
try {
|
|
163
258
|
const installed = await installCloudWorkspace({
|
|
164
259
|
identity,
|
|
@@ -173,11 +268,22 @@ export class UserGroundHost {
|
|
|
173
268
|
records: input.records,
|
|
174
269
|
destinationParent: input.destinationParent,
|
|
175
270
|
cloudHead: input.cloudHead,
|
|
176
|
-
readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
|
|
177
|
-
onStage: (stage) =>
|
|
178
|
-
|
|
179
|
-
stage
|
|
180
|
-
|
|
271
|
+
readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact, 10, { journey: "add", workspaceId: input.workspace.uuid }),
|
|
272
|
+
onStage: async (stage) => {
|
|
273
|
+
const now = performance.now();
|
|
274
|
+
this.stage({
|
|
275
|
+
primitive: stage === "watching" ? "watch" : "download",
|
|
276
|
+
stage,
|
|
277
|
+
status: "completed",
|
|
278
|
+
workspaceId: input.workspace.uuid,
|
|
279
|
+
durationMs: now - milestoneAt,
|
|
280
|
+
});
|
|
281
|
+
milestoneAt = now;
|
|
282
|
+
await this.options.onWorkspaceAddStage?.({
|
|
283
|
+
workspaceId: input.workspace.uuid,
|
|
284
|
+
stage,
|
|
285
|
+
});
|
|
286
|
+
},
|
|
181
287
|
});
|
|
182
288
|
// Add changes both disk and the exact SQLite projection. Open the
|
|
183
289
|
// selected root's singular Watch coverage before Detect can observe
|
|
@@ -192,6 +298,10 @@ export class UserGroundHost {
|
|
|
192
298
|
}
|
|
193
299
|
},
|
|
194
300
|
coverWorkspace: async ({ workspaceId, baseline }) => {
|
|
301
|
+
const started = performance.now();
|
|
302
|
+
this.stage({
|
|
303
|
+
primitive: "watch", stage: "add-coverage", status: "started", workspaceId,
|
|
304
|
+
});
|
|
195
305
|
const pending = readWorkspaceAddIntent(this.databasePath(identity), workspaceId);
|
|
196
306
|
const health = this.ensureWatchers(identity, pending || baseline === "authoritative-install" ? [workspaceId] : []);
|
|
197
307
|
assertHealthyWatch(this.watchHost.evidence(), workspaceId);
|
|
@@ -199,6 +309,17 @@ export class UserGroundHost {
|
|
|
199
309
|
await this.options.onWorkspaceAddStage?.({ workspaceId, stage: "watching" });
|
|
200
310
|
deleteWorkspaceAddIntent(this.databasePath(identity), workspaceId);
|
|
201
311
|
}
|
|
312
|
+
this.stage({
|
|
313
|
+
primitive: "watch",
|
|
314
|
+
stage: "add-coverage",
|
|
315
|
+
status: "completed",
|
|
316
|
+
workspaceId,
|
|
317
|
+
durationMs: performance.now() - started,
|
|
318
|
+
measurements: {
|
|
319
|
+
contentHandles: health.contentHandles,
|
|
320
|
+
addressHandles: health.addressHandles,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
202
323
|
return { active: true, roots: health.contentHandles };
|
|
203
324
|
},
|
|
204
325
|
};
|
|
@@ -236,7 +357,7 @@ export class UserGroundHost {
|
|
|
236
357
|
records: intent.records,
|
|
237
358
|
destinationParent: dirname(intent.destinationPath),
|
|
238
359
|
cloudHead: intent.cloudHead,
|
|
239
|
-
readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact),
|
|
360
|
+
readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact, 10, { journey: "add", workspaceId: intent.workspaceId }),
|
|
240
361
|
onStage: (stage) => this.options.onWorkspaceAddStage?.({
|
|
241
362
|
workspaceId: intent.workspaceId,
|
|
242
363
|
stage,
|
|
@@ -277,9 +398,24 @@ export class UserGroundHost {
|
|
|
277
398
|
this.rescanGenerationStartedAt = null;
|
|
278
399
|
let watch;
|
|
279
400
|
try {
|
|
401
|
+
const watchStarted = performance.now();
|
|
402
|
+
this.stage({
|
|
403
|
+
primitive: "watch", stage: "register-coverage", status: "started", workspaceId,
|
|
404
|
+
});
|
|
280
405
|
this.ensureWatchers(identity);
|
|
281
406
|
watch = this.watchHost.evidence();
|
|
282
407
|
assertHealthyWatch(watch, workspaceId);
|
|
408
|
+
this.stage({
|
|
409
|
+
primitive: "watch",
|
|
410
|
+
stage: "register-coverage",
|
|
411
|
+
status: "completed",
|
|
412
|
+
workspaceId,
|
|
413
|
+
durationMs: performance.now() - watchStarted,
|
|
414
|
+
measurements: {
|
|
415
|
+
contentHandles: watch.health.contentHandles,
|
|
416
|
+
addressHandles: watch.health.addressHandles,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
283
419
|
}
|
|
284
420
|
catch (error) {
|
|
285
421
|
this.finishColdOperation();
|
|
@@ -305,9 +441,20 @@ export class UserGroundHost {
|
|
|
305
441
|
// a parallel stream of Detect records for the same initial contents.
|
|
306
442
|
let releaseGround = null;
|
|
307
443
|
try {
|
|
444
|
+
const groundWaitStarted = performance.now();
|
|
445
|
+
this.stage({
|
|
446
|
+
primitive: "register", stage: "ground-wait", status: "started", workspaceId,
|
|
447
|
+
});
|
|
308
448
|
if (this.syncing)
|
|
309
449
|
await this.syncing;
|
|
310
450
|
releaseGround = await this.acquireGroundEffect();
|
|
451
|
+
this.stage({
|
|
452
|
+
primitive: "register",
|
|
453
|
+
stage: "ground-wait",
|
|
454
|
+
status: "completed",
|
|
455
|
+
workspaceId,
|
|
456
|
+
durationMs: performance.now() - groundWaitStarted,
|
|
457
|
+
});
|
|
311
458
|
const contentOwner = watch.roots.find((root) => root.rootId === workspaceId)?.contentOwner;
|
|
312
459
|
if (!contentOwner) {
|
|
313
460
|
throw new Error(`registered workspace ${workspaceId} has no content coverage owner`);
|
|
@@ -315,12 +462,16 @@ export class UserGroundHost {
|
|
|
315
462
|
const observations = this.watchHost.observations().filter((observation) => observation.directory === this.userRoot(identity)
|
|
316
463
|
|| pathWithin(contentOwner, observation.directory));
|
|
317
464
|
this.watchDirty = false;
|
|
465
|
+
const registrationStarted = performance.now();
|
|
466
|
+
this.stage({
|
|
467
|
+
primitive: "register", stage: "entity-registration", status: "started", workspaceId,
|
|
468
|
+
});
|
|
318
469
|
const scanned = await reconcileGround({
|
|
319
470
|
identity,
|
|
320
471
|
userRoot: this.userRoot(identity),
|
|
321
472
|
database: this.databasePath(identity),
|
|
322
473
|
cacheDir: this.cacheDir(identity),
|
|
323
|
-
onScan: this.
|
|
474
|
+
onScan: (evidence) => this.reportDetectScan(evidence),
|
|
324
475
|
registrationWorkspaceId: workspaceId,
|
|
325
476
|
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
|
|
326
477
|
});
|
|
@@ -328,17 +479,39 @@ export class UserGroundHost {
|
|
|
328
479
|
if (retry?.kind === "retry") {
|
|
329
480
|
throw new Error(retry.reasons.join("; "));
|
|
330
481
|
}
|
|
482
|
+
this.stage({
|
|
483
|
+
primitive: "register",
|
|
484
|
+
stage: "entity-registration",
|
|
485
|
+
status: "completed",
|
|
486
|
+
workspaceId,
|
|
487
|
+
durationMs: performance.now() - registrationStarted,
|
|
488
|
+
measurements: {
|
|
489
|
+
roots: scanned.publicationRootIds?.length ?? 0,
|
|
490
|
+
rows: scanned.acceptedMaterializedRows.length,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
const commitStarted = performance.now();
|
|
494
|
+
this.stage({
|
|
495
|
+
primitive: "register", stage: "entity-sqlite-commit", status: "started", workspaceId,
|
|
496
|
+
});
|
|
331
497
|
commitDetectedState(this.databasePath(identity), {
|
|
332
498
|
acceptedMaterializedRows: scanned.acceptedMaterializedRows,
|
|
333
499
|
materializedRemovals: scanned.materializedRemovals,
|
|
334
500
|
acceptedNotebookRows: scanned.acceptedNotebookRows,
|
|
335
501
|
notebookRemovals: scanned.notebookRemovals,
|
|
336
502
|
});
|
|
503
|
+
this.stage({
|
|
504
|
+
primitive: "register",
|
|
505
|
+
stage: "entity-sqlite-commit",
|
|
506
|
+
status: "completed",
|
|
507
|
+
workspaceId,
|
|
508
|
+
durationMs: performance.now() - commitStarted,
|
|
509
|
+
});
|
|
337
510
|
this.namedDetect?.refreshEnrollmentPolicy();
|
|
338
511
|
if (!scanned.publicationRootIds) {
|
|
339
512
|
throw new Error("cold registration reconciliation did not name its publication roots");
|
|
340
513
|
}
|
|
341
|
-
await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds));
|
|
514
|
+
await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds), { journey: "register", workspaceId });
|
|
342
515
|
this.watchHost.settle(observations);
|
|
343
516
|
}
|
|
344
517
|
catch (error) {
|
|
@@ -353,9 +526,21 @@ export class UserGroundHost {
|
|
|
353
526
|
this.finishColdOperation();
|
|
354
527
|
}
|
|
355
528
|
try {
|
|
529
|
+
const finalWatchStarted = performance.now();
|
|
356
530
|
this.ensureWatchers(identity);
|
|
357
531
|
watch = this.watchHost.evidence();
|
|
358
532
|
assertHealthyWatch(watch, workspaceId);
|
|
533
|
+
this.stage({
|
|
534
|
+
primitive: "watch",
|
|
535
|
+
stage: "register-final-proof",
|
|
536
|
+
status: "completed",
|
|
537
|
+
workspaceId,
|
|
538
|
+
durationMs: performance.now() - finalWatchStarted,
|
|
539
|
+
measurements: {
|
|
540
|
+
contentHandles: watch.health.contentHandles,
|
|
541
|
+
addressHandles: watch.health.addressHandles,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
359
544
|
}
|
|
360
545
|
catch (error) {
|
|
361
546
|
throw stageError("Watch coverage", error);
|
|
@@ -434,18 +619,28 @@ export class UserGroundHost {
|
|
|
434
619
|
throw new Error("gateway does not support the event-driven entity Record wire");
|
|
435
620
|
}
|
|
436
621
|
const store = new EntityRecordSqliteStore(initializeDatabase(this.databasePath(identity)));
|
|
437
|
-
const
|
|
622
|
+
const wireAuthority = createEntityRecordAuthorityPort({
|
|
438
623
|
request: (frame, acceptedTypes) => this.wire.request({ ...frame }, acceptedTypes),
|
|
439
624
|
onFrame: (listener) => this.wire.onEvent(listener),
|
|
440
625
|
onDisconnect: (listener) => this.wire.onDisconnect(listener),
|
|
441
626
|
});
|
|
442
|
-
const
|
|
443
|
-
authorityId: this.cloudState.resourceId,
|
|
627
|
+
const ports = observeEntityRecordPorts({
|
|
444
628
|
store,
|
|
445
|
-
authority,
|
|
629
|
+
authority: wireAuthority,
|
|
446
630
|
cargo: {
|
|
447
|
-
ensure: (record) => this.uploadSnapshotContent(identity, record.authorityId, [record.change.result]
|
|
631
|
+
ensure: (record) => this.uploadSnapshotContent(identity, record.authorityId, [record.change.result], {
|
|
632
|
+
journey: "send",
|
|
633
|
+
entityId: record.entityId,
|
|
634
|
+
mutationId: record.mutationId,
|
|
635
|
+
}),
|
|
448
636
|
},
|
|
637
|
+
onStage: this.options.onFilesStage,
|
|
638
|
+
});
|
|
639
|
+
const rail = new EntityRecordRail({
|
|
640
|
+
authorityId: this.cloudState.resourceId,
|
|
641
|
+
store: ports.store,
|
|
642
|
+
authority: ports.authority,
|
|
643
|
+
cargo: ports.cargo,
|
|
449
644
|
schedule: {
|
|
450
645
|
schedule(delayMs, callback) {
|
|
451
646
|
const timer = setTimeout(callback, delayMs);
|
|
@@ -482,7 +677,7 @@ export class UserGroundHost {
|
|
|
482
677
|
userRoot: this.userRoot(identity),
|
|
483
678
|
cacheDir: this.cacheDir(identity),
|
|
484
679
|
bindingDir: workspaceBindingDir(this.userRoot(identity), identity.deviceId),
|
|
485
|
-
readContent: (artifact) => this.downloadContent(this.cloudState.resourceId, this.cacheDir(identity), artifact, DOWNLOAD_CONTENT_RETRY_ATTEMPTS),
|
|
680
|
+
readContent: (artifact) => this.downloadContent(this.cloudState.resourceId, this.cacheDir(identity), artifact, DOWNLOAD_CONTENT_RETRY_ATTEMPTS, { journey: "apply" }),
|
|
486
681
|
captureLocal: async (intent, plan, position) => {
|
|
487
682
|
// Apply never suppresses Watch. This explicit whole-root suspicion is
|
|
488
683
|
// the synchronous proof boundary used only when Apply observed a
|
|
@@ -495,7 +690,17 @@ export class UserGroundHost {
|
|
|
495
690
|
}
|
|
496
691
|
},
|
|
497
692
|
acquireGround: () => this.acquireGroundEffect(APPLY_GROUND_WAIT_TIMEOUT_MS),
|
|
498
|
-
onStage: (evidence) =>
|
|
693
|
+
onStage: (evidence) => {
|
|
694
|
+
this.options.onApplyStage?.(evidence);
|
|
695
|
+
this.stage({
|
|
696
|
+
primitive: "apply",
|
|
697
|
+
stage: evidence.stage,
|
|
698
|
+
status: "observed",
|
|
699
|
+
entityId: evidence.entityId,
|
|
700
|
+
globalSequence: evidence.throughSequence,
|
|
701
|
+
measurements: { type: evidence.type, phase: evidence.phase },
|
|
702
|
+
});
|
|
703
|
+
},
|
|
499
704
|
now: () => this.options.now?.().getTime() ?? Date.now(),
|
|
500
705
|
});
|
|
501
706
|
const rail = new EntityApplyRail({
|
|
@@ -672,7 +877,7 @@ export class UserGroundHost {
|
|
|
672
877
|
});
|
|
673
878
|
await reconcileGround({
|
|
674
879
|
identity, userRoot, database, cacheDir,
|
|
675
|
-
onScan: this.
|
|
880
|
+
onScan: (evidence) => this.reportDetectScan(evidence),
|
|
676
881
|
});
|
|
677
882
|
return localValue(identity, userRoot, database);
|
|
678
883
|
},
|
|
@@ -693,7 +898,7 @@ export class UserGroundHost {
|
|
|
693
898
|
},
|
|
694
899
|
};
|
|
695
900
|
}
|
|
696
|
-
async uploadSnapshotContent(identity, resourceId, records) {
|
|
901
|
+
async uploadSnapshotContent(identity, resourceId, records, trace) {
|
|
697
902
|
const cache = this.cacheDir(identity);
|
|
698
903
|
const byHash = new Map();
|
|
699
904
|
for (const record of records) {
|
|
@@ -714,6 +919,27 @@ export class UserGroundHost {
|
|
|
714
919
|
bytes: manifest?.bytes ?? statSync(file).size,
|
|
715
920
|
};
|
|
716
921
|
});
|
|
922
|
+
const started = performance.now();
|
|
923
|
+
let uploadedChunks = 0;
|
|
924
|
+
let reusedChunks = 0;
|
|
925
|
+
let uploadedBytes = 0;
|
|
926
|
+
let inventoryRequests = 0;
|
|
927
|
+
const identifiers = {
|
|
928
|
+
...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
929
|
+
...(trace?.entityId ? { entityId: trace.entityId } : {}),
|
|
930
|
+
...(trace?.mutationId ? { mutationId: trace.mutationId } : {}),
|
|
931
|
+
};
|
|
932
|
+
this.stage({
|
|
933
|
+
primitive: "upload",
|
|
934
|
+
stage: "immutable-content",
|
|
935
|
+
status: "started",
|
|
936
|
+
...identifiers,
|
|
937
|
+
measurements: {
|
|
938
|
+
journey: trace?.journey ?? "bootstrap",
|
|
939
|
+
artifacts: sources.length,
|
|
940
|
+
declaredBytes: sources.reduce((total, source) => total + source.bytes, 0),
|
|
941
|
+
},
|
|
942
|
+
});
|
|
717
943
|
const small = sources.filter((source) => source.bytes <= CHUNK_BYTES);
|
|
718
944
|
const large = sources.filter((source) => source.bytes > CHUNK_BYTES);
|
|
719
945
|
const transfer = async (source) => {
|
|
@@ -735,6 +961,7 @@ export class UserGroundHost {
|
|
|
735
961
|
sha256: chunk.sha256,
|
|
736
962
|
bytes: chunk.bytes,
|
|
737
963
|
}, bytes, ["private.entity-content.stored"]));
|
|
964
|
+
uploadedBytes += bytes.byteLength;
|
|
738
965
|
};
|
|
739
966
|
try {
|
|
740
967
|
const receipt = await uploadArtifact(artifact, {
|
|
@@ -752,6 +979,7 @@ export class UserGroundHost {
|
|
|
752
979
|
}, {
|
|
753
980
|
sha256Hex,
|
|
754
981
|
missingChunks: async (_candidate, manifest) => {
|
|
982
|
+
inventoryRequests += 1;
|
|
755
983
|
const frame = await retryContent(() => this.wire.request({
|
|
756
984
|
type: "private.entity-content.inventory",
|
|
757
985
|
resource_id: resourceId,
|
|
@@ -794,6 +1022,7 @@ export class UserGroundHost {
|
|
|
794
1022
|
wire_bytes: encoded.bytes.byteLength,
|
|
795
1023
|
...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
|
|
796
1024
|
}, encoded.bytes, ["private.entity-content.stored-batch"]));
|
|
1025
|
+
uploadedBytes += encoded.bytes.byteLength;
|
|
797
1026
|
},
|
|
798
1027
|
putManifest: async (_candidate, manifest) => {
|
|
799
1028
|
if (manifestPresent)
|
|
@@ -810,8 +1039,11 @@ export class UserGroundHost {
|
|
|
810
1039
|
sha256: sha256Hex(bytes),
|
|
811
1040
|
bytes: bytes.length,
|
|
812
1041
|
}, bytes, ["private.entity-content.stored"]));
|
|
1042
|
+
uploadedBytes += bytes.byteLength;
|
|
813
1043
|
},
|
|
814
1044
|
});
|
|
1045
|
+
uploadedChunks += receipt.uploadedChunks;
|
|
1046
|
+
reusedChunks += receipt.reusedChunks;
|
|
815
1047
|
atomicWrite(manifestCacheFile(cache, artifact.contentHash), stableJson(receipt.manifest));
|
|
816
1048
|
}
|
|
817
1049
|
finally {
|
|
@@ -819,17 +1051,63 @@ export class UserGroundHost {
|
|
|
819
1051
|
await (await sourceFile.handle).close();
|
|
820
1052
|
}
|
|
821
1053
|
};
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1054
|
+
try {
|
|
1055
|
+
await boundedForEach(small, SMALL_CONTENT_UPLOAD_CONCURRENCY, transfer);
|
|
1056
|
+
for (const source of large) {
|
|
1057
|
+
await transfer(source);
|
|
1058
|
+
}
|
|
1059
|
+
this.stage({
|
|
1060
|
+
primitive: "upload",
|
|
1061
|
+
stage: "immutable-content",
|
|
1062
|
+
status: "completed",
|
|
1063
|
+
...identifiers,
|
|
1064
|
+
durationMs: performance.now() - started,
|
|
1065
|
+
measurements: {
|
|
1066
|
+
journey: trace?.journey ?? "bootstrap",
|
|
1067
|
+
artifacts: sources.length,
|
|
1068
|
+
declaredBytes: sources.reduce((total, source) => total + source.bytes, 0),
|
|
1069
|
+
uploadedBytes,
|
|
1070
|
+
uploadedChunks,
|
|
1071
|
+
reusedChunks,
|
|
1072
|
+
inventoryRequests,
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
this.stage({
|
|
1078
|
+
primitive: "upload",
|
|
1079
|
+
stage: "immutable-content",
|
|
1080
|
+
status: "failed",
|
|
1081
|
+
...identifiers,
|
|
1082
|
+
durationMs: performance.now() - started,
|
|
1083
|
+
measurements: { journey: trace?.journey ?? "bootstrap" },
|
|
1084
|
+
});
|
|
1085
|
+
throw error;
|
|
825
1086
|
}
|
|
826
1087
|
}
|
|
827
|
-
async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10) {
|
|
1088
|
+
async downloadContent(resourceId, cacheDir, artifact, retryAttempts = 10, trace) {
|
|
1089
|
+
const started = performance.now();
|
|
1090
|
+
let downloadedBytes = 0;
|
|
1091
|
+
let requests = 0;
|
|
1092
|
+
const identifiers = {
|
|
1093
|
+
...(trace?.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
1094
|
+
entityId: trace?.entityId ?? artifact.entityId,
|
|
1095
|
+
...(trace?.mutationId ? { mutationId: trace.mutationId } : {}),
|
|
1096
|
+
...(trace?.globalSequence ? { globalSequence: trace.globalSequence } : {}),
|
|
1097
|
+
};
|
|
1098
|
+
this.stage({
|
|
1099
|
+
primitive: "download",
|
|
1100
|
+
stage: "immutable-content",
|
|
1101
|
+
status: "started",
|
|
1102
|
+
...identifiers,
|
|
1103
|
+
measurements: { journey: trace?.journey ?? "bootstrap", kind: artifact.kind },
|
|
1104
|
+
});
|
|
828
1105
|
const cache = { target: null };
|
|
829
1106
|
const getChunk = async (manifest, index) => {
|
|
830
1107
|
const chunk = manifest.chunks[index];
|
|
831
1108
|
if (!chunk)
|
|
832
1109
|
throw new Error(`content manifest has no chunk ${index}`);
|
|
1110
|
+
requests += 1;
|
|
833
1111
|
const frame = await retryContent(() => this.wire.request({
|
|
834
1112
|
type: "private.entity-content.get",
|
|
835
1113
|
resource_id: resourceId,
|
|
@@ -839,12 +1117,15 @@ export class UserGroundHost {
|
|
|
839
1117
|
part_index: index,
|
|
840
1118
|
sha256: chunk.sha256,
|
|
841
1119
|
}, ["private.entity-content.data"]), true, retryAttempts);
|
|
842
|
-
|
|
1120
|
+
const bytes = binaryFrameBytes(frame);
|
|
1121
|
+
downloadedBytes += bytes.byteLength;
|
|
1122
|
+
return bytes;
|
|
843
1123
|
};
|
|
844
1124
|
try {
|
|
845
1125
|
const receipt = await downloadArtifact(artifact, {
|
|
846
1126
|
sha256Hex,
|
|
847
1127
|
getManifest: async () => {
|
|
1128
|
+
requests += 1;
|
|
848
1129
|
const frame = await retryContent(() => this.wire.request({
|
|
849
1130
|
type: "private.entity-content.get",
|
|
850
1131
|
resource_id: resourceId,
|
|
@@ -852,7 +1133,9 @@ export class UserGroundHost {
|
|
|
852
1133
|
kind: "manifest",
|
|
853
1134
|
part_index: 0,
|
|
854
1135
|
}, ["private.entity-content.data"]), false, retryAttempts);
|
|
855
|
-
const
|
|
1136
|
+
const manifestBytes = verifiedFrameBytes(frame);
|
|
1137
|
+
downloadedBytes += manifestBytes.byteLength;
|
|
1138
|
+
const candidate = JSON.parse(manifestBytes.toString("utf8"));
|
|
856
1139
|
const checked = checkContentManifest(candidate, artifact.contentHash);
|
|
857
1140
|
if (!checked.ok)
|
|
858
1141
|
throw new Error(checked.error);
|
|
@@ -883,7 +1166,10 @@ export class UserGroundHost {
|
|
|
883
1166
|
bytes: manifest.chunks[index]?.bytes,
|
|
884
1167
|
})),
|
|
885
1168
|
}, ["private.entity-content.data-batch"]), true, retryAttempts);
|
|
886
|
-
|
|
1169
|
+
requests += 1;
|
|
1170
|
+
const decoded = await decodeContentWireBytes(binaryWireFrameBytes(frame), frame.content_encoding, Number(frame.bytes));
|
|
1171
|
+
downloadedBytes += Number(frame.wire_bytes ?? frame.bytes);
|
|
1172
|
+
return decoded;
|
|
887
1173
|
},
|
|
888
1174
|
putChunk: async (_candidate, _manifest, index, bytes) => {
|
|
889
1175
|
if (!cache.target)
|
|
@@ -902,8 +1188,35 @@ export class UserGroundHost {
|
|
|
902
1188
|
return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
|
|
903
1189
|
},
|
|
904
1190
|
}, artifact.kind === "file" ? { concurrency: FILE_BATCH_DOWNLOAD_CONCURRENCY } : {});
|
|
1191
|
+
this.stage({
|
|
1192
|
+
primitive: "download",
|
|
1193
|
+
stage: "immutable-content",
|
|
1194
|
+
status: "completed",
|
|
1195
|
+
...identifiers,
|
|
1196
|
+
durationMs: performance.now() - started,
|
|
1197
|
+
measurements: {
|
|
1198
|
+
journey: trace?.journey ?? "bootstrap",
|
|
1199
|
+
kind: artifact.kind,
|
|
1200
|
+
declaredBytes: receipt.manifest.bytes,
|
|
1201
|
+
downloadedBytes,
|
|
1202
|
+
downloadedChunks: receipt.downloadedChunks,
|
|
1203
|
+
reusedChunks: receipt.reusedChunks,
|
|
1204
|
+
requests,
|
|
1205
|
+
},
|
|
1206
|
+
});
|
|
905
1207
|
return receipt.local;
|
|
906
1208
|
}
|
|
1209
|
+
catch (error) {
|
|
1210
|
+
this.stage({
|
|
1211
|
+
primitive: "download",
|
|
1212
|
+
stage: "immutable-content",
|
|
1213
|
+
status: "failed",
|
|
1214
|
+
...identifiers,
|
|
1215
|
+
durationMs: performance.now() - started,
|
|
1216
|
+
measurements: { journey: trace?.journey ?? "bootstrap", kind: artifact.kind, requests },
|
|
1217
|
+
});
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
907
1220
|
finally {
|
|
908
1221
|
await cache.target?.close();
|
|
909
1222
|
}
|
|
@@ -911,6 +1224,7 @@ export class UserGroundHost {
|
|
|
911
1224
|
ensureWatchers(identity, authoritativeBaselineRootIds = []) {
|
|
912
1225
|
if (this.closing)
|
|
913
1226
|
return this.watchHost.health();
|
|
1227
|
+
const started = performance.now();
|
|
914
1228
|
const root = this.userRoot(identity);
|
|
915
1229
|
const wanted = new Map([[root, {
|
|
916
1230
|
rootId: `user:${identity.userId}`,
|
|
@@ -935,6 +1249,18 @@ export class UserGroundHost {
|
|
|
935
1249
|
if (health.state !== "healthy" || health.contentHandles < 1) {
|
|
936
1250
|
throw new Error(`Watch coverage is degraded: ${health.reason ?? "no content coverage"}`);
|
|
937
1251
|
}
|
|
1252
|
+
this.stage({
|
|
1253
|
+
primitive: "watch",
|
|
1254
|
+
stage: "topology-reconcile",
|
|
1255
|
+
status: "completed",
|
|
1256
|
+
durationMs: performance.now() - started,
|
|
1257
|
+
measurements: {
|
|
1258
|
+
logicalRoots: health.logicalRoots,
|
|
1259
|
+
contentHandles: health.contentHandles,
|
|
1260
|
+
addressHandles: health.addressHandles,
|
|
1261
|
+
authoritativeBaselines: authoritativeBaselineRootIds.length,
|
|
1262
|
+
},
|
|
1263
|
+
});
|
|
938
1264
|
return health;
|
|
939
1265
|
}
|
|
940
1266
|
scheduleRescan(retryDelayMs) {
|
|
@@ -1030,7 +1356,15 @@ export class UserGroundHost {
|
|
|
1030
1356
|
}
|
|
1031
1357
|
}
|
|
1032
1358
|
async syncLocalChanges(identity) {
|
|
1359
|
+
const waitStarted = performance.now();
|
|
1360
|
+
this.stage({ primitive: "detect", stage: "ground-wait", status: "started" });
|
|
1033
1361
|
const release = await this.acquireGroundEffect();
|
|
1362
|
+
this.stage({
|
|
1363
|
+
primitive: "detect",
|
|
1364
|
+
stage: "ground-wait",
|
|
1365
|
+
status: "completed",
|
|
1366
|
+
durationMs: performance.now() - waitStarted,
|
|
1367
|
+
});
|
|
1034
1368
|
try {
|
|
1035
1369
|
await this.syncLocalChangesUnlocked(identity);
|
|
1036
1370
|
}
|
|
@@ -1042,6 +1376,17 @@ export class UserGroundHost {
|
|
|
1042
1376
|
if (!this.cloudState)
|
|
1043
1377
|
throw new Error("cloud state is unavailable for user-ground Watch");
|
|
1044
1378
|
const observations = this.watchHost.observations();
|
|
1379
|
+
const passStarted = performance.now();
|
|
1380
|
+
this.stage({
|
|
1381
|
+
primitive: "detect",
|
|
1382
|
+
stage: "pass",
|
|
1383
|
+
status: "started",
|
|
1384
|
+
measurements: {
|
|
1385
|
+
roots: observations.length,
|
|
1386
|
+
completeRoots: observations.filter(({ suspicion }) => suspicion.paths === null).length,
|
|
1387
|
+
namedPaths: observations.reduce((count, { suspicion }) => count + (suspicion.paths?.length ?? 0), 0),
|
|
1388
|
+
},
|
|
1389
|
+
});
|
|
1045
1390
|
let widenedRoots = new Set();
|
|
1046
1391
|
if (this.namedDetect) {
|
|
1047
1392
|
// The named lane is an optimization over the same settled evidence.
|
|
@@ -1052,7 +1397,7 @@ export class UserGroundHost {
|
|
|
1052
1397
|
if (named?.handled) {
|
|
1053
1398
|
for (const evidence of named.evidence) {
|
|
1054
1399
|
const observation = observations.find((candidate) => candidate.directory === evidence.directory);
|
|
1055
|
-
this.
|
|
1400
|
+
this.reportDetectScan({
|
|
1056
1401
|
rootId: observation?.rootId ?? evidence.directory,
|
|
1057
1402
|
directory: evidence.directory,
|
|
1058
1403
|
scope: "paths",
|
|
@@ -1070,6 +1415,17 @@ export class UserGroundHost {
|
|
|
1070
1415
|
this.watchHost.settle(observations);
|
|
1071
1416
|
this.applyRail?.localStateChanged();
|
|
1072
1417
|
this.recordRail?.localRecordsAvailable();
|
|
1418
|
+
this.stage({
|
|
1419
|
+
primitive: "detect",
|
|
1420
|
+
stage: "pass",
|
|
1421
|
+
status: "completed",
|
|
1422
|
+
durationMs: performance.now() - passStarted,
|
|
1423
|
+
measurements: {
|
|
1424
|
+
lane: "named",
|
|
1425
|
+
roots: named.evidence.length,
|
|
1426
|
+
records: named.evidence.reduce((count, evidence) => count + evidence.records, 0),
|
|
1427
|
+
},
|
|
1428
|
+
});
|
|
1073
1429
|
return;
|
|
1074
1430
|
}
|
|
1075
1431
|
widenedRoots = new Set(named?.widenedRoots ?? []);
|
|
@@ -1085,7 +1441,7 @@ export class UserGroundHost {
|
|
|
1085
1441
|
userRoot: this.userRoot(identity),
|
|
1086
1442
|
database: this.databasePath(identity),
|
|
1087
1443
|
cacheDir: this.cacheDir(identity),
|
|
1088
|
-
onScan: this.
|
|
1444
|
+
onScan: (evidence) => this.reportDetectScan(evidence),
|
|
1089
1445
|
suspicions: new Map(observations.map((observation) => [observation.directory, widenedRoots.has(observation.directory)
|
|
1090
1446
|
? { ...observation.suspicion, paths: null }
|
|
1091
1447
|
: observation.suspicion])),
|
|
@@ -1113,6 +1469,7 @@ export class UserGroundHost {
|
|
|
1113
1469
|
proposal,
|
|
1114
1470
|
}))
|
|
1115
1471
|
: []);
|
|
1472
|
+
const recordCommitStarted = performance.now();
|
|
1116
1473
|
commitDetectedState(this.databasePath(identity), {
|
|
1117
1474
|
acceptedMaterializedRows,
|
|
1118
1475
|
materializedRemovals,
|
|
@@ -1120,10 +1477,40 @@ export class UserGroundHost {
|
|
|
1120
1477
|
notebookRemovals,
|
|
1121
1478
|
proposals: detectedRecords,
|
|
1122
1479
|
});
|
|
1480
|
+
const recordCommitDuration = performance.now() - recordCommitStarted;
|
|
1481
|
+
if (detectedRecords.length === 0) {
|
|
1482
|
+
this.stage({
|
|
1483
|
+
primitive: "record",
|
|
1484
|
+
stage: "sqlite-transaction",
|
|
1485
|
+
status: "completed",
|
|
1486
|
+
durationMs: recordCommitDuration,
|
|
1487
|
+
measurements: { records: 0 },
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
for (const record of detectedRecords) {
|
|
1492
|
+
this.stage({
|
|
1493
|
+
primitive: "record",
|
|
1494
|
+
stage: "outbox-committed",
|
|
1495
|
+
status: "completed",
|
|
1496
|
+
entityId: record.proposal.entityId,
|
|
1497
|
+
mutationId: record.mutationId,
|
|
1498
|
+
durationMs: recordCommitDuration,
|
|
1499
|
+
measurements: { records: detectedRecords.length },
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1123
1503
|
this.namedDetect?.refreshEnrollmentPolicy();
|
|
1124
1504
|
this.watchHost.settle(observations);
|
|
1125
1505
|
this.applyRail?.localStateChanged();
|
|
1126
1506
|
this.recordRail?.localRecordsAvailable();
|
|
1507
|
+
this.stage({
|
|
1508
|
+
primitive: "detect",
|
|
1509
|
+
stage: "pass",
|
|
1510
|
+
status: "completed",
|
|
1511
|
+
durationMs: performance.now() - passStarted,
|
|
1512
|
+
measurements: { lane: "reconciliation", records: detectedRecords.length },
|
|
1513
|
+
});
|
|
1127
1514
|
}
|
|
1128
1515
|
async publishPendingSnapshotsBeforeLookup(identity, resourceId) {
|
|
1129
1516
|
const pending = readSnapshotPublications(this.databasePath(identity));
|
|
@@ -1145,7 +1532,7 @@ export class UserGroundHost {
|
|
|
1145
1532
|
/** Register is the deliberate cold graph-publication boundary. Ordinary
|
|
1146
1533
|
* Watch detection stops at a durable exact record and never rebuilds this
|
|
1147
1534
|
* resource snapshot. */
|
|
1148
|
-
async publishMaterializedSnapshot(identity, publicationRootIds) {
|
|
1535
|
+
async publishMaterializedSnapshot(identity, publicationRootIds, trace) {
|
|
1149
1536
|
const state = this.cloudState;
|
|
1150
1537
|
if (!state)
|
|
1151
1538
|
throw new Error("cloud state is unavailable for graph publication");
|
|
@@ -1169,7 +1556,7 @@ export class UserGroundHost {
|
|
|
1169
1556
|
const identical = pending.find((entry) => entry.resourceId === state.resourceId
|
|
1170
1557
|
&& entry.snapshotChecksum === checksum);
|
|
1171
1558
|
if (identical) {
|
|
1172
|
-
await this.publishPendingSnapshots(identity);
|
|
1559
|
+
await this.publishPendingSnapshots(identity, trace);
|
|
1173
1560
|
return;
|
|
1174
1561
|
}
|
|
1175
1562
|
if (pending.length > 0) {
|
|
@@ -1207,19 +1594,29 @@ export class UserGroundHost {
|
|
|
1207
1594
|
replacementRootIds: replacement.rootIds,
|
|
1208
1595
|
replacementRecords: replacement.records,
|
|
1209
1596
|
});
|
|
1210
|
-
await this.publishPendingSnapshots(identity);
|
|
1597
|
+
await this.publishPendingSnapshots(identity, trace);
|
|
1211
1598
|
}
|
|
1212
|
-
async publishPendingSnapshots(identity) {
|
|
1599
|
+
async publishPendingSnapshots(identity, trace) {
|
|
1213
1600
|
for (const pending of readSnapshotPublications(this.databasePath(identity))) {
|
|
1214
1601
|
const snapshot = snapshotFromRecords(JSON.parse(pending.snapshotJson).records || []);
|
|
1215
1602
|
const replacement = rootReplacementFromRecords(pending.replacementRootIds, pending.replacementRecords);
|
|
1216
1603
|
try {
|
|
1217
|
-
await this.uploadSnapshotContent(identity, pending.resourceId, pending.uploadRecords);
|
|
1604
|
+
await this.uploadSnapshotContent(identity, pending.resourceId, pending.uploadRecords, trace);
|
|
1218
1605
|
}
|
|
1219
1606
|
catch (error) {
|
|
1220
1607
|
throw pipelineStageError("cloud upload", error);
|
|
1221
1608
|
}
|
|
1222
1609
|
let frame;
|
|
1610
|
+
const graphCommitStarted = performance.now();
|
|
1611
|
+
if (trace?.journey === "register") {
|
|
1612
|
+
this.stage({
|
|
1613
|
+
primitive: "register",
|
|
1614
|
+
stage: "cloud-graph-commit",
|
|
1615
|
+
status: "started",
|
|
1616
|
+
...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
1617
|
+
...(trace.mutationId ? { mutationId: trace.mutationId } : {}),
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1223
1620
|
try {
|
|
1224
1621
|
frame = await this.wire.request({
|
|
1225
1622
|
type: "shared.mutation.submit",
|
|
@@ -1237,8 +1634,27 @@ export class UserGroundHost {
|
|
|
1237
1634
|
}, ["shared.mutation.ack"]);
|
|
1238
1635
|
}
|
|
1239
1636
|
catch (error) {
|
|
1637
|
+
if (trace?.journey === "register") {
|
|
1638
|
+
this.stage({
|
|
1639
|
+
primitive: "register",
|
|
1640
|
+
stage: "cloud-graph-commit",
|
|
1641
|
+
status: "failed",
|
|
1642
|
+
...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
1643
|
+
durationMs: performance.now() - graphCommitStarted,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1240
1646
|
throw pipelineStageError("cloud graph commit", error);
|
|
1241
1647
|
}
|
|
1648
|
+
if (trace?.journey === "register") {
|
|
1649
|
+
this.stage({
|
|
1650
|
+
primitive: "register",
|
|
1651
|
+
stage: "cloud-graph-commit",
|
|
1652
|
+
status: "completed",
|
|
1653
|
+
...(trace.workspaceId ? { workspaceId: trace.workspaceId } : {}),
|
|
1654
|
+
durationMs: performance.now() - graphCommitStarted,
|
|
1655
|
+
measurements: { version: Number(frame.version) },
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1242
1658
|
this.cloudState = {
|
|
1243
1659
|
resourceId: pending.resourceId,
|
|
1244
1660
|
authorityEpoch: Number(frame.authority_epoch),
|