canopycms 0.0.55 → 0.0.57
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/api/branch.d.ts +6 -0
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +63 -19
- package/dist/api/branch.js.map +1 -1
- package/dist/api/content.d.ts +2 -2
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +10 -2
- package/dist/api/entries.js.map +1 -1
- package/dist/api/github-sync.d.ts.map +1 -1
- package/dist/api/github-sync.js +28 -7
- package/dist/api/github-sync.js.map +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +8 -1
- package/dist/auth/caching-auth-plugin.d.ts.map +1 -1
- package/dist/auth/caching-auth-plugin.js +6 -1
- package/dist/auth/caching-auth-plugin.js.map +1 -1
- package/dist/auth/plugin.d.ts +19 -10
- package/dist/auth/plugin.d.ts.map +1 -1
- package/dist/auth/plugin.js +12 -10
- package/dist/auth/plugin.js.map +1 -1
- package/dist/branch-metadata.d.ts +71 -6
- package/dist/branch-metadata.d.ts.map +1 -1
- package/dist/branch-metadata.js +132 -116
- package/dist/branch-metadata.js.map +1 -1
- package/dist/branch-registry.d.ts +78 -9
- package/dist/branch-registry.d.ts.map +1 -1
- package/dist/branch-registry.js +155 -29
- package/dist/branch-registry.js.map +1 -1
- package/dist/branch-schema-cache.d.ts +126 -11
- package/dist/branch-schema-cache.d.ts.map +1 -1
- package/dist/branch-schema-cache.js +208 -58
- package/dist/branch-schema-cache.js.map +1 -1
- package/dist/build/generate-ai-content.d.ts.map +1 -1
- package/dist/build/generate-ai-content.js +7 -0
- package/dist/build/generate-ai-content.js.map +1 -1
- package/dist/cli/generate-ai-content.js +2358 -1556
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +16 -8
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +25 -0
- package/dist/cli/migrate.js.map +1 -1
- package/dist/cli/sync.js +5 -5
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +6 -3
- package/dist/cli/templates.js.map +1 -1
- package/dist/comment-store.d.ts +45 -15
- package/dist/comment-store.d.ts.map +1 -1
- package/dist/comment-store.js +80 -101
- package/dist/comment-store.js.map +1 -1
- package/dist/config/helpers.d.ts.map +1 -1
- package/dist/config/helpers.js +3 -0
- package/dist/config/helpers.js.map +1 -1
- package/dist/config/schemas/config.d.ts +3 -4
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +6 -3
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/types.d.ts +6 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config-test.d.ts +5 -3
- package/dist/config-test.d.ts.map +1 -1
- package/dist/config-test.js +9 -1
- package/dist/config-test.js.map +1 -1
- package/dist/content-index-generation.d.ts +40 -4
- package/dist/content-index-generation.d.ts.map +1 -1
- package/dist/content-index-generation.js +70 -73
- package/dist/content-index-generation.js.map +1 -1
- package/dist/content-listing.d.ts +3 -1
- package/dist/content-listing.d.ts.map +1 -1
- package/dist/content-listing.js +1 -0
- package/dist/content-listing.js.map +1 -1
- package/dist/content-store.d.ts +63 -0
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +421 -184
- package/dist/content-store.js.map +1 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +19 -15
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/hooks/useDraftManager.d.ts +7 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +69 -31
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useSchemaManager.d.ts +34 -10
- package/dist/editor/hooks/useSchemaManager.d.ts.map +1 -1
- package/dist/editor/hooks/useSchemaManager.js +20 -24
- package/dist/editor/hooks/useSchemaManager.js.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.d.ts +6 -5
- package/dist/editor/schema-editor/CollectionEditor.d.ts.map +1 -1
- package/dist/editor/schema-editor/CollectionEditor.js +9 -4
- package/dist/editor/schema-editor/CollectionEditor.js.map +1 -1
- package/dist/git-manager.d.ts +15 -10
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +16 -11
- package/dist/git-manager.js.map +1 -1
- package/dist/github-service.d.ts +63 -7
- package/dist/github-service.d.ts.map +1 -1
- package/dist/github-service.js +147 -39
- package/dist/github-service.js.map +1 -1
- package/dist/resource-generation.d.ts +54 -0
- package/dist/resource-generation.d.ts.map +1 -0
- package/dist/resource-generation.js +156 -0
- package/dist/resource-generation.js.map +1 -0
- package/dist/schema/schema-store.d.ts +29 -2
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +47 -8
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/services.js +4 -0
- package/dist/services.js.map +1 -1
- package/dist/static/index.d.ts +32 -0
- package/dist/static/index.d.ts.map +1 -1
- package/dist/static/index.js +90 -0
- package/dist/static/index.js.map +1 -1
- package/dist/sync-core.js +2 -2
- package/dist/sync-core.js.map +1 -1
- package/dist/utils/occ-json-write.d.ts +100 -0
- package/dist/utils/occ-json-write.d.ts.map +1 -0
- package/dist/utils/occ-json-write.js +234 -0
- package/dist/utils/occ-json-write.js.map +1 -0
- package/dist/worker/cms-worker.d.ts +6 -3
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +42 -41
- package/dist/worker/cms-worker.js.map +1 -1
- package/package.json +2 -1
|
@@ -7,12 +7,40 @@ export interface BranchMetadataFile {
|
|
|
7
7
|
branch: BranchMetadata;
|
|
8
8
|
}
|
|
9
9
|
export declare class BranchMetadataConflictError extends Error {
|
|
10
|
-
constructor();
|
|
10
|
+
constructor(message?: string);
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Manages branch.json — branch status and access ACLs, both security-adjacent
|
|
14
|
+
* state — under `.canopy-meta/` in a branch workspace.
|
|
15
|
+
*
|
|
16
|
+
* save() is protected by three layers, outermost to innermost (identical
|
|
17
|
+
* structure to {@link CommentStore}'s withMutation, see comment-store.ts):
|
|
18
|
+
*
|
|
19
|
+
* 1. {@link withLock} - an in-process FIFO mutex keyed by the resolved file
|
|
20
|
+
* path. Serializes concurrent mutators on the SAME process/host
|
|
21
|
+
* deterministically.
|
|
22
|
+
* 2. {@link withOccFileLock} - a server-enforced, cross-process/cross-host
|
|
23
|
+
* lock (proper-lockfile, mkdir-based). This is the actual fix for lost
|
|
24
|
+
* branch-status/ACL updates across two warm Lambda containers (or a
|
|
25
|
+
* Lambda + the EC2 worker) on EFS: rename-based OCC verification alone
|
|
26
|
+
* relies on a read-back that can be served from the writer's own local
|
|
27
|
+
* NFS dentry/attribute cache, so a foreign writer's rename can stay
|
|
28
|
+
* invisible for that cache's window (commonly 3-60s) and both writers
|
|
29
|
+
* conclude they won. A settle delay does not help — the cache window
|
|
30
|
+
* dwarfs any sleep worth paying — only server-enforced mutual exclusion
|
|
31
|
+
* does. Given branch.json carries status + ACLs, silently losing an
|
|
32
|
+
* update here is a correctness/security issue, not just a UX glitch.
|
|
33
|
+
* 3. {@link withOccRetry} around {@link writeOccJsonFile} - version/writeId
|
|
34
|
+
* based optimistic concurrency control. With layers 1-2 in place this is
|
|
35
|
+
* now a defense-in-depth backstop only, not the primary safety mechanism.
|
|
36
|
+
*
|
|
37
|
+
* See `utils/occ-json-write.ts` for full guarantee documentation of layers 2-3.
|
|
38
|
+
*/
|
|
12
39
|
export declare class BranchMetadataFileManager {
|
|
13
40
|
private readonly branchRoot;
|
|
14
41
|
private readonly filePath;
|
|
15
42
|
private readonly baseRoot;
|
|
43
|
+
private readonly settleMs;
|
|
16
44
|
private constructor();
|
|
17
45
|
/**
|
|
18
46
|
* Load branch metadata without requiring baseRoot.
|
|
@@ -23,14 +51,49 @@ export declare class BranchMetadataFileManager {
|
|
|
23
51
|
* Get a BranchMetadataFileManager instance configured for registry invalidation.
|
|
24
52
|
* Use this in API handlers to ensure registry cache is invalidated on updates.
|
|
25
53
|
*/
|
|
26
|
-
static get(branchRoot: string, baseRoot: string
|
|
54
|
+
static get(branchRoot: string, baseRoot: string, options?: {
|
|
55
|
+
settleMs?: number;
|
|
56
|
+
}): BranchMetadataFileManager;
|
|
27
57
|
private load;
|
|
28
58
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
59
|
+
* Write branch.json via the shared OCC helper, with the schemaVersion
|
|
60
|
+
* default applied here (payload shaping stays branch-metadata's concern).
|
|
61
|
+
*
|
|
62
|
+
* Throws the helper's raw {@link OccWriteConflictError} so the surrounding
|
|
63
|
+
* {@link withOccRetry} in save() recognizes and retries it; translation to
|
|
64
|
+
* the public `BranchMetadataConflictError` contract happens at the save()
|
|
65
|
+
* boundary, after retries are exhausted (translating earlier would make
|
|
66
|
+
* withOccRetry's default predicate miss it, since it only recognizes the
|
|
67
|
+
* raw error type).
|
|
68
|
+
*
|
|
69
|
+
* branch-metadata historically writes WITH a trailing newline, unlike
|
|
70
|
+
* comment-store; `trailingNewline: true` preserves that.
|
|
31
71
|
*/
|
|
32
72
|
private write;
|
|
33
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Run a save cycle under the full lock + OCC-retry stack described in the
|
|
75
|
+
* class doc comment. A conflict that survives every retry surfaces as the
|
|
76
|
+
* public `BranchMetadataConflictError`.
|
|
77
|
+
*
|
|
78
|
+
* Guards against a phantom-resurrection race with branch deletion: a
|
|
79
|
+
* caller's branchContext can be resolved BEFORE a concurrent
|
|
80
|
+
* deleteBranchHandler removes the branch directory, but this save() call
|
|
81
|
+
* only reaches here (and its own mkdir({recursive:true}) inside write())
|
|
82
|
+
* AFTER the removal. Without this check, that mkdir would silently
|
|
83
|
+
* recreate `.canopy-meta/` (and this save would recreate branch.json from
|
|
84
|
+
* defaults) inside a directory tree that no longer exists anywhere else --
|
|
85
|
+
* a registry entry with no clone behind it. Checking BEFORE the lock stack
|
|
86
|
+
* (rather than after) fails fast without paying for a lock acquisition on
|
|
87
|
+
* a doomed save.
|
|
88
|
+
*
|
|
89
|
+
* Residual window (accepted): a save that passes this check can still
|
|
90
|
+
* race a `rm` that starts moments later and is still mid-flight when this
|
|
91
|
+
* save's write lands, resurrecting the tree. Closing that fully would
|
|
92
|
+
* need a tombstone OUTSIDE the tree being removed -- the lockfile
|
|
93
|
+
* (`withOccFileLock`) this save takes next lives INSIDE `branchRoot`, so
|
|
94
|
+
* it cannot itself provide a wider guarantee than "the directory existed
|
|
95
|
+
* a moment ago."
|
|
96
|
+
*/
|
|
34
97
|
save(incoming: BranchMetadataUpdate): Promise<BranchMetadataFile>;
|
|
35
98
|
/**
|
|
36
99
|
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
@@ -49,7 +112,9 @@ export interface BranchMetadataUpdate {
|
|
|
49
112
|
* Get a BranchMetadataFileManager instance configured for registry invalidation.
|
|
50
113
|
* Use this in API handlers to ensure registry cache is invalidated on updates.
|
|
51
114
|
*/
|
|
52
|
-
export declare const getBranchMetadataFileManager: (branchRoot: string, baseRoot: string
|
|
115
|
+
export declare const getBranchMetadataFileManager: (branchRoot: string, baseRoot: string, options?: {
|
|
116
|
+
settleMs?: number;
|
|
117
|
+
}) => BranchMetadataFileManager;
|
|
53
118
|
/**
|
|
54
119
|
* Load branch context from metadata file (source of truth).
|
|
55
120
|
* Returns null if the branch doesn't exist.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-metadata.d.ts","sourceRoot":"","sources":["../src/branch-metadata.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"branch-metadata.d.ts","sourceRoot":"","sources":["../src/branch-metadata.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAA;AAG1E,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAarD,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,cAAc,CAAA;CACvB;AAID,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,SAAwD;CAI5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAE7C,OAAO;IAOP;;;OAGG;WACU,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAa7E;;;OAGG;IACH,MAAM,CAAC,GAAG,CACR,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9B,yBAAyB;YAId,IAAI;IAclB;;;;;;;;;;;;;OAaG;YACW,KAAK;IAenB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,IAAI,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAoEvE;;OAEG;YACW,kBAAkB;CAIjC;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,WAAW,CAAC,CAAC,CAAA;CAClE;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACvC,YAAY,MAAM,EAClB,UAAU,MAAM,EAChB,UAAU;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,KAC9B,yBAEF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,SAAS;IAC/C,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,aAAa,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,KAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAiB/B,CAAA"}
|
package/dist/branch-metadata.js
CHANGED
|
@@ -1,24 +1,52 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import fs from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { BranchRegistry } from './branch-registry.js';
|
|
5
4
|
import { resolveBranchPath } from './paths/index.js';
|
|
6
|
-
import {
|
|
5
|
+
import { isNotFoundError } from './utils/error.js';
|
|
7
6
|
import { withLock } from './utils/async-mutex.js';
|
|
7
|
+
import { writeOccJsonFile, withOccRetry, withOccFileLock, OccWriteConflictError, } from './utils/occ-json-write.js';
|
|
8
8
|
const BRANCH_META_DIR = '.canopy-meta';
|
|
9
9
|
const BRANCH_META_FILE = 'branch.json';
|
|
10
10
|
const CURRENT_SCHEMA_VERSION = 1;
|
|
11
11
|
export class BranchMetadataConflictError extends Error {
|
|
12
|
-
constructor() {
|
|
13
|
-
super(
|
|
12
|
+
constructor(message = 'Concurrent modification detected in branch metadata') {
|
|
13
|
+
super(message);
|
|
14
14
|
this.name = 'BranchMetadataConflictError';
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Manages branch.json — branch status and access ACLs, both security-adjacent
|
|
19
|
+
* state — under `.canopy-meta/` in a branch workspace.
|
|
20
|
+
*
|
|
21
|
+
* save() is protected by three layers, outermost to innermost (identical
|
|
22
|
+
* structure to {@link CommentStore}'s withMutation, see comment-store.ts):
|
|
23
|
+
*
|
|
24
|
+
* 1. {@link withLock} - an in-process FIFO mutex keyed by the resolved file
|
|
25
|
+
* path. Serializes concurrent mutators on the SAME process/host
|
|
26
|
+
* deterministically.
|
|
27
|
+
* 2. {@link withOccFileLock} - a server-enforced, cross-process/cross-host
|
|
28
|
+
* lock (proper-lockfile, mkdir-based). This is the actual fix for lost
|
|
29
|
+
* branch-status/ACL updates across two warm Lambda containers (or a
|
|
30
|
+
* Lambda + the EC2 worker) on EFS: rename-based OCC verification alone
|
|
31
|
+
* relies on a read-back that can be served from the writer's own local
|
|
32
|
+
* NFS dentry/attribute cache, so a foreign writer's rename can stay
|
|
33
|
+
* invisible for that cache's window (commonly 3-60s) and both writers
|
|
34
|
+
* conclude they won. A settle delay does not help — the cache window
|
|
35
|
+
* dwarfs any sleep worth paying — only server-enforced mutual exclusion
|
|
36
|
+
* does. Given branch.json carries status + ACLs, silently losing an
|
|
37
|
+
* update here is a correctness/security issue, not just a UX glitch.
|
|
38
|
+
* 3. {@link withOccRetry} around {@link writeOccJsonFile} - version/writeId
|
|
39
|
+
* based optimistic concurrency control. With layers 1-2 in place this is
|
|
40
|
+
* now a defense-in-depth backstop only, not the primary safety mechanism.
|
|
41
|
+
*
|
|
42
|
+
* See `utils/occ-json-write.ts` for full guarantee documentation of layers 2-3.
|
|
43
|
+
*/
|
|
17
44
|
export class BranchMetadataFileManager {
|
|
18
|
-
constructor(branchRoot, baseRoot) {
|
|
45
|
+
constructor(branchRoot, baseRoot, options) {
|
|
19
46
|
this.branchRoot = path.resolve(branchRoot);
|
|
20
47
|
this.filePath = path.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
21
48
|
this.baseRoot = baseRoot;
|
|
49
|
+
this.settleMs = options?.settleMs;
|
|
22
50
|
}
|
|
23
51
|
/**
|
|
24
52
|
* Load branch metadata without requiring baseRoot.
|
|
@@ -41,8 +69,8 @@ export class BranchMetadataFileManager {
|
|
|
41
69
|
* Get a BranchMetadataFileManager instance configured for registry invalidation.
|
|
42
70
|
* Use this in API handlers to ensure registry cache is invalidated on updates.
|
|
43
71
|
*/
|
|
44
|
-
static get(branchRoot, baseRoot) {
|
|
45
|
-
return new BranchMetadataFileManager(branchRoot, baseRoot);
|
|
72
|
+
static get(branchRoot, baseRoot, options) {
|
|
73
|
+
return new BranchMetadataFileManager(branchRoot, baseRoot, options);
|
|
46
74
|
}
|
|
47
75
|
async load() {
|
|
48
76
|
try {
|
|
@@ -59,126 +87,114 @@ export class BranchMetadataFileManager {
|
|
|
59
87
|
}
|
|
60
88
|
}
|
|
61
89
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
90
|
+
* Write branch.json via the shared OCC helper, with the schemaVersion
|
|
91
|
+
* default applied here (payload shaping stays branch-metadata's concern).
|
|
92
|
+
*
|
|
93
|
+
* Throws the helper's raw {@link OccWriteConflictError} so the surrounding
|
|
94
|
+
* {@link withOccRetry} in save() recognizes and retries it; translation to
|
|
95
|
+
* the public `BranchMetadataConflictError` contract happens at the save()
|
|
96
|
+
* boundary, after retries are exhausted (translating earlier would make
|
|
97
|
+
* withOccRetry's default predicate miss it, since it only recognizes the
|
|
98
|
+
* raw error type).
|
|
99
|
+
*
|
|
100
|
+
* branch-metadata historically writes WITH a trailing newline, unlike
|
|
101
|
+
* comment-store; `trailingNewline: true` preserves that.
|
|
64
102
|
*/
|
|
65
103
|
async write(meta, expectedVersion) {
|
|
66
|
-
const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
|
|
67
|
-
const writeId = randomUUID();
|
|
68
104
|
const payload = {
|
|
69
105
|
...meta,
|
|
70
106
|
schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION,
|
|
71
|
-
version: newVersion,
|
|
72
|
-
writeId,
|
|
73
107
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
return writeOccJsonFile(this.filePath, payload, {
|
|
109
|
+
expectedVersion,
|
|
110
|
+
settleMs: this.settleMs,
|
|
111
|
+
trailingNewline: true,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Run a save cycle under the full lock + OCC-retry stack described in the
|
|
116
|
+
* class doc comment. A conflict that survives every retry surfaces as the
|
|
117
|
+
* public `BranchMetadataConflictError`.
|
|
118
|
+
*
|
|
119
|
+
* Guards against a phantom-resurrection race with branch deletion: a
|
|
120
|
+
* caller's branchContext can be resolved BEFORE a concurrent
|
|
121
|
+
* deleteBranchHandler removes the branch directory, but this save() call
|
|
122
|
+
* only reaches here (and its own mkdir({recursive:true}) inside write())
|
|
123
|
+
* AFTER the removal. Without this check, that mkdir would silently
|
|
124
|
+
* recreate `.canopy-meta/` (and this save would recreate branch.json from
|
|
125
|
+
* defaults) inside a directory tree that no longer exists anywhere else --
|
|
126
|
+
* a registry entry with no clone behind it. Checking BEFORE the lock stack
|
|
127
|
+
* (rather than after) fails fast without paying for a lock acquisition on
|
|
128
|
+
* a doomed save.
|
|
129
|
+
*
|
|
130
|
+
* Residual window (accepted): a save that passes this check can still
|
|
131
|
+
* race a `rm` that starts moments later and is still mid-flight when this
|
|
132
|
+
* save's write lands, resurrecting the tree. Closing that fully would
|
|
133
|
+
* need a tombstone OUTSIDE the tree being removed -- the lockfile
|
|
134
|
+
* (`withOccFileLock`) this save takes next lives INSIDE `branchRoot`, so
|
|
135
|
+
* it cannot itself provide a wider guarantee than "the directory existed
|
|
136
|
+
* a moment ago."
|
|
137
|
+
*/
|
|
138
|
+
async save(incoming) {
|
|
100
139
|
try {
|
|
101
|
-
|
|
102
|
-
// rename + verify cycle. Not a correctness guarantee — the post-write
|
|
103
|
-
// writeId verification below is what actually detects cross-process races.
|
|
104
|
-
let currentVersion = null;
|
|
105
|
-
try {
|
|
106
|
-
const current = JSON.parse(await fs.readFile(this.filePath, 'utf-8'));
|
|
107
|
-
currentVersion = current.version ?? 0;
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
currentVersion = null;
|
|
111
|
-
}
|
|
112
|
-
if (currentVersion !== expectedVersion) {
|
|
113
|
-
throw new BranchMetadataConflictError();
|
|
114
|
-
}
|
|
115
|
-
// Atomic rename
|
|
116
|
-
await fs.rename(tempPath, this.filePath);
|
|
117
|
-
// Post-write verification: confirm our write landed (catches cross-process races)
|
|
118
|
-
const afterWrite = JSON.parse(await fs.readFile(this.filePath, 'utf-8'));
|
|
119
|
-
if (afterWrite.writeId !== writeId) {
|
|
120
|
-
throw new BranchMetadataConflictError();
|
|
121
|
-
}
|
|
140
|
+
await fs.stat(this.branchRoot);
|
|
122
141
|
}
|
|
123
142
|
catch (err) {
|
|
124
|
-
|
|
143
|
+
if (isNotFoundError(err)) {
|
|
144
|
+
throw new BranchMetadataConflictError('Branch no longer exists');
|
|
145
|
+
}
|
|
125
146
|
throw err;
|
|
126
147
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
let saved;
|
|
149
|
+
try {
|
|
150
|
+
saved = await withLock(this.filePath, () => withOccFileLock(this.filePath, () => withOccRetry(async () => {
|
|
151
|
+
const { meta: existing, version } = await this.load();
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
const defaults = {
|
|
154
|
+
name: 'unknown',
|
|
155
|
+
status: 'editing',
|
|
156
|
+
access: {},
|
|
157
|
+
createdBy: 'unknown',
|
|
158
|
+
createdAt: now,
|
|
159
|
+
updatedAt: now,
|
|
160
|
+
};
|
|
161
|
+
const merged = {
|
|
162
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
163
|
+
version: version ?? 0,
|
|
164
|
+
branch: {
|
|
165
|
+
...defaults,
|
|
166
|
+
...existing?.branch,
|
|
167
|
+
...incoming.branch,
|
|
168
|
+
access: {
|
|
169
|
+
...existing?.branch?.access,
|
|
170
|
+
...incoming.branch?.access,
|
|
171
|
+
},
|
|
172
|
+
// Immutable after creation
|
|
173
|
+
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
174
|
+
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
175
|
+
// Fork point is recorded once at creation; later saves must not move it
|
|
176
|
+
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const written = await this.write(merged, version);
|
|
180
|
+
merged.version = written.version;
|
|
181
|
+
merged.writeId = written.writeId;
|
|
182
|
+
return merged;
|
|
183
|
+
})));
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
if (err instanceof OccWriteConflictError) {
|
|
187
|
+
throw new BranchMetadataConflictError();
|
|
142
188
|
}
|
|
189
|
+
throw err;
|
|
143
190
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
name: 'unknown',
|
|
152
|
-
status: 'editing',
|
|
153
|
-
access: {},
|
|
154
|
-
createdBy: 'unknown',
|
|
155
|
-
createdAt: now,
|
|
156
|
-
updatedAt: now,
|
|
157
|
-
};
|
|
158
|
-
const merged = {
|
|
159
|
-
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
160
|
-
version: version ?? 0,
|
|
161
|
-
branch: {
|
|
162
|
-
...defaults,
|
|
163
|
-
...existing?.branch,
|
|
164
|
-
...incoming.branch,
|
|
165
|
-
access: {
|
|
166
|
-
...existing?.branch?.access,
|
|
167
|
-
...incoming.branch?.access,
|
|
168
|
-
},
|
|
169
|
-
// Immutable after creation
|
|
170
|
-
createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
|
|
171
|
-
createdAt: existing?.branch.createdAt ?? defaults.createdAt,
|
|
172
|
-
// Fork point is recorded once at creation; later saves must not move it
|
|
173
|
-
baseBranch: existing?.branch.baseBranch ?? incoming.branch?.baseBranch,
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
const written = await this.write(merged, version);
|
|
177
|
-
merged.version = written.version;
|
|
178
|
-
merged.writeId = written.writeId;
|
|
179
|
-
await this.invalidateRegistry();
|
|
180
|
-
return merged;
|
|
181
|
-
}));
|
|
191
|
+
// Registry invalidation AFTER releasing the lockfile: the protocol only
|
|
192
|
+
// requires the bump to land strictly after the branch.json write (it
|
|
193
|
+
// does), and the registry's eager regeneration is O(branch count) fs
|
|
194
|
+
// reads on EFS — holding the server-enforced lock through it would
|
|
195
|
+
// extend every save's critical section for no correctness gain.
|
|
196
|
+
await this.invalidateRegistry();
|
|
197
|
+
return saved;
|
|
182
198
|
}
|
|
183
199
|
/**
|
|
184
200
|
* Invalidates the registry cache so next list() call regenerates from branch.json files.
|
|
@@ -192,8 +208,8 @@ export class BranchMetadataFileManager {
|
|
|
192
208
|
* Get a BranchMetadataFileManager instance configured for registry invalidation.
|
|
193
209
|
* Use this in API handlers to ensure registry cache is invalidated on updates.
|
|
194
210
|
*/
|
|
195
|
-
export const getBranchMetadataFileManager = (branchRoot, baseRoot) => {
|
|
196
|
-
return BranchMetadataFileManager.get(branchRoot, baseRoot);
|
|
211
|
+
export const getBranchMetadataFileManager = (branchRoot, baseRoot, options) => {
|
|
212
|
+
return BranchMetadataFileManager.get(branchRoot, baseRoot, options);
|
|
197
213
|
};
|
|
198
214
|
/**
|
|
199
215
|
* Load branch context from metadata file (source of truth).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-metadata.js","sourceRoot":"","sources":["../src/branch-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"branch-metadata.js","sourceRoot":"","sources":["../src/branch-metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,43 +2,112 @@ import type { BranchContext } from './types';
|
|
|
2
2
|
export interface BranchRegistrySnapshot {
|
|
3
3
|
version: number;
|
|
4
4
|
branches: BranchContext[];
|
|
5
|
+
/**
|
|
6
|
+
* The resource-generation.ts marker token this snapshot was built against, or
|
|
7
|
+
* null if it was built before any bump ever occurred on this root. Compared
|
|
8
|
+
* against the live marker (via isGenerationCurrent) to decide freshness.
|
|
9
|
+
*/
|
|
10
|
+
generation: string | null;
|
|
5
11
|
}
|
|
6
12
|
/**
|
|
7
13
|
* BranchRegistry is a read-only cache for fast branch listing.
|
|
8
14
|
* Individual branch.json files are the source of truth.
|
|
9
15
|
*
|
|
16
|
+
* ## Cross-process freshness (resource-generation.ts marker protocol)
|
|
17
|
+
*
|
|
18
|
+
* Several warm Lambda containers plus the EC2 worker can share one branch-clone
|
|
19
|
+
* root over EFS, each with its own in-process cache of `branches.json`. There is
|
|
20
|
+
* no shared memory and no cross-host file watching, so freshness is coordinated
|
|
21
|
+
* via the generic on-disk generation marker in resource-generation.ts: every
|
|
22
|
+
* snapshot embeds the marker token it was built against, and every read
|
|
23
|
+
* cheaply re-checks the live marker before trusting the cached snapshot. See
|
|
24
|
+
* that module's doc comment for the full protocol and residual staleness
|
|
25
|
+
* windows (A/B/C/E).
|
|
26
|
+
*
|
|
27
|
+
* BranchRegistry is exactly the "durable snapshot consumer" case called out
|
|
28
|
+
* there: a regeneration whose scan is served from stale NFS dentry/attribute
|
|
29
|
+
* caches can record a FRESH token over STALE data (window E), and because the
|
|
30
|
+
* result is written to `branches.json`, that staleness becomes durable and
|
|
31
|
+
* shared with every other host that reads the marker - not just one process's
|
|
32
|
+
* memory. Two mitigations close the practical gap:
|
|
33
|
+
*
|
|
34
|
+
* - invalidate() eager-regenerates immediately after its own bump. The
|
|
35
|
+
* mutating host's own scan is necessarily coherent with the mutation it just
|
|
36
|
+
* made (no NFS round trip was needed to observe its own write), so this
|
|
37
|
+
* closes window (E) for the common case without waiting for some other host
|
|
38
|
+
* to lazily pull the change.
|
|
39
|
+
* - get() implements a suspicious-miss backstop: a branch that "should" exist
|
|
40
|
+
* but is absent from the cached snapshot forces one fresh regeneration
|
|
41
|
+
* (throttled), bounding how long a bad snapshot can hide a real branch.
|
|
42
|
+
*
|
|
43
|
+
* regenerate() also skips persisting a snapshot when the marker read itself
|
|
44
|
+
* fails (`{ ok: false }`, e.g. unreadable for a non-ENOENT reason): a snapshot
|
|
45
|
+
* embedding a token we cannot attribute to "matches this scan" would be
|
|
46
|
+
* indistinguishable from a correctly-attributed one to every future reader, so
|
|
47
|
+
* the safer behavior is to serve the fresh scan without writing it durably and
|
|
48
|
+
* let the next read retry the marker.
|
|
49
|
+
*
|
|
10
50
|
* Design:
|
|
11
|
-
* - list() returns cached
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
51
|
+
* - list() returns the cached snapshot if its embedded generation token still
|
|
52
|
+
* matches the live marker, regenerates from branch.json files otherwise
|
|
53
|
+
* - invalidate() bumps the marker (durable, cross-process) and eager-regenerates
|
|
54
|
+
* - Concurrent regeneration within one process is deduped to a single scan
|
|
55
|
+
* (regenInFlight); across processes, regeneration is still safe since all
|
|
56
|
+
* processes produce identical output from the same branch.json files
|
|
14
57
|
*/
|
|
15
58
|
export declare class BranchRegistry {
|
|
16
59
|
private readonly root;
|
|
17
60
|
private readonly registryPath;
|
|
18
61
|
private readonly stalePath;
|
|
19
62
|
private readonly tempPath;
|
|
63
|
+
/** Shared in-flight scan so concurrent list()/get() callers on one instance await a single regeneration. */
|
|
64
|
+
private regenInFlight;
|
|
65
|
+
/** Throttle clock for the get() suspicious-miss backstop. */
|
|
66
|
+
private lastForcedRefreshMs;
|
|
20
67
|
constructor(root: string);
|
|
21
68
|
/**
|
|
22
|
-
* Returns all branches. Uses
|
|
69
|
+
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
70
|
+
* token matches the live marker, regenerates otherwise.
|
|
23
71
|
*/
|
|
24
72
|
list(): Promise<BranchContext[]>;
|
|
25
73
|
/**
|
|
26
74
|
* Returns a single branch by name. Uses cache if available.
|
|
75
|
+
*
|
|
76
|
+
* Suspicious-miss backstop: if `name` isn't in the (possibly cached) list,
|
|
77
|
+
* that's a signal the snapshot may be stale in the specific direction where
|
|
78
|
+
* a branch that now exists is missing from it - force one fresh
|
|
79
|
+
* regeneration and re-search before giving up, throttled per instance so a
|
|
80
|
+
* genuinely-absent branch costs at most one extra scan per throttle window.
|
|
81
|
+
* This bounds durable window-E staleness for the "branch exists but
|
|
82
|
+
* snapshot predates it" direction; see the class doc comment.
|
|
27
83
|
*/
|
|
28
84
|
get(name: string): Promise<BranchContext | undefined>;
|
|
85
|
+
private shouldForceRefresh;
|
|
29
86
|
/**
|
|
30
|
-
* Marks the cache as stale
|
|
31
|
-
*
|
|
87
|
+
* Marks the cache as stale for every process sharing this root by bumping
|
|
88
|
+
* the cross-process generation marker, then eager-regenerates on this host
|
|
89
|
+
* (see class doc comment for why). The bump must succeed: a swallowed
|
|
90
|
+
* failure here would leave the registry stale indefinitely with no
|
|
91
|
+
* bounding backstop, unlike a failed eager regen below, which only forgoes
|
|
92
|
+
* closing window (E) early - the bump alone already restored correctness
|
|
93
|
+
* for every future reader. A failed eager regen therefore must not fail the
|
|
94
|
+
* caller's save/delete.
|
|
32
95
|
*/
|
|
33
96
|
invalidate(): Promise<void>;
|
|
34
97
|
/**
|
|
35
|
-
* Scans branch directories and rebuilds the cache
|
|
36
|
-
*
|
|
98
|
+
* Scans branch directories and rebuilds the cache, deduping concurrent
|
|
99
|
+
* callers on this instance to a single underlying scan. Never loops waiting
|
|
100
|
+
* for the snapshot's embedded token to match the marker - under a bump
|
|
101
|
+
* storm that could livelock; a caller that wants the very latest state
|
|
102
|
+
* after a concurrent bump should call list() again after this resolves.
|
|
37
103
|
*/
|
|
38
104
|
private regenerate;
|
|
105
|
+
private performRegenerate;
|
|
39
106
|
/**
|
|
40
107
|
* Scans the root directory for branch subdirectories with valid branch.json files.
|
|
108
|
+
* Protected (rather than exported as a test hook) so tests can subclass and
|
|
109
|
+
* override to simulate cross-process interleavings.
|
|
41
110
|
*/
|
|
42
|
-
|
|
111
|
+
protected scanBranchDirectories(): Promise<BranchContext[]>;
|
|
43
112
|
}
|
|
44
113
|
//# sourceMappingURL=branch-registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"branch-registry.d.ts","sourceRoot":"","sources":["../src/branch-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"branch-registry.d.ts","sourceRoot":"","sources":["../src/branch-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AA4B5C,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB;;;;OAIG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IAEjC,4GAA4G;IAC5G,OAAO,CAAC,aAAa,CAAwC;IAE7D,6DAA6D;IAC7D,OAAO,CAAC,mBAAmB,CAAI;gBAEnB,IAAI,EAAE,MAAM;IAOxB;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IA2BtC;;;;;;;;;;OAUG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAW3D,OAAO,CAAC,kBAAkB;IAO1B;;;;;;;;;OASG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBjC;;;;;;OAMG;YACW,UAAU;YAYV,iBAAiB;IA4C/B;;;;OAIG;cACa,qBAAqB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;CAiClE"}
|