@amalgm/shell 0.1.136 → 0.1.138

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 CHANGED
@@ -291,9 +291,13 @@ shell only assembles.
291
291
  destinations, and an interrupted Add may retain them only with disposable
292
292
  chunk evidence beside the ground database. Resume rereads and re-hashes
293
293
  every retained chunk against the pinned manifest, downloads every missing
294
- or corrupt chunk, proves the ordered whole-artifact hash, and fsyncs each
295
- completed file before rows or reveal are trusted. Links and repository
296
- transports keep their type-specific cache and adapter rails.
294
+ or corrupt chunk, and proves the ordered whole-artifact hash. Durability
295
+ is one explicit barrier over the whole staged tree — every file,
296
+ duplicate, and repository — that must succeed before entity rows are
297
+ committed or ground is revealed; no staged file is fsynced on its own. A
298
+ failed barrier leaves the intent retained, no rows committed, no visible
299
+ destination, and staging re-proven as untrusted on retry. Links and
300
+ repository transports keep their type-specific cache and adapter rails.
297
301
  One recovered Add intent that cannot currently be satisfied remains
298
302
  durable and reports its exact failure, but it cannot hold unrelated machine
299
303
  readiness or other Add responsibilities hostage.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The set-level durability law for Files Add.
3
+ *
4
+ * Staged content is verified while it streams and is never trusted until it
5
+ * is re-proven, so no staged file needs to be durable before the next one is
6
+ * written. What must hold is one barrier: after every file, duplicate, and
7
+ * repository has been materialized, the whole staged tree is durable before
8
+ * entity rows are committed or ground is revealed. A failed barrier fails the
9
+ * Add with its intent retained — nothing committed, nothing visible — and the
10
+ * retry re-proves the staging as untrusted substrate.
11
+ *
12
+ * Linux settles the filesystem holding the staging root in one syncfs(2) —
13
+ * coreutils `sync -f` — so twelve thousand small files cost one journal
14
+ * flush rather than one commit each serialized through the journal. Node
15
+ * exposes no syncfs, so that is one child process. Elsewhere every regular
16
+ * file and directory under the root is flushed through its own handle:
17
+ * libuv's fsync is F_FULLFSYNC on macOS and FlushFileBuffers on Windows,
18
+ * where NTFS's own log carries the directory entries with their files. A
19
+ * directory-only fsync never guarantees file contents and is never used.
20
+ */
21
+ export type DurabilityBarrier = (root: string) => Promise<void>;
22
+ export declare const durabilityBarrier: DurabilityBarrier;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The set-level durability law for Files Add.
3
+ *
4
+ * Staged content is verified while it streams and is never trusted until it
5
+ * is re-proven, so no staged file needs to be durable before the next one is
6
+ * written. What must hold is one barrier: after every file, duplicate, and
7
+ * repository has been materialized, the whole staged tree is durable before
8
+ * entity rows are committed or ground is revealed. A failed barrier fails the
9
+ * Add with its intent retained — nothing committed, nothing visible — and the
10
+ * retry re-proves the staging as untrusted substrate.
11
+ *
12
+ * Linux settles the filesystem holding the staging root in one syncfs(2) —
13
+ * coreutils `sync -f` — so twelve thousand small files cost one journal
14
+ * flush rather than one commit each serialized through the journal. Node
15
+ * exposes no syncfs, so that is one child process. Elsewhere every regular
16
+ * file and directory under the root is flushed through its own handle:
17
+ * libuv's fsync is F_FULLFSYNC on macOS and FlushFileBuffers on Windows,
18
+ * where NTFS's own log carries the directory entries with their files. A
19
+ * directory-only fsync never guarantees file contents and is never used.
20
+ */
21
+ import { execFile } from "node:child_process";
22
+ import { constants } from "node:fs";
23
+ import { lstat, open, readdir } from "node:fs/promises";
24
+ import { join } from "node:path";
25
+ import { promisify } from "node:util";
26
+ import { boundedForEach } from "./bounded-for-each.js";
27
+ const execFileAsync = promisify(execFile);
28
+ /** Handles in flight while a tree is flushed entry by entry. */
29
+ const FLUSH_CONCURRENCY = 16;
30
+ async function syncFilesystem(root) {
31
+ await execFileAsync("sync", ["-f", root]);
32
+ }
33
+ async function flushEntry(path) {
34
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
35
+ try {
36
+ await handle.sync();
37
+ }
38
+ finally {
39
+ await handle.close();
40
+ }
41
+ }
42
+ async function syncTree(root) {
43
+ const files = [];
44
+ const directories = [root];
45
+ for (let index = 0; index < directories.length; index += 1) {
46
+ const directory = directories[index];
47
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
48
+ const path = join(directory, entry.name);
49
+ if (entry.isDirectory())
50
+ directories.push(path);
51
+ else if (entry.isFile())
52
+ files.push(path);
53
+ }
54
+ }
55
+ const flushed = process.platform === "win32" ? files : [...files, ...directories];
56
+ await boundedForEach(flushed, FLUSH_CONCURRENCY, flushEntry);
57
+ }
58
+ const platformBarrier = process.platform === "linux" ? syncFilesystem : syncTree;
59
+ export const durabilityBarrier = async (root) => {
60
+ try {
61
+ const stats = await lstat(root);
62
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
63
+ throw new Error("staging root is not a physical directory");
64
+ }
65
+ await platformBarrier(root);
66
+ }
67
+ catch (error) {
68
+ const detail = error instanceof Error ? error.message : String(error);
69
+ throw new Error(`durability barrier failed for ${root}: ${detail}`, { cause: error });
70
+ }
71
+ };
72
+ //# sourceMappingURL=durability-barrier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durability-barrier.js","sourceRoot":"","sources":["../src/durability-barrier.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { type EntityType, type FilesAddPorts, type FilesRegisterPorts, type User
3
3
  import Database from "better-sqlite3";
4
4
  import { type FilesStageObserver } from "./files-observability.js";
5
5
  import { type EntityApplyStageEvidence } from "./entity-apply-host.js";
6
+ import { type DurabilityBarrier } from "./durability-barrier.js";
6
7
  import { type OpenNativeWatch, type WatchCoverageEvidence, type WatchHealth } from "./watching/index.js";
7
8
  interface GroundRow {
8
9
  readonly resourceId: string;
@@ -73,6 +74,9 @@ export interface UserGroundHostOptions {
73
74
  readonly onWatchHealth?: (health: WatchHealth) => void;
74
75
  /** Host injection used by platform certification and failure examinations. */
75
76
  readonly openWatch?: OpenNativeWatch;
77
+ /** The one durability barrier Add takes over its staged tree before rows or
78
+ * reveal; injected only by failure examinations. */
79
+ readonly durabilityBarrier?: DurabilityBarrier;
76
80
  /** Root-scoped Detect evidence for product diagnostics and examinations. */
77
81
  readonly onDetectScan?: (evidence: DetectScanEvidence) => void;
78
82
  /** Transient Apply progress; durable responsibility remains in SQLite. */
@@ -15,6 +15,7 @@ import { ContentCacheDownloadSet, captureContentFile, contentBlockCollector, has
15
15
  import { RegisterUploadFusion } from "./register-upload-fusion.js";
16
16
  import { StagedContentSet, addStagingEvidenceDir, clearAddStagingEvidence, } from "./workspace-add-staging.js";
17
17
  import { boundedForEach } from "./bounded-for-each.js";
18
+ import { durabilityBarrier as platformDurabilityBarrier, } from "./durability-barrier.js";
18
19
  import { DirectContentPlane } from "./content-direct.js";
19
20
  import { encodeContentWireBytes } from "./content-wire-codec.js";
20
21
  import { TransferLegs } from "./transfer-instrumentation.js";
@@ -44,6 +45,12 @@ const CARGO_SEGMENT_LOAD_CONCURRENCY = 8;
44
45
  * few broad ranged reads. Memory is bounded by the byte laws below, never by
45
46
  * this count. */
46
47
  const DIRECT_OBJECT_DOWNLOAD_CONCURRENCY = CONTENT_REFS_BATCH_OBJECTS;
48
+ /** Independent destination writers. Pack reads are intentionally few, but
49
+ * one pack may contain thousands of unrelated files; those files must not
50
+ * inherit the pack's request count as their filesystem concurrency. Sixteen
51
+ * handles are bounded on a 1 GiB runtime and enough to hide metadata latency
52
+ * behind the volume's byte throughput. */
53
+ const DIRECT_CONTENT_STORE_CONCURRENCY = 16;
47
54
  // Opening one protected object transiently retains the fetched envelope,
48
55
  // WebCrypto's ciphertext-with-tag input, and the plaintext. Bound plaintext
49
56
  // to two full blocks so those three representations stay below 96 MiB even
@@ -388,6 +395,7 @@ export class UserGroundHost {
388
395
  cloudHead: input.cloudHead,
389
396
  deliveryWatermark: input.deliveryWatermark,
390
397
  readContent: (artifacts, staged) => this.downloadContentSet(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: input.workspace.uuid }, staged),
398
+ durabilityBarrier: this.options.durabilityBarrier ?? platformDurabilityBarrier,
391
399
  adoptMaterialization: (rootUUID) => {
392
400
  const settled = this.adoptCloudDeliveryBaseline(identity, input.deliveryWatermark, rootUUID);
393
401
  this.applyRail?.recordsAvailable();
@@ -517,6 +525,7 @@ export class UserGroundHost {
517
525
  cloudHead: intent.cloudHead,
518
526
  deliveryWatermark: intent.deliveryWatermark,
519
527
  readContent: (artifacts, staged) => this.downloadContentSet(intent.resourceId, this.cacheDir(identity), artifacts.map((artifact) => ({ artifact })), 10, { journey: "add", workspaceId: intent.workspaceId }, staged),
528
+ durabilityBarrier: this.options.durabilityBarrier ?? platformDurabilityBarrier,
520
529
  adoptMaterialization: (rootUUID) => {
521
530
  this.adoptCloudDeliveryBaseline(identity, intent.deliveryWatermark, rootUUID);
522
531
  this.applyRail?.recordsAvailable();
@@ -1962,6 +1971,7 @@ export class UserGroundHost {
1962
1971
  },
1963
1972
  }, {
1964
1973
  concurrency: DIRECT_OBJECT_DOWNLOAD_CONCURRENCY,
1974
+ storeConcurrency: DIRECT_CONTENT_STORE_CONCURRENCY,
1965
1975
  maxInFlightBytes: DIRECT_OBJECT_DOWNLOAD_MAX_IN_FLIGHT_BYTES,
1966
1976
  });
1967
1977
  const content = new Map();
@@ -6073,7 +6083,7 @@ function installCloudWorkspaceReferences(input) {
6073
6083
  return references;
6074
6084
  }
6075
6085
  async function installCloudWorkspace(input) {
6076
- const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, cloudHead, deliveryWatermark, readContent, onStage, } = input;
6086
+ const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, cloudHead, deliveryWatermark, readContent, durabilityBarrier, onStage, } = input;
6077
6087
  const existing = readGroundClosureRows(database, workspace.uuid);
6078
6088
  const existingBoundary = existing.find((row) => row.uuid === workspace.uuid);
6079
6089
  const pending = readWorkspaceAddIntent(database, workspace.uuid);
@@ -6229,11 +6239,19 @@ async function installCloudWorkspace(input) {
6229
6239
  ? join(intent.destinationPath, ...row.relativePath.split("/"))
6230
6240
  : intent.destinationPath,
6231
6241
  }));
6242
+ const graphMs = Math.round(performance.now() - graphStarted);
6243
+ // Every staged byte was proven as it streamed; none of it counts as durable
6244
+ // until the whole tree — files, duplicates, repositories — passes one
6245
+ // barrier. Rows and reveal follow that barrier, never precede it; a failed
6246
+ // barrier leaves the intent retained and the staging untrusted.
6247
+ const barrierStarted = performance.now();
6248
+ await durabilityBarrier(intent.stagingPath);
6232
6249
  // The milestone's duration spans the whole materialization; its legs say
6233
6250
  // where that wall time went after the content set itself.
6234
6251
  await onStage?.("materialization-verified", {
6235
6252
  ...materialized.timing,
6236
- graphMs: Math.round(performance.now() - graphStarted),
6253
+ graphMs,
6254
+ durabilityBarrierMs: Math.round(performance.now() - barrierStarted),
6237
6255
  repositories: materialized.repositories.length,
6238
6256
  });
6239
6257
  persistRows(database, identity, intent.resourceId, intent.workspaceId, rows);