@amalgm/shell 0.1.104 → 0.1.106
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 +14 -0
- package/dist/entity-apply-store.js +60 -4
- package/dist/entity-apply-store.js.map +1 -1
- package/dist/entity-record-store.js +15 -1
- package/dist/entity-record-store.js.map +1 -1
- package/dist/snapshot-delivery-baseline.d.ts +12 -0
- package/dist/snapshot-delivery-baseline.js +58 -0
- package/dist/snapshot-delivery-baseline.js.map +1 -0
- package/dist/user-ground-host.d.ts +5 -0
- package/dist/user-ground-host.js +155 -26
- package/dist/user-ground-host.js.map +1 -1
- package/dist/wire-client.js +1 -1
- 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 } 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, 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";
|
|
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, 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";
|
|
@@ -25,14 +25,14 @@ import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
|
25
25
|
import { NodeWatchHost, } from "./watching/index.js";
|
|
26
26
|
import { GroundCoordinator } from "./ground-coordination.js";
|
|
27
27
|
import { submitMutationOperation } from "./paged-mutation-submit.js";
|
|
28
|
+
import { adoptSnapshotDeliveryBaseline } from "./snapshot-delivery-baseline.js";
|
|
28
29
|
import { WireClient, WireRequestError } from "./wire-client.js";
|
|
29
30
|
const SMALL_CONTENT_UPLOAD_CONCURRENCY = 16;
|
|
30
|
-
/** Storage-object lanes for one download set.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* direct fetches cost the gateway nothing, and memory stays bounded by
|
|
34
|
-
* lanes × one block. */
|
|
31
|
+
/** Storage-object lanes for one download set. Tiny objects use the wide
|
|
32
|
+
* request window, while the separate byte budget narrows large chunks so
|
|
33
|
+
* request concurrency can never silently become an unbounded memory claim. */
|
|
35
34
|
const DIRECT_OBJECT_DOWNLOAD_CONCURRENCY = 128;
|
|
35
|
+
const DIRECT_OBJECT_DOWNLOAD_MAX_IN_FLIGHT_BYTES = 8 * CHUNK_BYTES;
|
|
36
36
|
const DETECT_QUIET_MS = 12;
|
|
37
37
|
const DETECT_MAX_DEFERRAL_MS = 75;
|
|
38
38
|
const DETECT_RETRY_MS = 250;
|
|
@@ -260,13 +260,31 @@ export class UserGroundHost {
|
|
|
260
260
|
const cloud = await this.cloudPort(identity).lookup(resourceId);
|
|
261
261
|
if (!cloud)
|
|
262
262
|
throw new Error("this user has no cloud entity registry");
|
|
263
|
+
// The authority's active snapshot is the compact base; Add's active
|
|
264
|
+
// materialization head is that snapshot plus the complete committed
|
|
265
|
+
// tail this machine has now received. Fold the tail before Live
|
|
266
|
+
// selects the workspace so a destination created after mutations is
|
|
267
|
+
// exact immediately, rather than first installing stale ground and
|
|
268
|
+
// racing Apply afterward.
|
|
269
|
+
await this.recordRail?.flush();
|
|
270
|
+
const deliveryWatermark = readAuthorityDeliveryCursor(this.databasePath(identity), resourceId);
|
|
271
|
+
if (deliveryWatermark < cloud.deliveryWatermark) {
|
|
272
|
+
throw new Error("local delivery cursor is behind the active snapshot watermark");
|
|
273
|
+
}
|
|
274
|
+
const snapshot = snapshotFromRecords(recordsAtDeliveryPrefix(cloud.snapshot.records, readAcceptedResults(this.databasePath(identity), resourceId, cloud.deliveryWatermark, deliveryWatermark)));
|
|
275
|
+
const current = {
|
|
276
|
+
...cloud,
|
|
277
|
+
deliveryWatermark,
|
|
278
|
+
checksum: sha256Hex(stableJson(snapshot)),
|
|
279
|
+
snapshot,
|
|
280
|
+
};
|
|
263
281
|
this.stage({
|
|
264
282
|
primitive: "download",
|
|
265
283
|
stage: "registry-lookup",
|
|
266
284
|
status: "completed",
|
|
267
285
|
durationMs: performance.now() - started,
|
|
268
286
|
});
|
|
269
|
-
return
|
|
287
|
+
return current;
|
|
270
288
|
},
|
|
271
289
|
materializeWorkspace: async (input) => {
|
|
272
290
|
const groundWaitStarted = performance.now();
|
|
@@ -299,9 +317,10 @@ export class UserGroundHost {
|
|
|
299
317
|
records: input.records,
|
|
300
318
|
destinationParent: input.destinationParent,
|
|
301
319
|
cloudHead: input.cloudHead,
|
|
320
|
+
deliveryWatermark: input.deliveryWatermark,
|
|
302
321
|
readContent: (artifacts) => this.downloadContentSet(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: input.workspace.uuid }),
|
|
303
322
|
adoptMaterialization: (rootUUID) => {
|
|
304
|
-
const settled = this.
|
|
323
|
+
const settled = this.adoptCloudDeliveryBaseline(identity, input.deliveryWatermark, rootUUID);
|
|
305
324
|
this.applyRail?.recordsAvailable();
|
|
306
325
|
if (settled > 0) {
|
|
307
326
|
this.stage({
|
|
@@ -425,9 +444,10 @@ export class UserGroundHost {
|
|
|
425
444
|
records: intent.records,
|
|
426
445
|
destinationParent: dirname(intent.destinationPath),
|
|
427
446
|
cloudHead: intent.cloudHead,
|
|
447
|
+
deliveryWatermark: intent.deliveryWatermark,
|
|
428
448
|
readContent: (artifacts) => this.downloadContentSet(intent.resourceId, this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: intent.workspaceId }),
|
|
429
449
|
adoptMaterialization: (rootUUID) => {
|
|
430
|
-
this.
|
|
450
|
+
this.adoptCloudDeliveryBaseline(identity, intent.deliveryWatermark, rootUUID);
|
|
431
451
|
this.applyRail?.recordsAvailable();
|
|
432
452
|
},
|
|
433
453
|
onStage: (stage) => this.options.onWorkspaceAddStage?.({
|
|
@@ -505,7 +525,8 @@ export class UserGroundHost {
|
|
|
505
525
|
// the publish path re-proves seal evidence and repairs any missing
|
|
506
526
|
// uploads before this answer is lawful.
|
|
507
527
|
try {
|
|
508
|
-
await this.
|
|
528
|
+
const deliveryWatermark = await this.prepareSnapshotPublication(identity, { journey: "register", workspaceId });
|
|
529
|
+
await this.publishMaterializedSnapshot(identity, new Set([workspaceId]), deliveryWatermark, { journey: "register", workspaceId });
|
|
509
530
|
}
|
|
510
531
|
finally {
|
|
511
532
|
this.finishColdOperation();
|
|
@@ -520,6 +541,10 @@ export class UserGroundHost {
|
|
|
520
541
|
// Register is a cold declaration, not a filesystem edit. It performs one
|
|
521
542
|
// complete reconciliation and publishes one graph head; it must not mint
|
|
522
543
|
// a parallel stream of Detect records for the same initial contents.
|
|
544
|
+
// Freeze the official prefix before taking ground ownership. Apply may
|
|
545
|
+
// need that same ground, so draining it inside the registration scope
|
|
546
|
+
// would make the two rails wait on one another.
|
|
547
|
+
const deliveryWatermark = await this.prepareSnapshotPublication(identity, { journey: "register", workspaceId });
|
|
523
548
|
let releaseGround = null;
|
|
524
549
|
try {
|
|
525
550
|
const groundWaitStarted = performance.now();
|
|
@@ -613,7 +638,7 @@ export class UserGroundHost {
|
|
|
613
638
|
if (!scanned.publicationRootIds) {
|
|
614
639
|
throw new Error("cold registration reconciliation did not name its publication roots");
|
|
615
640
|
}
|
|
616
|
-
await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds), { journey: "register", workspaceId });
|
|
641
|
+
await this.publishMaterializedSnapshot(identity, new Set(scanned.publicationRootIds), deliveryWatermark, { journey: "register", workspaceId });
|
|
617
642
|
this.watchHost.settle(observations);
|
|
618
643
|
}
|
|
619
644
|
catch (error) {
|
|
@@ -711,6 +736,20 @@ export class UserGroundHost {
|
|
|
711
736
|
if (this.syncing)
|
|
712
737
|
await this.syncing;
|
|
713
738
|
}
|
|
739
|
+
/** Install the exact snapshot/tail boundary before Receive or Apply can
|
|
740
|
+
* treat older official history as materialization work. The SQLite cursor,
|
|
741
|
+
* retained Inbox resolution, and Apply baseline all derive from the same N. */
|
|
742
|
+
adoptCloudDeliveryBaseline(identity, deliveryWatermark, rootUUID = null) {
|
|
743
|
+
const database = initializeDatabase(this.databasePath(identity));
|
|
744
|
+
let resolved = 0;
|
|
745
|
+
try {
|
|
746
|
+
resolved = adoptSnapshotDeliveryBaseline(database, privateEntityResourceId(identity.userId, sha256Hex), deliveryWatermark, this.options.now?.().getTime() ?? Date.now());
|
|
747
|
+
}
|
|
748
|
+
finally {
|
|
749
|
+
database.close();
|
|
750
|
+
}
|
|
751
|
+
return resolved + (this.applyHost?.adoptVerifiedMaterialization(rootUUID) ?? 0);
|
|
752
|
+
}
|
|
714
753
|
async startRecordRail(identity) {
|
|
715
754
|
if (!this.cloudState)
|
|
716
755
|
throw new Error("entity Record rail requires cloud authority");
|
|
@@ -718,12 +757,13 @@ export class UserGroundHost {
|
|
|
718
757
|
throw new Error("entity Record rail already started");
|
|
719
758
|
await this.wire.open();
|
|
720
759
|
// One floor, no per-feature gates: this shell and its gateway release
|
|
721
|
-
// together, and the gateway ships first.
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
if (this.wire.protocolVersion <
|
|
760
|
+
// together, and the gateway ships first. 11 = snapshot delivery
|
|
761
|
+
// watermarks; an older gateway cannot atomically bind Add's graph to the
|
|
762
|
+
// official entity-record prefix it already includes.
|
|
763
|
+
if (this.wire.protocolVersion < 11) {
|
|
725
764
|
throw new Error("gateway speaks an older wire protocol than this shell");
|
|
726
765
|
}
|
|
766
|
+
this.adoptCloudDeliveryBaseline(identity, this.cloudState.deliveryWatermark);
|
|
727
767
|
const store = new EntityRecordSqliteStore(initializeDatabase(this.databasePath(identity)));
|
|
728
768
|
const wireAuthority = createEntityRecordAuthorityPort({
|
|
729
769
|
request: (frame, acceptedTypes, binaryBody) => binaryBody === undefined
|
|
@@ -1362,7 +1402,10 @@ export class UserGroundHost {
|
|
|
1362
1402
|
const sealed = await target(artifact).seal();
|
|
1363
1403
|
return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
|
|
1364
1404
|
},
|
|
1365
|
-
}, {
|
|
1405
|
+
}, {
|
|
1406
|
+
concurrency: DIRECT_OBJECT_DOWNLOAD_CONCURRENCY,
|
|
1407
|
+
maxInFlightBytes: DIRECT_OBJECT_DOWNLOAD_MAX_IN_FLIGHT_BYTES,
|
|
1408
|
+
});
|
|
1366
1409
|
const content = new Map();
|
|
1367
1410
|
for (const receipt of receipts) {
|
|
1368
1411
|
declaredBytes += receipt.manifest.bytes;
|
|
@@ -1758,6 +1801,7 @@ export class UserGroundHost {
|
|
|
1758
1801
|
authorityEpoch: first.authorityEpoch,
|
|
1759
1802
|
headVersion: first.baseVersion,
|
|
1760
1803
|
checksum: "",
|
|
1804
|
+
deliveryWatermark: first.deliveryWatermark,
|
|
1761
1805
|
records: snapshotFromRecords(JSON.parse(first.snapshotJson).records || []).records,
|
|
1762
1806
|
};
|
|
1763
1807
|
try {
|
|
@@ -1776,13 +1820,10 @@ export class UserGroundHost {
|
|
|
1776
1820
|
/** Register is the deliberate cold graph-publication boundary. Ordinary
|
|
1777
1821
|
* Watch detection stops at a durable exact record and never rebuilds this
|
|
1778
1822
|
* resource snapshot. */
|
|
1779
|
-
async
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1782
|
-
//
|
|
1783
|
-
// and a conclusive refusal marks the row rejected so the snapshot
|
|
1784
|
-
// derived below may supersede it. A transport failure keeps the row
|
|
1785
|
-
// pending — its outcome is unknown, so nothing may replace it.
|
|
1823
|
+
async prepareSnapshotPublication(identity, trace) {
|
|
1824
|
+
// An older durable publication resolves before the new boundary derives.
|
|
1825
|
+
// Unknown outcomes remain pending; a conclusive refusal may be replaced
|
|
1826
|
+
// by the newer complete snapshot below.
|
|
1786
1827
|
try {
|
|
1787
1828
|
await this.publishPendingSnapshots(identity, trace);
|
|
1788
1829
|
}
|
|
@@ -1790,13 +1831,30 @@ export class UserGroundHost {
|
|
|
1790
1831
|
if (!conclusivelyRefused(error))
|
|
1791
1832
|
throw error;
|
|
1792
1833
|
}
|
|
1834
|
+
await this.recordRail?.flush();
|
|
1835
|
+
const state = this.cloudState;
|
|
1836
|
+
if (!state)
|
|
1837
|
+
throw new Error("cloud state is unavailable for graph publication");
|
|
1838
|
+
// Freeze N immediately after Receive reaches its explicit boundary. Apply
|
|
1839
|
+
// then settles every Inbox row already known at that boundary. A later
|
|
1840
|
+
// delivery may run concurrently, but the graph still declares the older
|
|
1841
|
+
// N and the authority refuses it if that newer record commits first.
|
|
1842
|
+
const deliveryWatermark = readAuthorityDeliveryCursor(this.databasePath(identity), state.resourceId);
|
|
1843
|
+
if (deliveryWatermark < state.deliveryWatermark) {
|
|
1844
|
+
throw new Error("local delivery cursor is behind the active snapshot watermark");
|
|
1845
|
+
}
|
|
1846
|
+
await this.applyRail?.flush();
|
|
1847
|
+
return deliveryWatermark;
|
|
1848
|
+
}
|
|
1849
|
+
async publishMaterializedSnapshot(identity, publicationRootIds, deliveryWatermark, trace) {
|
|
1793
1850
|
const state = this.cloudState;
|
|
1794
1851
|
if (!state)
|
|
1795
1852
|
throw new Error("cloud state is unavailable for graph publication");
|
|
1853
|
+
const officialRecords = recordsAtDeliveryPrefix(state.records, readAcceptedResults(this.databasePath(identity), state.resourceId, state.deliveryWatermark, deliveryWatermark));
|
|
1796
1854
|
const localRecords = recordsRetainingMissingEvidence(readGroundRowsForRoots(this.databasePath(identity), "entities", publicationRootIds)
|
|
1797
1855
|
.map(portableRecord), readGroundRowsForRoots(this.databasePath(identity), "detection_notebook", publicationRootIds)
|
|
1798
1856
|
.map(portableRecord));
|
|
1799
|
-
const merged = mergeMaterializedRoots(
|
|
1857
|
+
const merged = mergeMaterializedRoots(officialRecords, localRecords);
|
|
1800
1858
|
const traveling = travelingRecords(merged, sha256Hex);
|
|
1801
1859
|
const impossibleGitLeaf = traveling.find((record) => (record.type === "file.text" || record.type === "file.binary" || record.type === "link")
|
|
1802
1860
|
&& record.payloadVersion?.startsWith("git:"));
|
|
@@ -1824,7 +1882,7 @@ export class UserGroundHost {
|
|
|
1824
1882
|
const uploadRecords = cargo
|
|
1825
1883
|
.filter((entry) => !sealedArtifacts.has(entry.contentHash))
|
|
1826
1884
|
.map((entry) => entry.record);
|
|
1827
|
-
if (checksum === state.checksum) {
|
|
1885
|
+
if (checksum === state.checksum && deliveryWatermark === state.deliveryWatermark) {
|
|
1828
1886
|
// The graph is already committed; only cargo can be missing. Repair
|
|
1829
1887
|
// uploads need no journal row — a crash simply re-runs the same
|
|
1830
1888
|
// repair on the next Register.
|
|
@@ -1855,6 +1913,7 @@ export class UserGroundHost {
|
|
|
1855
1913
|
baseVersion: state.headVersion,
|
|
1856
1914
|
snapshotChecksum: checksum,
|
|
1857
1915
|
snapshotJson,
|
|
1916
|
+
deliveryWatermark,
|
|
1858
1917
|
uploadRecords,
|
|
1859
1918
|
replacementRootIds: replacement.rootIds,
|
|
1860
1919
|
replacementRecords: replacement.records,
|
|
@@ -1921,6 +1980,7 @@ export class UserGroundHost {
|
|
|
1921
1980
|
contract: ENTITY_CLOUD_CONTRACT,
|
|
1922
1981
|
schemaVersion: ENTITY_CLOUD_SCHEMA_VERSION,
|
|
1923
1982
|
operationKind: replacement.kind,
|
|
1983
|
+
deliveryWatermark: pending.deliveryWatermark,
|
|
1924
1984
|
}, replacement);
|
|
1925
1985
|
}
|
|
1926
1986
|
catch (error) {
|
|
@@ -1957,6 +2017,7 @@ export class UserGroundHost {
|
|
|
1957
2017
|
authorityEpoch: Number(frame.authority_epoch),
|
|
1958
2018
|
headVersion: Number(frame.version),
|
|
1959
2019
|
checksum: pending.snapshotChecksum,
|
|
2020
|
+
deliveryWatermark: Number(frame.delivery_watermark),
|
|
1960
2021
|
records: snapshot.records,
|
|
1961
2022
|
};
|
|
1962
2023
|
deleteJournalEntry(this.databasePath(identity), pending.mutationId);
|
|
@@ -2145,6 +2206,10 @@ function initializeDatabase(file) {
|
|
|
2145
2206
|
authority_id TEXT PRIMARY KEY,
|
|
2146
2207
|
received_through INTEGER NOT NULL CHECK(received_through >= 0)
|
|
2147
2208
|
);
|
|
2209
|
+
CREATE TABLE IF NOT EXISTS authority_snapshot_baselines (
|
|
2210
|
+
authority_id TEXT PRIMARY KEY,
|
|
2211
|
+
delivery_sequence INTEGER NOT NULL CHECK(delivery_sequence >= 0)
|
|
2212
|
+
);
|
|
2148
2213
|
CREATE TABLE IF NOT EXISTS snapshot_publications (
|
|
2149
2214
|
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2150
2215
|
mutation_id TEXT NOT NULL UNIQUE,
|
|
@@ -2454,9 +2519,13 @@ function parseWorkspaceAddIntent(json) {
|
|
|
2454
2519
|
const cloudHead = String(value.cloudHead || "");
|
|
2455
2520
|
const destinationPath = String(value.destinationPath || "");
|
|
2456
2521
|
const stagingPath = String(value.stagingPath || "");
|
|
2522
|
+
const deliveryWatermark = Number(value.deliveryWatermark);
|
|
2457
2523
|
if (!UUID.test(workspaceId) || !resourceId || !/^[0-9a-f]{64}$/i.test(cloudHead)) {
|
|
2458
2524
|
throw new Error("workspace Add intent identity is invalid");
|
|
2459
2525
|
}
|
|
2526
|
+
if (!Number.isSafeInteger(deliveryWatermark) || deliveryWatermark < 0) {
|
|
2527
|
+
throw new Error(`workspace Add intent ${workspaceId} has no delivery watermark`);
|
|
2528
|
+
}
|
|
2460
2529
|
if (!isAbsolute(destinationPath)
|
|
2461
2530
|
|| stagingPath !== workspaceAddStagingPath(destinationPath, workspaceId)) {
|
|
2462
2531
|
throw new Error(`workspace Add intent ${workspaceId} has invalid placement`);
|
|
@@ -2465,6 +2534,7 @@ function parseWorkspaceAddIntent(json) {
|
|
|
2465
2534
|
workspaceId,
|
|
2466
2535
|
resourceId,
|
|
2467
2536
|
cloudHead: cloudHead.toLowerCase(),
|
|
2537
|
+
deliveryWatermark,
|
|
2468
2538
|
destinationPath,
|
|
2469
2539
|
stagingPath,
|
|
2470
2540
|
records: snapshotFromRecords(Array.isArray(value.records) ? value.records : []).records,
|
|
@@ -2576,11 +2646,16 @@ function readSnapshotPublications(file) {
|
|
|
2576
2646
|
|| !Array.isArray(publication.replacementRecords)) {
|
|
2577
2647
|
throw new Error(`snapshot publication ${row.mutationId} has no exact root replacement`);
|
|
2578
2648
|
}
|
|
2649
|
+
const deliveryWatermark = Number(publication.deliveryWatermark);
|
|
2650
|
+
if (!Number.isSafeInteger(deliveryWatermark) || deliveryWatermark < 0) {
|
|
2651
|
+
throw new Error(`snapshot publication ${row.mutationId} has no delivery watermark`);
|
|
2652
|
+
}
|
|
2579
2653
|
return {
|
|
2580
2654
|
mutationId: row.mutationId,
|
|
2581
2655
|
resourceId: row.resourceId,
|
|
2582
2656
|
authorityEpoch: Number(publication.authorityEpoch),
|
|
2583
2657
|
baseVersion: Number(publication.baseVersion),
|
|
2658
|
+
deliveryWatermark,
|
|
2584
2659
|
snapshotChecksum: String(publication.snapshotChecksum),
|
|
2585
2660
|
snapshotJson: String(publication.snapshotJson),
|
|
2586
2661
|
uploadRecords: snapshotFromRecords(publication.uploadRecords).records,
|
|
@@ -2607,6 +2682,55 @@ function hasRecordsBeyondSnapshot(file) {
|
|
|
2607
2682
|
database.close();
|
|
2608
2683
|
}
|
|
2609
2684
|
}
|
|
2685
|
+
function readAuthorityDeliveryCursor(file, authorityId) {
|
|
2686
|
+
const database = initializeDatabase(file);
|
|
2687
|
+
try {
|
|
2688
|
+
const row = database.prepare(`
|
|
2689
|
+
SELECT received_through AS receivedThrough
|
|
2690
|
+
FROM authority_delivery_cursors WHERE authority_id = ?
|
|
2691
|
+
`).get(authorityId);
|
|
2692
|
+
return row?.receivedThrough ?? 0;
|
|
2693
|
+
}
|
|
2694
|
+
finally {
|
|
2695
|
+
database.close();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
/** Latest official result for every UUID in the newly received complete
|
|
2699
|
+
* authority prefix. The record carries the whole logical result, so roots
|
|
2700
|
+
* this machine has not materialized still advance honestly in a Register
|
|
2701
|
+
* snapshot without inventing filesystem evidence. */
|
|
2702
|
+
function readAcceptedResults(file, authorityId, afterDelivery, throughDelivery) {
|
|
2703
|
+
if (throughDelivery <= afterDelivery)
|
|
2704
|
+
return [];
|
|
2705
|
+
const database = initializeDatabase(file);
|
|
2706
|
+
try {
|
|
2707
|
+
const latest = new Map();
|
|
2708
|
+
for (const { recordJson } of database.prepare(`
|
|
2709
|
+
SELECT record_json AS recordJson FROM record_inbox
|
|
2710
|
+
WHERE authority_id = ?
|
|
2711
|
+
AND delivery_sequence > ? AND delivery_sequence <= ?
|
|
2712
|
+
ORDER BY delivery_sequence
|
|
2713
|
+
`).iterate(authorityId, afterDelivery, throughDelivery)) {
|
|
2714
|
+
const accepted = decodeAcceptedEntityRecord(recordJson);
|
|
2715
|
+
latest.set(accepted.entityId, accepted.change.result);
|
|
2716
|
+
}
|
|
2717
|
+
return [...latest.values()];
|
|
2718
|
+
}
|
|
2719
|
+
finally {
|
|
2720
|
+
database.close();
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
function recordsAtDeliveryPrefix(base, results) {
|
|
2724
|
+
if (results.length === 0)
|
|
2725
|
+
return base;
|
|
2726
|
+
const records = new Map(base.map((record) => [record.uuid, record]));
|
|
2727
|
+
for (const result of results)
|
|
2728
|
+
records.set(result.uuid, result);
|
|
2729
|
+
// Entity channels are independently ordered. Container membership is the
|
|
2730
|
+
// one derived relationship, so derive it once from the final prefix rather
|
|
2731
|
+
// than letting record arrival order choose a container version.
|
|
2732
|
+
return recordsRetainingMissingEvidence([...records.values()], []);
|
|
2733
|
+
}
|
|
2610
2734
|
/** Record is the SQLite transaction that accepts Detect's exact proposals and
|
|
2611
2735
|
* advances the last-verified notebook. Watch may settle only after this
|
|
2612
2736
|
* function returns. The record table is the local-only outbox. */
|
|
@@ -2709,6 +2833,7 @@ function supersedeSnapshotJournal(file, entry) {
|
|
|
2709
2833
|
`).run(entry.mutationId, entry.resourceId, stableJson({
|
|
2710
2834
|
authorityEpoch: entry.authorityEpoch,
|
|
2711
2835
|
baseVersion: entry.baseVersion,
|
|
2836
|
+
deliveryWatermark: entry.deliveryWatermark,
|
|
2712
2837
|
snapshotChecksum: entry.snapshotChecksum,
|
|
2713
2838
|
snapshotJson: entry.snapshotJson,
|
|
2714
2839
|
uploadRecords: entry.uploadRecords,
|
|
@@ -4598,7 +4723,7 @@ function installCloudWorkspaceReferences(input) {
|
|
|
4598
4723
|
return references;
|
|
4599
4724
|
}
|
|
4600
4725
|
async function installCloudWorkspace(input) {
|
|
4601
|
-
const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, cloudHead, readContent, onStage, } = input;
|
|
4726
|
+
const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, cloudHead, deliveryWatermark, readContent, onStage, } = input;
|
|
4602
4727
|
const allLocalRows = readRows(database);
|
|
4603
4728
|
const existingBoundary = allLocalRows.find((row) => row.uuid === workspace.uuid);
|
|
4604
4729
|
const existing = existingBoundary
|
|
@@ -4652,6 +4777,7 @@ async function installCloudWorkspace(input) {
|
|
|
4652
4777
|
workspace: activeSelection.workspace,
|
|
4653
4778
|
references: activeReferences,
|
|
4654
4779
|
});
|
|
4780
|
+
input.adoptMaterialization(workspace.uuid);
|
|
4655
4781
|
return {
|
|
4656
4782
|
path: root.absolutePath,
|
|
4657
4783
|
rows: existing.length,
|
|
@@ -4705,6 +4831,7 @@ async function installCloudWorkspace(input) {
|
|
|
4705
4831
|
workspaceId: workspace.uuid,
|
|
4706
4832
|
resourceId,
|
|
4707
4833
|
cloudHead,
|
|
4834
|
+
deliveryWatermark,
|
|
4708
4835
|
destinationPath: destination,
|
|
4709
4836
|
stagingPath,
|
|
4710
4837
|
records: snapshotFromRecords(records).records,
|
|
@@ -4783,6 +4910,7 @@ function readyFromFrame(frame) {
|
|
|
4783
4910
|
const cloud = {
|
|
4784
4911
|
resourceId: String(frame.resource_id || ""),
|
|
4785
4912
|
version: Number(frame.head_version),
|
|
4913
|
+
deliveryWatermark: Number(frame.delivery_watermark),
|
|
4786
4914
|
checksum: String(frame.snapshot_checksum || ""),
|
|
4787
4915
|
snapshot: parseSnapshot(bytes),
|
|
4788
4916
|
};
|
|
@@ -4793,6 +4921,7 @@ function readyFromFrame(frame) {
|
|
|
4793
4921
|
authorityEpoch: Number(frame.authority_epoch),
|
|
4794
4922
|
headVersion: cloud.version,
|
|
4795
4923
|
checksum: cloud.checksum,
|
|
4924
|
+
deliveryWatermark: cloud.deliveryWatermark,
|
|
4796
4925
|
records: cloud.snapshot.records,
|
|
4797
4926
|
},
|
|
4798
4927
|
};
|