@amalgm/shell 0.1.133 → 0.1.134
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/dist/content-cache-host.d.ts +27 -3
- package/dist/content-cache-host.js +82 -7
- package/dist/content-cache-host.js.map +1 -1
- package/dist/register-upload-fusion.d.ts +83 -0
- package/dist/register-upload-fusion.js +169 -0
- package/dist/register-upload-fusion.js.map +1 -0
- package/dist/user-ground-host.d.ts +8 -0
- package/dist/user-ground-host.js +135 -60
- package/dist/user-ground-host.js.map +1 -1
- package/dist/wire-client.js +1 -1
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -11,7 +11,8 @@ import { EntityApplyHost, } from "./entity-apply-host.js";
|
|
|
11
11
|
import { EntityRecordSqliteStore } from "./entity-record-store.js";
|
|
12
12
|
import { observeEntityRecordPorts } from "./entity-record-observability.js";
|
|
13
13
|
import { indexRowsByOutermostRoot } from "./ground-root-index.js";
|
|
14
|
-
import { ContentCacheDownloadSet, captureContentFile, hasSealedContent, hashContentFile, pruneSettledContentCache, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
14
|
+
import { ContentCacheDownloadSet, captureContentFile, contentBlockCollector, hasSealedContent, hashContentFile, pruneSettledContentCache, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
15
|
+
import { RegisterUploadFusion } from "./register-upload-fusion.js";
|
|
15
16
|
import { StagedContentSet, addStagingEvidenceDir, clearAddStagingEvidence, } from "./workspace-add-staging.js";
|
|
16
17
|
import { DirectContentPlane } from "./content-direct.js";
|
|
17
18
|
import { encodeContentWireBytes } from "./content-wire-codec.js";
|
|
@@ -676,20 +677,75 @@ export class UserGroundHost {
|
|
|
676
677
|
this.stage({
|
|
677
678
|
primitive: "register", stage: "entity-registration", status: "started", workspaceId,
|
|
678
679
|
});
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
680
|
+
// The walk's own read is also the upload: smalls ride cargo
|
|
681
|
+
// segments, larges stream blocks and seal last. The flush settles
|
|
682
|
+
// every fused effect before publication plans, so the upload
|
|
683
|
+
// stage's seal inventory — the guarantor for anything fusion
|
|
684
|
+
// could not ship — already sees these seals.
|
|
685
|
+
const fusionResourceId = this.cloudState.resourceId;
|
|
686
|
+
const fusionLegs = new TransferLegs();
|
|
687
|
+
const fusion = new RegisterUploadFusion({
|
|
688
|
+
sealedContentHashes: (hashes) => this.sealedContentHashes(fusionResourceId, hashes),
|
|
689
|
+
sendCargo: async (artifacts, payload) => {
|
|
690
|
+
const { sealed } = await this.putCargo(fusionResourceId, artifacts, payload, fusionLegs);
|
|
691
|
+
return sealed;
|
|
692
|
+
},
|
|
693
|
+
putBlocks: async (chunks, bytes) => {
|
|
694
|
+
await this.putContentBlocks(fusionResourceId, chunks, bytes, fusionLegs);
|
|
695
|
+
},
|
|
696
|
+
sealManifest: async (manifest) => {
|
|
697
|
+
await this.putContentManifest(fusionResourceId, manifest, fusionLegs);
|
|
698
|
+
},
|
|
688
699
|
});
|
|
700
|
+
const scanned = await (async () => {
|
|
701
|
+
try {
|
|
702
|
+
return await reconcileGround({
|
|
703
|
+
identity,
|
|
704
|
+
userRoot: this.userRoot(identity),
|
|
705
|
+
database: this.databasePath(identity),
|
|
706
|
+
cacheDir: this.cacheDir(identity),
|
|
707
|
+
onScan: (evidence) => this.reportDetectScan(evidence),
|
|
708
|
+
measuring: this.measuring(),
|
|
709
|
+
registrationWorkspaceId: workspaceId,
|
|
710
|
+
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion])),
|
|
711
|
+
fusion,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
finally {
|
|
715
|
+
// Even an abandoned walk settles its in-flight cargo, and a
|
|
716
|
+
// stale-capture re-walk then skips whatever already sealed.
|
|
717
|
+
const fused = await fusion.flush();
|
|
718
|
+
this.stage({
|
|
719
|
+
primitive: "register",
|
|
720
|
+
stage: "fused-upload",
|
|
721
|
+
status: "completed",
|
|
722
|
+
workspaceId,
|
|
723
|
+
measurements: {
|
|
724
|
+
smallArtifacts: fused.smallArtifacts,
|
|
725
|
+
smallBytes: fused.smallBytes,
|
|
726
|
+
cargoSegments: fused.cargoSegments,
|
|
727
|
+
sealedSkipped: fused.sealedSkipped,
|
|
728
|
+
largeArtifacts: fused.largeArtifacts,
|
|
729
|
+
largeBytes: fused.largeBytes,
|
|
730
|
+
...(fused.disabledReason !== null ? { disabled: fused.disabledReason } : {}),
|
|
731
|
+
...fusionLegs.report(),
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
})();
|
|
689
736
|
const retry = scanned.detection.find((plan) => plan.kind === "retry");
|
|
690
737
|
if (retry?.kind === "retry") {
|
|
691
738
|
throw new Error(retry.reasons.join("; "));
|
|
692
739
|
}
|
|
740
|
+
// The classic upload re-proved every source byte against its
|
|
741
|
+
// captured hash when it read it. The fused rail already shipped
|
|
742
|
+
// those bytes, so the equivalent proof is the fingerprint law at
|
|
743
|
+
// stat cost: any fused source that moved after capture makes this
|
|
744
|
+
// attempt stale, and the re-walk publishes what stands there now.
|
|
745
|
+
const movedGround = await fusion.movedGround();
|
|
746
|
+
if (movedGround !== null) {
|
|
747
|
+
throw new StaleCaptureError(`${movedGround} changed after its fused upload`);
|
|
748
|
+
}
|
|
693
749
|
const registrationDuration = performance.now() - registrationStarted;
|
|
694
750
|
const commitStarted = performance.now();
|
|
695
751
|
this.stage({
|
|
@@ -867,10 +923,10 @@ export class UserGroundHost {
|
|
|
867
923
|
throw new Error("entity Record rail already started");
|
|
868
924
|
await this.wire.open();
|
|
869
925
|
// One floor, no per-feature gates: this shell and its gateway release
|
|
870
|
-
// together, and the gateway ships first.
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
if (this.wire.protocolVersion <
|
|
926
|
+
// together, and the gateway ships first. 15 = one-read Register capture:
|
|
927
|
+
// large-file blocks become durable while capture owns their bytes, then
|
|
928
|
+
// the manifest seals last. An older gateway cannot accept that rail.
|
|
929
|
+
if (this.wire.protocolVersion < 15) {
|
|
874
930
|
throw new Error("gateway speaks an older wire protocol than this shell");
|
|
875
931
|
}
|
|
876
932
|
this.adoptCloudDeliveryBaseline(identity, this.cloudState.deliveryWatermark);
|
|
@@ -1413,6 +1469,40 @@ export class UserGroundHost {
|
|
|
1413
1469
|
wireBytes: encoded.bytes.byteLength,
|
|
1414
1470
|
};
|
|
1415
1471
|
}
|
|
1472
|
+
/** Store one wave of provisional content blocks, addressed by their own
|
|
1473
|
+
* digests and bound to no artifact. Resolving means every block is
|
|
1474
|
+
* durable at the authority. Answers the wire bytes actually sent. */
|
|
1475
|
+
async putContentBlocks(resourceId, blocks, payload, legs) {
|
|
1476
|
+
const encoded = await legs.time("encodeMs", () => encodeContentWireBytes(payload));
|
|
1477
|
+
const frame = await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1478
|
+
type: "private.entity-content.put-blocks",
|
|
1479
|
+
resource_id: resourceId,
|
|
1480
|
+
blocks,
|
|
1481
|
+
bytes: payload.byteLength,
|
|
1482
|
+
wire_bytes: encoded.bytes.byteLength,
|
|
1483
|
+
...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
|
|
1484
|
+
}, encoded.bytes, ["private.entity-content.stored-blocks"])));
|
|
1485
|
+
legs.add("gatewayVerifyMs", Number(frame.verify_ms));
|
|
1486
|
+
legs.add("gatewayStoreMs", Number(frame.store_ms));
|
|
1487
|
+
return encoded.bytes.byteLength;
|
|
1488
|
+
}
|
|
1489
|
+
/** Seal one artifact's manifest. The authority re-proves every named
|
|
1490
|
+
* block durable before anything becomes visible, so this frame is only
|
|
1491
|
+
* lawful after the complete file went over the wire. */
|
|
1492
|
+
async putContentManifest(resourceId, manifest, legs) {
|
|
1493
|
+
const bytes = Buffer.from(stableJson(manifest), "utf8");
|
|
1494
|
+
const frame = await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1495
|
+
type: "private.entity-content.put",
|
|
1496
|
+
resource_id: resourceId,
|
|
1497
|
+
content_hash: manifest.contentHash,
|
|
1498
|
+
sha256: sha256Hex(bytes),
|
|
1499
|
+
bytes: bytes.length,
|
|
1500
|
+
}, bytes, ["private.entity-content.stored"])));
|
|
1501
|
+
legs.add("gatewayVerifyMs", Number(frame.verify_ms));
|
|
1502
|
+
legs.add("gatewayStoreMs", Number(frame.store_ms));
|
|
1503
|
+
legs.add("gatewaySealMs", Number(frame.seal_ms));
|
|
1504
|
+
return bytes.length;
|
|
1505
|
+
}
|
|
1416
1506
|
async uploadSnapshotContent(identity, resourceId, records, trace, manifestHints) {
|
|
1417
1507
|
const cache = this.cacheDir(identity);
|
|
1418
1508
|
const byHash = new Map();
|
|
@@ -1550,22 +1640,12 @@ export class UserGroundHost {
|
|
|
1550
1640
|
const { artifact, file, manifest: knownManifest, bytes: fileBytes } = source;
|
|
1551
1641
|
const sourceFile = { handle: null };
|
|
1552
1642
|
let manifestPresent = false;
|
|
1553
|
-
const
|
|
1643
|
+
const chunkBlocks = (manifest, indexes) => indexes.map((index) => {
|
|
1554
1644
|
const chunk = manifest.chunks[index];
|
|
1555
1645
|
if (!chunk)
|
|
1556
1646
|
throw new Error(`content manifest has no chunk ${index}`);
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
resource_id: resourceId,
|
|
1560
|
-
content_hash: artifact.contentHash,
|
|
1561
|
-
kind: "chunk",
|
|
1562
|
-
part_index: index,
|
|
1563
|
-
part_count: manifest.chunks.length,
|
|
1564
|
-
sha256: chunk.sha256,
|
|
1565
|
-
bytes: chunk.bytes,
|
|
1566
|
-
}, bytes, ["private.entity-content.stored"])));
|
|
1567
|
-
uploadedBytes += bytes.byteLength;
|
|
1568
|
-
};
|
|
1647
|
+
return { sha256: chunk.sha256, bytes: chunk.bytes };
|
|
1648
|
+
});
|
|
1569
1649
|
try {
|
|
1570
1650
|
const receipt = await uploadArtifact(artifact, {
|
|
1571
1651
|
bytes: fileBytes,
|
|
@@ -1595,40 +1675,16 @@ export class UserGroundHost {
|
|
|
1595
1675
|
}
|
|
1596
1676
|
return frame.missing;
|
|
1597
1677
|
},
|
|
1598
|
-
putChunk: async (_candidate, manifest, index, bytes) =>
|
|
1678
|
+
putChunk: async (_candidate, manifest, index, bytes) => {
|
|
1679
|
+
uploadedBytes += await this.putContentBlocks(resourceId, chunkBlocks(manifest, [index]), bytes, legs);
|
|
1680
|
+
},
|
|
1599
1681
|
putChunkBatch: async (_candidate, manifest, indexes, bytes) => {
|
|
1600
|
-
|
|
1601
|
-
await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1602
|
-
type: "private.entity-content.put-batch",
|
|
1603
|
-
resource_id: resourceId,
|
|
1604
|
-
content_hash: artifact.contentHash,
|
|
1605
|
-
part_count: manifest.chunks.length,
|
|
1606
|
-
chunks: indexes.map((index) => ({
|
|
1607
|
-
part_index: index,
|
|
1608
|
-
sha256: manifest.chunks[index]?.sha256,
|
|
1609
|
-
bytes: manifest.chunks[index]?.bytes,
|
|
1610
|
-
})),
|
|
1611
|
-
bytes: bytes.byteLength,
|
|
1612
|
-
wire_bytes: encoded.bytes.byteLength,
|
|
1613
|
-
...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
|
|
1614
|
-
}, encoded.bytes, ["private.entity-content.stored-batch"])));
|
|
1615
|
-
uploadedBytes += encoded.bytes.byteLength;
|
|
1682
|
+
uploadedBytes += await this.putContentBlocks(resourceId, chunkBlocks(manifest, indexes), bytes, legs);
|
|
1616
1683
|
},
|
|
1617
1684
|
putManifest: async (_candidate, manifest) => {
|
|
1618
1685
|
if (manifestPresent)
|
|
1619
1686
|
return;
|
|
1620
|
-
|
|
1621
|
-
await legs.time("wireMs", () => retryContent(() => this.wire.requestBinary({
|
|
1622
|
-
type: "private.entity-content.put",
|
|
1623
|
-
resource_id: resourceId,
|
|
1624
|
-
content_hash: artifact.contentHash,
|
|
1625
|
-
kind: "manifest",
|
|
1626
|
-
part_index: manifest.chunks.length,
|
|
1627
|
-
part_count: manifest.chunks.length,
|
|
1628
|
-
sha256: sha256Hex(bytes),
|
|
1629
|
-
bytes: bytes.length,
|
|
1630
|
-
}, bytes, ["private.entity-content.stored"])));
|
|
1631
|
-
uploadedBytes += bytes.byteLength;
|
|
1687
|
+
uploadedBytes += await this.putContentManifest(resourceId, manifest, legs);
|
|
1632
1688
|
},
|
|
1633
1689
|
});
|
|
1634
1690
|
uploadedChunks += receipt.uploadedChunks;
|
|
@@ -4102,7 +4158,7 @@ function materializedWorkspaceBindings(bindingDir) {
|
|
|
4102
4158
|
return bindings.sort((left, right) => left.workspaceId.localeCompare(right.workspaceId));
|
|
4103
4159
|
}
|
|
4104
4160
|
async function reconcileGround(input) {
|
|
4105
|
-
const { identity, userRoot, database, cacheDir, suspicions, coreScan, registrationWorkspaceId, onScan, measuring = false, } = input;
|
|
4161
|
+
const { identity, userRoot, database, cacheDir, suspicions, coreScan, registrationWorkspaceId, fusion, onScan, measuring = false, } = input;
|
|
4106
4162
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
4107
4163
|
const bindingDir = workspaceBindingDir(userRoot, identity.deviceId);
|
|
4108
4164
|
const boundRootEntries = materializedWorkspaceBindings(bindingDir);
|
|
@@ -4328,9 +4384,11 @@ async function reconcileGround(input) {
|
|
|
4328
4384
|
suspicion: observationFor(rootPath),
|
|
4329
4385
|
// A register scan publishes one complete snapshot; it never mints Send
|
|
4330
4386
|
// records for the same initial contents, so it plans no detection and
|
|
4331
|
-
// stages no cache bytes —
|
|
4387
|
+
// stages no cache bytes — the fused rail ships content from the walk's
|
|
4388
|
+
// own read, and upload re-reads only what fusion could not seal.
|
|
4332
4389
|
plansSendRecords: suspicions !== undefined && registrationWorkspaceId === undefined,
|
|
4333
4390
|
largeManifests,
|
|
4391
|
+
fusion,
|
|
4334
4392
|
});
|
|
4335
4393
|
}
|
|
4336
4394
|
const changedUUIDs = new Set([
|
|
@@ -4358,7 +4416,7 @@ async function reconcileGround(input) {
|
|
|
4358
4416
|
};
|
|
4359
4417
|
}
|
|
4360
4418
|
async function reconcileRoot(input) {
|
|
4361
|
-
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, measureEvidence = false, plansSendRecords, largeManifests, } = input;
|
|
4419
|
+
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, measureEvidence = false, plansSendRecords, largeManifests, fusion, } = input;
|
|
4362
4420
|
// Prior rows reach this scan from every binding this root encloses. A row's
|
|
4363
4421
|
// stored relativePath is relative to the root that last committed it, which
|
|
4364
4422
|
// is a different frame when an enclosing boundary was just registered
|
|
@@ -4711,9 +4769,20 @@ async function reconcileRoot(input) {
|
|
|
4711
4769
|
captureTasks.push(captureGate(async () => {
|
|
4712
4770
|
const taskStarted = measureEvidence ? performance.now() : 0;
|
|
4713
4771
|
let current = stats;
|
|
4772
|
+
// Repository leaves ship on the Git-native rail; everything else
|
|
4773
|
+
// may fuse its capture read with upload. Fusion is opportunistic:
|
|
4774
|
+
// once disabled, captures continue exactly as the classic path
|
|
4775
|
+
// and the upload stage ships what the walk could not.
|
|
4776
|
+
const fused = activeRepository ? undefined : fusion;
|
|
4714
4777
|
for (let attempt = 1;; attempt++) {
|
|
4715
4778
|
try {
|
|
4716
|
-
const
|
|
4779
|
+
const collect = fused?.enabled && current.size <= CHUNK_BYTES
|
|
4780
|
+
? contentBlockCollector(current.size)
|
|
4781
|
+
: null;
|
|
4782
|
+
const captured = fused && current.size > CHUNK_BYTES
|
|
4783
|
+
? (await fused.captureLarge(absolutePath, current))
|
|
4784
|
+
?? await captureContentFile(absolutePath, current, activeRepository || !plansSendRecords ? null : cacheDir)
|
|
4785
|
+
: await captureContentFile(absolutePath, current, activeRepository || !plansSendRecords ? null : cacheDir, collect);
|
|
4717
4786
|
if (measureEvidence && evidence) {
|
|
4718
4787
|
evidence.captureBusyMs += performance.now() - taskStarted;
|
|
4719
4788
|
evidence.captureBytes += captured.bytes;
|
|
@@ -4726,6 +4795,12 @@ async function reconcileRoot(input) {
|
|
|
4726
4795
|
if (captured.bytes > CHUNK_BYTES) {
|
|
4727
4796
|
largeManifests?.set(captured.contentHash, captured.manifest);
|
|
4728
4797
|
}
|
|
4798
|
+
// The capture proved these exact bytes against the fingerprint
|
|
4799
|
+
// law; hand them to the cargo rail. Waiting here is window
|
|
4800
|
+
// backpressure, bounding memory by the capture pool's width.
|
|
4801
|
+
if (collect && fused) {
|
|
4802
|
+
await fused.addSmall(absolutePath, current, captured, collect.collected);
|
|
4803
|
+
}
|
|
4729
4804
|
return;
|
|
4730
4805
|
}
|
|
4731
4806
|
catch (error) {
|