@amalgm/shell 0.1.137 → 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 +7 -3
- package/dist/durability-barrier.d.ts +22 -0
- package/dist/durability-barrier.js +72 -0
- package/dist/durability-barrier.js.map +1 -0
- package/dist/user-ground-host.d.ts +4 -0
- package/dist/user-ground-host.js +13 -2
- package/dist/user-ground-host.js.map +1 -1
- package/dist/workspace-add-staging.d.ts +14 -9
- package/dist/workspace-add-staging.js +14 -23
- package/dist/workspace-add-staging.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
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
|
|
295
|
-
|
|
296
|
-
|
|
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. */
|
package/dist/user-ground-host.js
CHANGED
|
@@ -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";
|
|
@@ -394,6 +395,7 @@ export class UserGroundHost {
|
|
|
394
395
|
cloudHead: input.cloudHead,
|
|
395
396
|
deliveryWatermark: input.deliveryWatermark,
|
|
396
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,
|
|
397
399
|
adoptMaterialization: (rootUUID) => {
|
|
398
400
|
const settled = this.adoptCloudDeliveryBaseline(identity, input.deliveryWatermark, rootUUID);
|
|
399
401
|
this.applyRail?.recordsAvailable();
|
|
@@ -523,6 +525,7 @@ export class UserGroundHost {
|
|
|
523
525
|
cloudHead: intent.cloudHead,
|
|
524
526
|
deliveryWatermark: intent.deliveryWatermark,
|
|
525
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,
|
|
526
529
|
adoptMaterialization: (rootUUID) => {
|
|
527
530
|
this.adoptCloudDeliveryBaseline(identity, intent.deliveryWatermark, rootUUID);
|
|
528
531
|
this.applyRail?.recordsAvailable();
|
|
@@ -6080,7 +6083,7 @@ function installCloudWorkspaceReferences(input) {
|
|
|
6080
6083
|
return references;
|
|
6081
6084
|
}
|
|
6082
6085
|
async function installCloudWorkspace(input) {
|
|
6083
|
-
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;
|
|
6084
6087
|
const existing = readGroundClosureRows(database, workspace.uuid);
|
|
6085
6088
|
const existingBoundary = existing.find((row) => row.uuid === workspace.uuid);
|
|
6086
6089
|
const pending = readWorkspaceAddIntent(database, workspace.uuid);
|
|
@@ -6236,11 +6239,19 @@ async function installCloudWorkspace(input) {
|
|
|
6236
6239
|
? join(intent.destinationPath, ...row.relativePath.split("/"))
|
|
6237
6240
|
: intent.destinationPath,
|
|
6238
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);
|
|
6239
6249
|
// The milestone's duration spans the whole materialization; its legs say
|
|
6240
6250
|
// where that wall time went after the content set itself.
|
|
6241
6251
|
await onStage?.("materialization-verified", {
|
|
6242
6252
|
...materialized.timing,
|
|
6243
|
-
graphMs
|
|
6253
|
+
graphMs,
|
|
6254
|
+
durabilityBarrierMs: Math.round(performance.now() - barrierStarted),
|
|
6244
6255
|
repositories: materialized.repositories.length,
|
|
6245
6256
|
});
|
|
6246
6257
|
persistRows(database, identity, intent.resourceId, intent.workspaceId, rows);
|