@amalgm/shell 0.1.93 → 0.1.95
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 +44 -31
- package/README.md +2 -1
- package/dist/content-cache-host.d.ts +2 -4
- package/dist/content-cache-host.js +11 -37
- package/dist/content-cache-host.js.map +1 -1
- package/dist/content-direct.d.ts +31 -0
- package/dist/content-direct.js +101 -0
- package/dist/content-direct.js.map +1 -0
- package/dist/content-wire-codec.d.ts +3 -7
- package/dist/content-wire-codec.js +4 -25
- package/dist/content-wire-codec.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/user-ground-host.d.ts +5 -0
- package/dist/user-ground-host.js +103 -127
- package/dist/user-ground-host.js.map +1 -1
- package/dist/wire-client.d.ts +0 -1
- package/dist/wire-client.js +7 -16
- package/dist/wire-client.js.map +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, download as downloadArtifact, encodeEntityRecord, EntityApplyRail, EntityRecordRail, membershipHash, parseSnapshot, pathIsSuspect, 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, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, indexRepositoryTerritory, download as downloadArtifact, 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";
|
|
@@ -12,7 +12,8 @@ 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
14
|
import { ContentCacheDownload, captureContentFile, hashContentFile, sealCapturedArtifact, } from "./content-cache-host.js";
|
|
15
|
-
import {
|
|
15
|
+
import { DirectContentPlane } from "./content-direct.js";
|
|
16
|
+
import { encodeContentWireBytes } from "./content-wire-codec.js";
|
|
16
17
|
import { exactTextReplay, planGroundDetection, reconcileGroundUUIDs, } from "./detection/portable.js";
|
|
17
18
|
import { AcceptedInboxResultIndex } from "./detection/accepted-result.js";
|
|
18
19
|
import { sameNotebookBasisRow } from "./detection/notebook-basis.js";
|
|
@@ -26,8 +27,13 @@ import { GroundCoordinator } from "./ground-coordination.js";
|
|
|
26
27
|
import { submitMutationOperation } from "./paged-mutation-submit.js";
|
|
27
28
|
import { WireClient, WireRequestError } from "./wire-client.js";
|
|
28
29
|
const SMALL_CONTENT_UPLOAD_CONCURRENCY = 16;
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
/** Artifact lanes for a download phase. Direct storage fetches cost the
|
|
31
|
+
* gateway nothing, so parallelism is bounded by this machine's pipe rather
|
|
32
|
+
* than authority fairness. */
|
|
33
|
+
const FILE_CONTENT_DOWNLOAD_CONCURRENCY = 16;
|
|
34
|
+
/** Block lanes within one artifact. Most files are a single block; large
|
|
35
|
+
* repository packs fan their chunks across this. */
|
|
36
|
+
const DIRECT_BLOCK_DOWNLOAD_CONCURRENCY = 8;
|
|
31
37
|
const DETECT_QUIET_MS = 12;
|
|
32
38
|
const DETECT_MAX_DEFERRAL_MS = 75;
|
|
33
39
|
const DETECT_RETRY_MS = 250;
|
|
@@ -47,6 +53,7 @@ const GROUND_ROW_COLUMNS = [
|
|
|
47
53
|
export class UserGroundHost {
|
|
48
54
|
options;
|
|
49
55
|
wire;
|
|
56
|
+
direct;
|
|
50
57
|
watchHost;
|
|
51
58
|
activeIdentity = null;
|
|
52
59
|
rescanTimer = null;
|
|
@@ -75,6 +82,9 @@ export class UserGroundHost {
|
|
|
75
82
|
runtimeVersion: options.runtimeVersion,
|
|
76
83
|
requestTimeoutMs: options.wireRequestTimeoutMs,
|
|
77
84
|
});
|
|
85
|
+
this.direct = new DirectContentPlane({
|
|
86
|
+
request: (frame, accept) => this.wire.request(frame, accept),
|
|
87
|
+
});
|
|
78
88
|
this.watchHost = new NodeWatchHost({
|
|
79
89
|
open: options.openWatch,
|
|
80
90
|
onSignal: () => this.scheduleRescan(),
|
|
@@ -461,7 +471,17 @@ export class UserGroundHost {
|
|
|
461
471
|
&& record.payloadVersion === workspaceId);
|
|
462
472
|
if (hasRegisteredBoundaryInDatabase(database, workspaceId)
|
|
463
473
|
&& hasRegisteredBoundary(this.cloudState.records)) {
|
|
464
|
-
|
|
474
|
+
// Already declared — but Register may only return once cargo closure
|
|
475
|
+
// holds under the current content contract. The graph head can be
|
|
476
|
+
// unchanged while a contract cutover moved its cargo namespace, so
|
|
477
|
+
// the publish path re-proves seal evidence and repairs any missing
|
|
478
|
+
// uploads before this answer is lawful.
|
|
479
|
+
try {
|
|
480
|
+
await this.publishMaterializedSnapshot(identity, new Set([workspaceId]), { journey: "register", workspaceId });
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
this.finishColdOperation();
|
|
484
|
+
}
|
|
465
485
|
return {
|
|
466
486
|
registered: true,
|
|
467
487
|
entityCount: countDescendantRows(database, workspaceId),
|
|
@@ -670,10 +690,10 @@ export class UserGroundHost {
|
|
|
670
690
|
throw new Error("entity Record rail already started");
|
|
671
691
|
await this.wire.open();
|
|
672
692
|
// One floor, no per-feature gates: this shell and its gateway release
|
|
673
|
-
// together, and the gateway ships first.
|
|
674
|
-
// an older gateway drops the
|
|
675
|
-
// publish.
|
|
676
|
-
if (this.wire.protocolVersion <
|
|
693
|
+
// together, and the gateway ships first. 10 = contract-aware cargo
|
|
694
|
+
// closure; an older gateway drops the sealed-evidence frame the upload
|
|
695
|
+
// planner depends on, so nothing this shell registers could publish.
|
|
696
|
+
if (this.wire.protocolVersion < 10) {
|
|
677
697
|
throw new Error("gateway speaks an older wire protocol than this shell");
|
|
678
698
|
}
|
|
679
699
|
const store = new EntityRecordSqliteStore(initializeDatabase(this.databasePath(identity)));
|
|
@@ -1220,42 +1240,13 @@ export class UserGroundHost {
|
|
|
1220
1240
|
measurements: { journey: trace?.journey ?? "bootstrap", kind: artifact.kind },
|
|
1221
1241
|
});
|
|
1222
1242
|
const cache = { target: null };
|
|
1223
|
-
//
|
|
1224
|
-
//
|
|
1225
|
-
// the
|
|
1226
|
-
//
|
|
1227
|
-
const
|
|
1228
|
-
const sliceChunks = (packed, manifest, indexes) => {
|
|
1229
|
-
const offsets = [];
|
|
1230
|
-
let offset = 0;
|
|
1231
|
-
for (const chunk of manifest.chunks) {
|
|
1232
|
-
offsets.push(offset);
|
|
1233
|
-
offset += chunk.bytes;
|
|
1234
|
-
}
|
|
1235
|
-
return Buffer.concat(indexes.map((index) => {
|
|
1236
|
-
const chunk = manifest.chunks[index];
|
|
1237
|
-
const start = offsets[index];
|
|
1238
|
-
if (!chunk || start === undefined)
|
|
1239
|
-
throw new Error(`content manifest has no chunk ${index}`);
|
|
1240
|
-
return packed.subarray(start, start + chunk.bytes);
|
|
1241
|
-
}));
|
|
1242
|
-
};
|
|
1243
|
-
const getChunk = async (manifest, index) => {
|
|
1244
|
-
if (prefetched.bytes)
|
|
1245
|
-
return sliceChunks(prefetched.bytes, manifest, [index]);
|
|
1246
|
-
const chunk = manifest.chunks[index];
|
|
1247
|
-
if (!chunk)
|
|
1248
|
-
throw new Error(`content manifest has no chunk ${index}`);
|
|
1243
|
+
// Every storage object — the manifest and each block — is fetched
|
|
1244
|
+
// straight from content storage over a presigned URL and opened locally;
|
|
1245
|
+
// the wire carries only the key handoff and signing requests. A retry
|
|
1246
|
+
// asks for a fresh URL every attempt.
|
|
1247
|
+
const fetchObject = async (object) => {
|
|
1249
1248
|
requests += 1;
|
|
1250
|
-
const
|
|
1251
|
-
type: "private.entity-content.get",
|
|
1252
|
-
resource_id: resourceId,
|
|
1253
|
-
content_hash: artifact.contentHash,
|
|
1254
|
-
kind: "chunk",
|
|
1255
|
-
part_index: index,
|
|
1256
|
-
sha256: chunk.sha256,
|
|
1257
|
-
}, ["private.entity-content.data"]), true, retryAttempts);
|
|
1258
|
-
const bytes = binaryFrameBytes(frame);
|
|
1249
|
+
const bytes = await retryContent(() => this.direct.fetchObject(resourceId, object), true, retryAttempts);
|
|
1259
1250
|
downloadedBytes += bytes.byteLength;
|
|
1260
1251
|
return bytes;
|
|
1261
1252
|
};
|
|
@@ -1267,73 +1258,39 @@ export class UserGroundHost {
|
|
|
1267
1258
|
// proves chunk identity; downloaded bytes are still hash-verified,
|
|
1268
1259
|
// so a stale local manifest can only fail loudly, never corrupt.
|
|
1269
1260
|
const local = knownManifest ?? readCachedManifest(cacheDir, artifact.contentHash);
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}, ["private.entity-content.artifact"]), true, retryAttempts);
|
|
1280
|
-
const manifestJson = String(frame.manifest_json ?? "");
|
|
1281
|
-
downloadedBytes += Buffer.byteLength(manifestJson);
|
|
1282
|
-
const checked = checkContentManifest(JSON.parse(manifestJson), artifact.contentHash);
|
|
1283
|
-
if (!checked.ok)
|
|
1284
|
-
throw new Error(checked.error);
|
|
1285
|
-
if (frame.complete === true) {
|
|
1286
|
-
const decoded = await decodeContentWireBytes(binaryWireFrameBytes(frame), frame.content_encoding, Number(frame.bytes));
|
|
1287
|
-
if (decoded.byteLength !== checked.value.bytes) {
|
|
1288
|
-
throw new Error("artifact payload does not match its manifest");
|
|
1289
|
-
}
|
|
1290
|
-
prefetched.bytes = decoded;
|
|
1291
|
-
downloadedBytes += Number(frame.wire_bytes ?? frame.bytes);
|
|
1292
|
-
}
|
|
1293
|
-
cache.target = new ContentCacheDownload(cacheDir, artifact, checked.value);
|
|
1294
|
-
return checked.value;
|
|
1261
|
+
const manifest = local ?? await (async () => {
|
|
1262
|
+
const bytes = await fetchObject({ kind: "manifest", sha256: artifact.contentHash });
|
|
1263
|
+
const checked = checkContentManifest(JSON.parse(bytes.toString("utf8")), artifact.contentHash);
|
|
1264
|
+
if (!checked.ok)
|
|
1265
|
+
throw new Error(checked.error);
|
|
1266
|
+
return checked.value;
|
|
1267
|
+
})();
|
|
1268
|
+
cache.target = new ContentCacheDownload(cacheDir, artifact, manifest);
|
|
1269
|
+
return manifest;
|
|
1295
1270
|
},
|
|
1296
1271
|
hasChunk: async (_candidate, _manifest, index) => {
|
|
1297
1272
|
if (!cache.target)
|
|
1298
1273
|
throw new Error("download cache has no content manifest");
|
|
1299
1274
|
return cache.target.hasChunk(index);
|
|
1300
1275
|
},
|
|
1301
|
-
getChunk: async (_candidate, manifest, index) =>
|
|
1302
|
-
|
|
1303
|
-
if (
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
type: "private.entity-content.get-batch",
|
|
1307
|
-
resource_id: resourceId,
|
|
1308
|
-
content_hash: artifact.contentHash,
|
|
1309
|
-
chunks: indexes.map((index) => ({
|
|
1310
|
-
part_index: index,
|
|
1311
|
-
sha256: manifest.chunks[index]?.sha256,
|
|
1312
|
-
bytes: manifest.chunks[index]?.bytes,
|
|
1313
|
-
})),
|
|
1314
|
-
}, ["private.entity-content.data-batch"]), true, retryAttempts);
|
|
1315
|
-
requests += 1;
|
|
1316
|
-
const decoded = await decodeContentWireBytes(binaryWireFrameBytes(frame), frame.content_encoding, Number(frame.bytes));
|
|
1317
|
-
downloadedBytes += Number(frame.wire_bytes ?? frame.bytes);
|
|
1318
|
-
return decoded;
|
|
1276
|
+
getChunk: async (_candidate, manifest, index) => {
|
|
1277
|
+
const chunk = manifest.chunks[index];
|
|
1278
|
+
if (!chunk)
|
|
1279
|
+
throw new Error(`content manifest has no chunk ${index}`);
|
|
1280
|
+
return fetchObject({ kind: "block", sha256: chunk.sha256 });
|
|
1319
1281
|
},
|
|
1320
1282
|
putChunk: async (_candidate, _manifest, index, bytes) => {
|
|
1321
1283
|
if (!cache.target)
|
|
1322
1284
|
throw new Error("download cache has no content manifest");
|
|
1323
1285
|
await cache.target.putChunk(index, bytes);
|
|
1324
1286
|
},
|
|
1325
|
-
putChunkBatch: async (_candidate, _manifest, indexes, bytes) => {
|
|
1326
|
-
if (!cache.target)
|
|
1327
|
-
throw new Error("download cache has no content manifest");
|
|
1328
|
-
await cache.target.putChunkBatch(indexes, bytes);
|
|
1329
|
-
},
|
|
1330
1287
|
sealArtifact: async () => {
|
|
1331
1288
|
if (!cache.target)
|
|
1332
1289
|
throw new Error("download cache has no content manifest");
|
|
1333
1290
|
const sealed = await cache.target.seal();
|
|
1334
1291
|
return { local: sealed, bytes: sealed.bytes, contentHash: sealed.contentHash };
|
|
1335
1292
|
},
|
|
1336
|
-
},
|
|
1293
|
+
}, { concurrency: DIRECT_BLOCK_DOWNLOAD_CONCURRENCY });
|
|
1337
1294
|
this.stage({
|
|
1338
1295
|
primitive: "download",
|
|
1339
1296
|
stage: "immutable-content",
|
|
@@ -1757,9 +1714,37 @@ export class UserGroundHost {
|
|
|
1757
1714
|
const snapshot = snapshotFromRecords(traveling);
|
|
1758
1715
|
const snapshotJson = stableJson(snapshot);
|
|
1759
1716
|
const checksum = sha256Hex(snapshotJson);
|
|
1760
|
-
if (checksum === state.checksum)
|
|
1761
|
-
return;
|
|
1762
1717
|
const localIds = new Set(localRecords.map((record) => record.uuid));
|
|
1718
|
+
// Graph membership is never proof of sealed cargo: a record in the cloud
|
|
1719
|
+
// graph may name content sealed only under a prior content contract,
|
|
1720
|
+
// whose namespace the current readers never touch. Only the authority's
|
|
1721
|
+
// (resource, hash, contract) seal evidence may skip an upload — and the
|
|
1722
|
+
// question is asked even when the graph itself is unchanged, so Register
|
|
1723
|
+
// always re-establishes cargo closure before returning.
|
|
1724
|
+
const cargo = snapshot.records.flatMap((record) => {
|
|
1725
|
+
if (!localIds.has(record.uuid))
|
|
1726
|
+
return [];
|
|
1727
|
+
const artifact = artifactForRecord(record);
|
|
1728
|
+
return artifact ? [{ record, contentHash: artifact.contentHash }] : [];
|
|
1729
|
+
});
|
|
1730
|
+
const sealedArtifacts = await this.sealedContentHashes(state.resourceId, [...new Set(cargo.map((entry) => entry.contentHash))]);
|
|
1731
|
+
const uploadRecords = cargo
|
|
1732
|
+
.filter((entry) => !sealedArtifacts.has(entry.contentHash))
|
|
1733
|
+
.map((entry) => entry.record);
|
|
1734
|
+
if (checksum === state.checksum) {
|
|
1735
|
+
// The graph is already committed; only cargo can be missing. Repair
|
|
1736
|
+
// uploads need no journal row — a crash simply re-runs the same
|
|
1737
|
+
// repair on the next Register.
|
|
1738
|
+
if (uploadRecords.length > 0) {
|
|
1739
|
+
try {
|
|
1740
|
+
await this.uploadSnapshotContent(identity, state.resourceId, uploadRecords, trace);
|
|
1741
|
+
}
|
|
1742
|
+
catch (error) {
|
|
1743
|
+
throw pipelineStageError("cloud upload", error);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1763
1748
|
const replacementRootIds = localRecords
|
|
1764
1749
|
.filter((record) => record.parentUUID === null)
|
|
1765
1750
|
.map((record) => record.uuid);
|
|
@@ -1770,16 +1755,6 @@ export class UserGroundHost {
|
|
|
1770
1755
|
return root !== null && replacementRoots.has(root);
|
|
1771
1756
|
});
|
|
1772
1757
|
const replacement = rootReplacementFromRecords(replacementRootIds, replacementRecords);
|
|
1773
|
-
const publishedArtifacts = new Set(state.records.flatMap((record) => {
|
|
1774
|
-
const artifact = artifactForRecord(record);
|
|
1775
|
-
return artifact ? [artifact.contentHash] : [];
|
|
1776
|
-
}));
|
|
1777
|
-
const uploadRecords = snapshot.records.filter((record) => {
|
|
1778
|
-
if (!localIds.has(record.uuid))
|
|
1779
|
-
return false;
|
|
1780
|
-
const artifact = artifactForRecord(record);
|
|
1781
|
-
return artifact !== null && !publishedArtifacts.has(artifact.contentHash);
|
|
1782
|
-
});
|
|
1783
1758
|
supersedeSnapshotJournal(this.databasePath(identity), {
|
|
1784
1759
|
mutationId: randomUUID(),
|
|
1785
1760
|
resourceId: state.resourceId,
|
|
@@ -1794,6 +1769,29 @@ export class UserGroundHost {
|
|
|
1794
1769
|
});
|
|
1795
1770
|
await this.publishPendingSnapshots(identity, trace);
|
|
1796
1771
|
}
|
|
1772
|
+
/** Which of these heads does the authority hold sealed under the current
|
|
1773
|
+
* content contract? Batched so a whole workspace costs a handful of
|
|
1774
|
+
* frames; the answer is the only permission to skip an upload. */
|
|
1775
|
+
async sealedContentHashes(resourceId, hashes) {
|
|
1776
|
+
const batches = [];
|
|
1777
|
+
for (let start = 0; start < hashes.length; start += PRESIGN_BATCH_OBJECTS) {
|
|
1778
|
+
batches.push(hashes.slice(start, start + PRESIGN_BATCH_OBJECTS));
|
|
1779
|
+
}
|
|
1780
|
+
const sealed = new Set();
|
|
1781
|
+
const answers = await Promise.all(batches.map((batch) => retryContent(() => this.wire.request({
|
|
1782
|
+
type: "private.entity-content.sealed",
|
|
1783
|
+
resource_id: resourceId,
|
|
1784
|
+
content_hashes: batch,
|
|
1785
|
+
}, ["private.entity-content.sealed-result"]))));
|
|
1786
|
+
for (const frame of answers) {
|
|
1787
|
+
if (!Array.isArray(frame.sealed)) {
|
|
1788
|
+
throw new Error("content authority answered an invalid seal inventory");
|
|
1789
|
+
}
|
|
1790
|
+
for (const hash of frame.sealed)
|
|
1791
|
+
sealed.add(String(hash));
|
|
1792
|
+
}
|
|
1793
|
+
return sealed;
|
|
1794
|
+
}
|
|
1797
1795
|
async publishPendingSnapshots(identity, trace) {
|
|
1798
1796
|
for (const pending of readSnapshotPublications(this.databasePath(identity))) {
|
|
1799
1797
|
// A rejected row is a settled fact, not an intent: the authority
|
|
@@ -4689,28 +4687,6 @@ function readyFromFrame(frame) {
|
|
|
4689
4687
|
},
|
|
4690
4688
|
};
|
|
4691
4689
|
}
|
|
4692
|
-
/** Expose the WebSocket payload as a view. Live verifies content blocks
|
|
4693
|
-
* against their manifest, so the host must not copy or hash those bytes a
|
|
4694
|
-
* second time before returning them to the portable operation. */
|
|
4695
|
-
function binaryFrameBytes(frame) {
|
|
4696
|
-
const bytes = binaryWireFrameBytes(frame);
|
|
4697
|
-
if (frame.content_encoding !== undefined || bytes.length !== Number(frame.bytes)) {
|
|
4698
|
-
throw new Error("wire content byte count does not match its header");
|
|
4699
|
-
}
|
|
4700
|
-
return bytes;
|
|
4701
|
-
}
|
|
4702
|
-
function binaryWireFrameBytes(frame) {
|
|
4703
|
-
if (!(frame.data_bin instanceof Uint8Array)) {
|
|
4704
|
-
throw new Error("wire content response is not a binary frame");
|
|
4705
|
-
}
|
|
4706
|
-
const bytes = Buffer.isBuffer(frame.data_bin)
|
|
4707
|
-
? frame.data_bin
|
|
4708
|
-
: Buffer.from(frame.data_bin.buffer, frame.data_bin.byteOffset, frame.data_bin.byteLength);
|
|
4709
|
-
if (bytes.length !== Number(frame.wire_bytes ?? frame.bytes)) {
|
|
4710
|
-
throw new Error("wire content byte count does not match its header");
|
|
4711
|
-
}
|
|
4712
|
-
return bytes;
|
|
4713
|
-
}
|
|
4714
4690
|
/** Immutable content reads and writes are idempotent. Retry transient wire or
|
|
4715
4691
|
* authority failures; reads additionally tolerate the brief not-found window
|
|
4716
4692
|
* between an immutable object acknowledgement and globally visible R2 state. */
|