canopycms 0.0.61-int.75 → 0.0.62
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/ai/handler.js +1 -1
- package/dist/ai/handler.js.map +1 -1
- package/dist/ai/json-to-markdown.js +1 -2
- package/dist/ai/json-to-markdown.js.map +1 -1
- package/dist/api/__test__/mock-client.d.ts +5 -1
- package/dist/api/__test__/mock-client.d.ts.map +1 -1
- package/dist/api/__test__/mock-client.js +9 -2
- package/dist/api/__test__/mock-client.js.map +1 -1
- package/dist/api/admin-branch-health.d.ts +36 -1
- package/dist/api/admin-branch-health.d.ts.map +1 -1
- package/dist/api/admin-branch-health.js +176 -2
- package/dist/api/admin-branch-health.js.map +1 -1
- package/dist/api/admin.d.ts +8 -1
- package/dist/api/admin.d.ts.map +1 -1
- package/dist/api/admin.js.map +1 -1
- package/dist/api/assets.d.ts.map +1 -1
- package/dist/api/assets.js +36 -4
- package/dist/api/assets.js.map +1 -1
- package/dist/api/branch-status.d.ts.map +1 -1
- package/dist/api/branch-status.js +56 -0
- package/dist/api/branch-status.js.map +1 -1
- package/dist/api/branch-withdraw.d.ts.map +1 -1
- package/dist/api/branch-withdraw.js +14 -3
- package/dist/api/branch-withdraw.js.map +1 -1
- package/dist/api/branch.d.ts +31 -2
- package/dist/api/branch.d.ts.map +1 -1
- package/dist/api/branch.js +268 -15
- package/dist/api/branch.js.map +1 -1
- package/dist/api/client.d.ts +5 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +6 -0
- package/dist/api/client.js.map +1 -1
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +11 -5
- package/dist/api/content.d.ts.map +1 -1
- package/dist/api/content.js +91 -10
- package/dist/api/content.js.map +1 -1
- package/dist/api/entries.d.ts +10 -0
- package/dist/api/entries.d.ts.map +1 -1
- package/dist/api/entries.js +51 -24
- package/dist/api/entries.js.map +1 -1
- package/dist/api/guards.d.ts.map +1 -1
- package/dist/api/guards.js +12 -5
- package/dist/api/guards.js.map +1 -1
- package/dist/api/permissions.d.ts +2 -2
- package/dist/api/permissions.d.ts.map +1 -1
- package/dist/api/permissions.js +51 -3
- package/dist/api/permissions.js.map +1 -1
- package/dist/api/reference-options.d.ts.map +1 -1
- package/dist/api/reference-options.js +10 -17
- package/dist/api/reference-options.js.map +1 -1
- package/dist/api/resolve-references.d.ts.map +1 -1
- package/dist/api/resolve-references.js +3 -1
- package/dist/api/resolve-references.js.map +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/schema.d.ts.map +1 -1
- package/dist/api/schema.js +16 -15
- package/dist/api/schema.js.map +1 -1
- package/dist/api/settings-helpers.d.ts.map +1 -1
- package/dist/api/settings-helpers.js +6 -5
- package/dist/api/settings-helpers.js.map +1 -1
- package/dist/assets/pipeline.d.ts +11 -2
- package/dist/assets/pipeline.d.ts.map +1 -1
- package/dist/assets/pipeline.js +114 -2
- package/dist/assets/pipeline.js.map +1 -1
- package/dist/assets/transform-directives.d.ts +18 -0
- package/dist/assets/transform-directives.d.ts.map +1 -1
- package/dist/assets/transform-directives.js +18 -0
- package/dist/assets/transform-directives.js.map +1 -1
- package/dist/assets/transform.d.ts.map +1 -1
- package/dist/assets/transform.js +20 -16
- package/dist/assets/transform.js.map +1 -1
- package/dist/auth/types.d.ts +29 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.d.ts.map +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/index.js.map +1 -1
- package/dist/authorization/protected-branch.d.ts +75 -0
- package/dist/authorization/protected-branch.d.ts.map +1 -1
- package/dist/authorization/protected-branch.js +39 -0
- package/dist/authorization/protected-branch.js.map +1 -1
- package/dist/branch-health.d.ts +13 -0
- package/dist/branch-health.d.ts.map +1 -1
- package/dist/branch-health.js +23 -0
- package/dist/branch-health.js.map +1 -1
- package/dist/branch-registry.d.ts.map +1 -1
- package/dist/branch-registry.js +5 -1
- package/dist/branch-registry.js.map +1 -1
- package/dist/branch-schema-cache.d.ts.map +1 -1
- package/dist/branch-schema-cache.js +9 -0
- package/dist/branch-schema-cache.js.map +1 -1
- package/dist/build/generate-ai-content.js +1 -1
- package/dist/build/generate-ai-content.js.map +1 -1
- package/dist/cli/cli.js +2215 -1280
- package/dist/cli/generate-ai-content.js +1158 -585
- package/dist/cli/init.d.ts +8 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +459 -152
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/project-detect.d.ts +89 -0
- package/dist/cli/project-detect.d.ts.map +1 -0
- package/dist/cli/project-detect.js +240 -0
- package/dist/cli/project-detect.js.map +1 -0
- package/dist/cli/template-files/Dockerfile.cms.template +30 -7
- package/dist/cli/template-files/canopycms.config.ts.template +7 -0
- package/dist/cli/template-files/cdk-app.ts.template +82 -0
- package/dist/cli/template-files/cdk.json.template +9 -0
- package/dist/cli/template-files/cms-stack.ts.template +130 -0
- package/dist/cli/template-files/deploy-cms.yml.template +125 -13
- package/dist/cli/template-files/dockerignore.template +2 -1
- package/dist/cli/templates.d.ts +26 -2
- package/dist/cli/templates.d.ts.map +1 -1
- package/dist/cli/templates.js +36 -4
- package/dist/cli/templates.js.map +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config/schemas/config.d.ts +2 -3
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +21 -1
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config/schemas/field.d.ts +15 -15
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +0 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts +18 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +36 -12
- package/dist/config/validation.js.map +1 -1
- package/dist/content-id-index.d.ts +97 -5
- package/dist/content-id-index.d.ts.map +1 -1
- package/dist/content-id-index.js +170 -18
- package/dist/content-id-index.js.map +1 -1
- package/dist/content-reader.js +1 -1
- package/dist/content-reader.js.map +1 -1
- package/dist/content-store.d.ts +92 -7
- package/dist/content-store.d.ts.map +1 -1
- package/dist/content-store.js +445 -259
- package/dist/content-store.js.map +1 -1
- package/dist/dev-content-watcher.d.ts.map +1 -1
- package/dist/dev-content-watcher.js +4 -1
- package/dist/dev-content-watcher.js.map +1 -1
- package/dist/editor/BranchManager.d.ts +15 -0
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +61 -15
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/editor/CanopyEditor.d.ts.map +1 -1
- package/dist/editor/CanopyEditor.js +3 -3
- package/dist/editor/CanopyEditor.js.map +1 -1
- package/dist/editor/CanopyEditorPage.d.ts +9 -1
- package/dist/editor/CanopyEditorPage.d.ts.map +1 -1
- package/dist/editor/CanopyEditorPage.js +9 -2
- package/dist/editor/CanopyEditorPage.js.map +1 -1
- package/dist/editor/Editor.d.ts +9 -1
- package/dist/editor/Editor.d.ts.map +1 -1
- package/dist/editor/Editor.js +156 -60
- package/dist/editor/Editor.js.map +1 -1
- package/dist/editor/FormRenderer.d.ts.map +1 -1
- package/dist/editor/FormRenderer.js +12 -0
- package/dist/editor/FormRenderer.js.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.d.ts.map +1 -1
- package/dist/editor/admin/SystemHealthPanel.js +14 -9
- package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
- package/dist/editor/components/EditorHeader.d.ts +6 -0
- package/dist/editor/components/EditorHeader.d.ts.map +1 -1
- package/dist/editor/components/EditorHeader.js +76 -23
- package/dist/editor/components/EditorHeader.js.map +1 -1
- package/dist/editor/components/EntryCreateModal.d.ts +9 -1
- package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
- package/dist/editor/components/EntryCreateModal.js +25 -7
- package/dist/editor/components/EntryCreateModal.js.map +1 -1
- package/dist/editor/context/SWRProvider.d.ts +31 -0
- package/dist/editor/context/SWRProvider.d.ts.map +1 -0
- package/dist/editor/context/SWRProvider.js +12 -0
- package/dist/editor/context/SWRProvider.js.map +1 -0
- package/dist/editor/context/index.d.ts +3 -0
- package/dist/editor/context/index.d.ts.map +1 -1
- package/dist/editor/context/index.js +3 -0
- package/dist/editor/context/index.js.map +1 -1
- package/dist/editor/editor-utils.d.ts.map +1 -1
- package/dist/editor/editor-utils.js +8 -2
- package/dist/editor/editor-utils.js.map +1 -1
- package/dist/editor/fields/DateTimeField.d.ts +46 -0
- package/dist/editor/fields/DateTimeField.d.ts.map +1 -0
- package/dist/editor/fields/DateTimeField.js +66 -0
- package/dist/editor/fields/DateTimeField.js.map +1 -0
- package/dist/editor/fields/ImageField.d.ts.map +1 -1
- package/dist/editor/fields/ImageField.js +27 -3
- package/dist/editor/fields/ImageField.js.map +1 -1
- package/dist/editor/fields/NumberField.d.ts +34 -0
- package/dist/editor/fields/NumberField.d.ts.map +1 -0
- package/dist/editor/fields/NumberField.js +53 -0
- package/dist/editor/fields/NumberField.js.map +1 -0
- package/dist/editor/fields/NumberListField.d.ts +25 -0
- package/dist/editor/fields/NumberListField.d.ts.map +1 -0
- package/dist/editor/fields/NumberListField.js +53 -0
- package/dist/editor/fields/NumberListField.js.map +1 -0
- package/dist/editor/fields/ReferenceField.d.ts.map +1 -1
- package/dist/editor/fields/ReferenceField.js +52 -31
- package/dist/editor/fields/ReferenceField.js.map +1 -1
- package/dist/editor/hooks/__test__/test-utils.d.ts +38 -3
- package/dist/editor/hooks/__test__/test-utils.d.ts.map +1 -1
- package/dist/editor/hooks/__test__/test-utils.js +50 -4
- package/dist/editor/hooks/__test__/test-utils.js.map +1 -1
- package/dist/editor/hooks/index.d.ts +4 -1
- package/dist/editor/hooks/index.d.ts.map +1 -1
- package/dist/editor/hooks/index.js +4 -1
- package/dist/editor/hooks/index.js.map +1 -1
- package/dist/editor/hooks/useBranchManager.d.ts +21 -1
- package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
- package/dist/editor/hooks/useBranchManager.js +183 -58
- package/dist/editor/hooks/useBranchManager.js.map +1 -1
- package/dist/editor/hooks/useBranchesData.d.ts +57 -0
- package/dist/editor/hooks/useBranchesData.d.ts.map +1 -0
- package/dist/editor/hooks/useBranchesData.js +69 -0
- package/dist/editor/hooks/useBranchesData.js.map +1 -0
- package/dist/editor/hooks/useCommentSystem.d.ts.map +1 -1
- package/dist/editor/hooks/useCommentSystem.js +24 -22
- package/dist/editor/hooks/useCommentSystem.js.map +1 -1
- package/dist/editor/hooks/useCommentsData.d.ts +29 -0
- package/dist/editor/hooks/useCommentsData.d.ts.map +1 -0
- package/dist/editor/hooks/useCommentsData.js +40 -0
- package/dist/editor/hooks/useCommentsData.js.map +1 -0
- package/dist/editor/hooks/useDraftManager.d.ts +8 -0
- package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
- package/dist/editor/hooks/useDraftManager.js +290 -36
- package/dist/editor/hooks/useDraftManager.js.map +1 -1
- package/dist/editor/hooks/useEntriesData.d.ts +35 -0
- package/dist/editor/hooks/useEntriesData.d.ts.map +1 -0
- package/dist/editor/hooks/useEntriesData.js +96 -0
- package/dist/editor/hooks/useEntriesData.js.map +1 -0
- package/dist/editor/hooks/useEntryManager.d.ts +29 -15
- package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
- package/dist/editor/hooks/useEntryManager.js +284 -119
- package/dist/editor/hooks/useEntryManager.js.map +1 -1
- package/dist/editor/hooks/usePermissionManager.d.ts +2 -2
- package/dist/editor/hooks/usePermissionManager.d.ts.map +1 -1
- package/dist/editor/hooks/usePermissionManager.js +11 -9
- package/dist/editor/hooks/usePermissionManager.js.map +1 -1
- package/dist/editor/hooks/useReferenceResolution.d.ts.map +1 -1
- package/dist/editor/hooks/useReferenceResolution.js +24 -1
- package/dist/editor/hooks/useReferenceResolution.js.map +1 -1
- package/dist/editor/media/MediaLibraryBody.d.ts.map +1 -1
- package/dist/editor/media/MediaLibraryBody.js +12 -2
- package/dist/editor/media/MediaLibraryBody.js.map +1 -1
- package/dist/editor/permission-manager/GroupSelector.d.ts.map +1 -1
- package/dist/editor/permission-manager/GroupSelector.js +7 -2
- package/dist/editor/permission-manager/GroupSelector.js.map +1 -1
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +3 -3
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts.map +1 -1
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +1 -0
- package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js.map +1 -1
- package/dist/editor/permission-manager/types.d.ts +6 -4
- package/dist/editor/permission-manager/types.d.ts.map +1 -1
- package/dist/editor/test-setup.js +8 -0
- package/dist/editor/test-setup.js.map +1 -1
- package/dist/editor/theme.d.ts +8 -8
- package/dist/git-manager.d.ts +175 -14
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +354 -39
- package/dist/git-manager.js.map +1 -1
- package/dist/github-service.d.ts.map +1 -1
- package/dist/github-service.js +13 -7
- package/dist/github-service.js.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/handler.js +78 -29
- package/dist/http/handler.js.map +1 -1
- package/dist/http/router.d.ts.map +1 -1
- package/dist/http/router.js +40 -3
- package/dist/http/router.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +7 -8
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/deployment-name-fixtures.d.ts +29 -0
- package/dist/operating-mode/deployment-name-fixtures.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name-fixtures.js +55 -0
- package/dist/operating-mode/deployment-name-fixtures.js.map +1 -0
- package/dist/operating-mode/deployment-name.d.ts +37 -0
- package/dist/operating-mode/deployment-name.d.ts.map +1 -0
- package/dist/operating-mode/deployment-name.js +86 -0
- package/dist/operating-mode/deployment-name.js.map +1 -0
- package/dist/operating-mode/index.d.ts +6 -1
- package/dist/operating-mode/index.d.ts.map +1 -1
- package/dist/operating-mode/index.js +10 -1
- package/dist/operating-mode/index.js.map +1 -1
- package/dist/operating-mode/mode-env.d.ts +58 -0
- package/dist/operating-mode/mode-env.d.ts.map +1 -0
- package/dist/operating-mode/mode-env.js +97 -0
- package/dist/operating-mode/mode-env.js.map +1 -0
- package/dist/operating-mode/types.d.ts +13 -3
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/paths/branch-name.d.ts +38 -0
- package/dist/paths/branch-name.d.ts.map +1 -1
- package/dist/paths/branch-name.js +46 -0
- package/dist/paths/branch-name.js.map +1 -1
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.d.ts.map +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/index.js.map +1 -1
- package/dist/reference-resolver.d.ts +6 -2
- package/dist/reference-resolver.d.ts.map +1 -1
- package/dist/reference-resolver.js +8 -1
- package/dist/reference-resolver.js.map +1 -1
- package/dist/resolve-canopy-user.d.ts +34 -0
- package/dist/resolve-canopy-user.d.ts.map +1 -0
- package/dist/resolve-canopy-user.js +55 -0
- package/dist/resolve-canopy-user.js.map +1 -0
- package/dist/schema/meta-loader.d.ts.map +1 -1
- package/dist/schema/meta-loader.js +10 -2
- package/dist/schema/meta-loader.js.map +1 -1
- package/dist/schema/schema-store.d.ts +64 -4
- package/dist/schema/schema-store.d.ts.map +1 -1
- package/dist/schema/schema-store.js +113 -30
- package/dist/schema/schema-store.js.map +1 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -1
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +40 -6
- package/dist/services.js.map +1 -1
- package/dist/settings-workspace.d.ts +27 -2
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +142 -58
- package/dist/settings-workspace.js.map +1 -1
- package/dist/types.d.ts +73 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/content-write-lock.d.ts +127 -0
- package/dist/utils/content-write-lock.d.ts.map +1 -0
- package/dist/utils/content-write-lock.js +170 -0
- package/dist/utils/content-write-lock.js.map +1 -0
- package/dist/utils/debug.d.ts +11 -0
- package/dist/utils/debug.d.ts.map +1 -1
- package/dist/utils/debug.js +12 -1
- package/dist/utils/debug.js.map +1 -1
- package/dist/utils/error.d.ts +1 -1
- package/dist/utils/error.js +1 -1
- package/dist/utils/git.d.ts +54 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +91 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +71 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +85 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/sanitize-href.d.ts +28 -3
- package/dist/utils/sanitize-href.d.ts.map +1 -1
- package/dist/utils/sanitize-href.js +64 -6
- package/dist/utils/sanitize-href.js.map +1 -1
- package/dist/validation/entry-link-validator.d.ts +1 -1
- package/dist/validation/entry-link-validator.js +3 -3
- package/dist/validation/entry-link-validator.js.map +1 -1
- package/dist/validation/entry-type-reference-validator.d.ts +33 -0
- package/dist/validation/entry-type-reference-validator.d.ts.map +1 -0
- package/dist/validation/entry-type-reference-validator.js +110 -0
- package/dist/validation/entry-type-reference-validator.js.map +1 -0
- package/dist/validation/entry-validator.js +1 -1
- package/dist/validation/entry-validator.js.map +1 -1
- package/dist/worker/cms-worker.d.ts +245 -3
- package/dist/worker/cms-worker.d.ts.map +1 -1
- package/dist/worker/cms-worker.js +1084 -253
- package/dist/worker/cms-worker.js.map +1 -1
- package/dist/worker/log.d.ts +61 -0
- package/dist/worker/log.d.ts.map +1 -0
- package/dist/worker/log.js +74 -0
- package/dist/worker/log.js.map +1 -0
- package/package.json +4 -2
|
@@ -2,66 +2,125 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { GitManager } from './git-manager.js';
|
|
4
4
|
import { createDebugLogger } from './utils/debug.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getErrorMessage } from './utils/error.js';
|
|
6
|
+
import { acquireProvisioningLock } from './utils/provisioning-lock.js';
|
|
7
|
+
import { RESERVED_SETTINGS_BRANCH_PREFIX } from './paths/index.js';
|
|
6
8
|
const log = createDebugLogger({ prefix: 'SettingsWorkspace' });
|
|
7
9
|
// In-memory lock to prevent concurrent workspace initialization within the same process.
|
|
8
10
|
// Settings only need one lock (not per-branch like content branches).
|
|
9
11
|
let settingsInitLock = null;
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
+
const SETTINGS_INIT_LOCK_DIR = '.settings-init';
|
|
13
|
+
const SETTINGS_INIT_LOCK_NAME = 'lock';
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* Stale locks (older than LOCK_STALE_MS) are cleaned up automatically.
|
|
15
|
+
* Directory the cross-process init lock is anchored on — a dedicated sibling of
|
|
16
|
+
* the settings root, and NOT the settings root itself.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
+
* Two properties this name has to satisfy:
|
|
19
|
+
*
|
|
20
|
+
* 1. **It must not pre-create the settings root.** `acquireProvisioningLock`
|
|
21
|
+
* mkdir's its target, and `GitManager.initializeWorkspace` clones INTO the
|
|
22
|
+
* settings root — `git clone` refuses a destination that already has content
|
|
23
|
+
* in it. So the lock lives beside the settings root, never inside it.
|
|
24
|
+
* 2. **It must not alias any other proper-lockfile target.** proper-lockfile
|
|
25
|
+
* keys its module-level bookkeeping (refresh timer + release function) by the
|
|
26
|
+
* TARGET path passed to `lock()`, not by `lockfilePath`, so two live locks in
|
|
27
|
+
* one process that share a target clobber each other's registry entry (see
|
|
28
|
+
* `.claude/future-tasks/proper-lockfile-hazards.md`). This target is a
|
|
29
|
+
* dedicated dot-directory, so it collides with none of the existing targets:
|
|
30
|
+
* the content-branches root and the bare remote's parent (provisioning locks
|
|
31
|
+
* — note the latter IS `path.dirname(settingsRoot)`, which is precisely why
|
|
32
|
+
* this cannot be anchored there: settings init calls into
|
|
33
|
+
* `ensureLocalSimulatedRemote` while holding this lock), `<branchRoot>/.canopy-meta`
|
|
34
|
+
* (content-write lock), `<workspaceRoot>/.tasks` (worker task lock), and the
|
|
35
|
+
* individual JSON file paths used by `withOccFileLock`.
|
|
18
36
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
39
|
-
if (ageMs < LOCK_STALE_MS) {
|
|
40
|
-
// Lock is fresh — another process is initializing
|
|
41
|
-
return false;
|
|
37
|
+
export function settingsInitLockTarget(settingsRoot) {
|
|
38
|
+
return path.join(path.dirname(path.resolve(settingsRoot)), SETTINGS_INIT_LOCK_DIR);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether a settings workspace actually holds settings data yet. Used by the
|
|
42
|
+
* rename guard to tell an already-populated workspace (refuse) apart from a
|
|
43
|
+
* clone that was interrupted before its orphan branch was ever created
|
|
44
|
+
* (harmless — let init finish). File names come from the operating-mode
|
|
45
|
+
* strategy so this stays in step with whatever the mode calls them.
|
|
46
|
+
*/
|
|
47
|
+
async function settingsFilesPresent(settingsRoot) {
|
|
48
|
+
const names = ['permissions.json', 'groups.json'];
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
try {
|
|
51
|
+
await fs.access(path.join(settingsRoot, name));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Not present — keep checking the rest.
|
|
42
56
|
}
|
|
43
|
-
// Stale lock — another process likely crashed during init
|
|
44
|
-
log.debug('workspace', 'Removing stale settings init lock', { ageMs });
|
|
45
|
-
await fs.unlink(lockPath).catch(() => { });
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// Lock file vanished between check and stat — try again
|
|
49
57
|
}
|
|
50
|
-
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Rename guard (settings-branch protection). Refuses to boot when an
|
|
62
|
+
* ALREADY-POPULATED settings workspace would be re-orphaned under a different
|
|
63
|
+
* branch name.
|
|
64
|
+
*
|
|
65
|
+
* Trace of what happens without it: GitManager.initializeWorkspace sees an
|
|
66
|
+
* existing .git (no clone happens) and goes straight to
|
|
67
|
+
* createOrphanSettingsBranch(branchName). If that name isn't already known, git
|
|
68
|
+
* runs `checkout --orphan <name>` + `rm -rf .` + an empty `--allow-empty`
|
|
69
|
+
* commit — orphan branches share no history, so this isn't a "migration", it
|
|
70
|
+
* PERMANENTLY WIPES permissions.json/groups.json with nothing to recover from.
|
|
71
|
+
* That almost always means deploymentName / settingsBranch /
|
|
72
|
+
* CANOPYCMS_DEPLOYMENT_NAME changed on a live deployment (see
|
|
73
|
+
* resolveDeploymentName in operating-mode/deployment-name.ts).
|
|
74
|
+
*
|
|
75
|
+
* What this deliberately does NOT refuse, and why it matters:
|
|
76
|
+
* initializeWorkspace clones at the BASE branch and only creates the orphan
|
|
77
|
+
* branch several steps later (addConfig, resolveRemoteUrl, ensureRemote come in
|
|
78
|
+
* between). If that first init is interrupted — a Lambda timeout on a slow EFS
|
|
79
|
+
* clone, an OOM, a spot interruption — it leaves a perfectly valid repo sitting
|
|
80
|
+
* on the base branch with NO settings files in it. Refusing on "branch differs"
|
|
81
|
+
* alone would then brick the deployment permanently on every subsequent boot, to
|
|
82
|
+
* protect data that was never written. So the refusal additionally requires
|
|
83
|
+
* evidence that this really is a populated settings workspace: either it is
|
|
84
|
+
* checked out on some OTHER settings branch, or the settings files are actually
|
|
85
|
+
* on disk.
|
|
86
|
+
*
|
|
87
|
+
* This function is NEVER gated on holding the init lock — see its two call
|
|
88
|
+
* sites in ensureGitWorkspace and the comment there.
|
|
89
|
+
*/
|
|
90
|
+
async function assertSettingsWorkspaceIdentity(options) {
|
|
91
|
+
const repoExists = await GitManager.repoExistsAt(options.settingsRoot);
|
|
92
|
+
if (!repoExists)
|
|
93
|
+
return;
|
|
94
|
+
let currentBranch;
|
|
95
|
+
let readError;
|
|
51
96
|
try {
|
|
52
|
-
const
|
|
53
|
-
await
|
|
54
|
-
await handle.close();
|
|
55
|
-
return true;
|
|
97
|
+
const existing = new GitManager({ repoPath: options.settingsRoot });
|
|
98
|
+
currentBranch = (await existing.status()).current ?? undefined;
|
|
56
99
|
}
|
|
57
|
-
catch (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
catch (err) {
|
|
101
|
+
currentBranch = undefined;
|
|
102
|
+
readError = getErrorMessage(err);
|
|
103
|
+
}
|
|
104
|
+
const onDifferentBranch = currentBranch !== options.branchName;
|
|
105
|
+
const looksLikeSettingsBranch = currentBranch !== undefined && currentBranch.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX);
|
|
106
|
+
const hasSettingsData = await settingsFilesPresent(options.settingsRoot);
|
|
107
|
+
if (onDifferentBranch && (looksLikeSettingsBranch || hasSettingsData)) {
|
|
108
|
+
throw new Error(`CanopyCMS: refusing to initialize settings workspace at ${options.settingsRoot}. ` +
|
|
109
|
+
(currentBranch
|
|
110
|
+
? `It is currently on branch '${currentBranch}', but this deployment resolved ` +
|
|
111
|
+
`settings branch '${options.branchName}'.`
|
|
112
|
+
: `Its current branch could not be read (${readError ?? 'unknown error'}), but it ` +
|
|
113
|
+
`holds settings files, and this deployment resolved settings branch ` +
|
|
114
|
+
`'${options.branchName}'.`) +
|
|
115
|
+
` Proceeding would run \`git checkout --orphan\` + \`rm -rf .\` on this workspace, ` +
|
|
116
|
+
`which PERMANENTLY WIPES permissions.json/groups.json (orphan branches share no ` +
|
|
117
|
+
`history — there is nothing to migrate from). This almost always means ` +
|
|
118
|
+
`deploymentName, settingsBranch, or CANOPYCMS_DEPLOYMENT_NAME changed on a ` +
|
|
119
|
+
`deployment that already has a populated settings workspace. To resolve: restore ` +
|
|
120
|
+
`the previous value so this resolves back to ` +
|
|
121
|
+
`'${currentBranch ?? options.branchName}', or, if starting fresh is genuinely ` +
|
|
122
|
+
`intended, move ${options.settingsRoot} aside manually first.`);
|
|
61
123
|
}
|
|
62
|
-
}
|
|
63
|
-
async function releaseFileLock(lockPath) {
|
|
64
|
-
await fs.unlink(lockPath).catch(() => { });
|
|
65
124
|
}
|
|
66
125
|
/**
|
|
67
126
|
* Manages settings filesystem workspace and git operations.
|
|
@@ -74,9 +133,10 @@ async function releaseFileLock(lockPath) {
|
|
|
74
133
|
* - Interact with the branch registry
|
|
75
134
|
* - Check for special cases (settings are always settings)
|
|
76
135
|
*
|
|
77
|
-
* Uses two layers of locking:
|
|
136
|
+
* Uses two layers of locking, mirroring BranchWorkspaceManager:
|
|
78
137
|
* - In-memory Promise lock for within-process serialization (Lambda request lifecycle)
|
|
79
|
-
* -
|
|
138
|
+
* - `acquireProvisioningLock` (proper-lockfile) for cross-process/cross-host
|
|
139
|
+
* synchronization of the init itself (multiple Lambda containers on EFS)
|
|
80
140
|
*/
|
|
81
141
|
export class SettingsWorkspaceManager {
|
|
82
142
|
constructor(config) {
|
|
@@ -96,13 +156,34 @@ export class SettingsWorkspaceManager {
|
|
|
96
156
|
branchName: options.branchName,
|
|
97
157
|
mode: options.mode,
|
|
98
158
|
});
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
159
|
+
// Rename guard, run LOCK-FREE and unconditionally, before any waiting.
|
|
160
|
+
// A deployment whose settings-branch name no longer matches the
|
|
161
|
+
// workspace on disk is misconfigured, not contended: it must refuse
|
|
162
|
+
// immediately rather than queue behind a live provisioner for what can
|
|
163
|
+
// be minutes. Running it here also keeps the guarantee the guard has
|
|
164
|
+
// always had — no code path reaches initializeWorkspace without it
|
|
165
|
+
// having run in THIS process.
|
|
166
|
+
await assertSettingsWorkspaceIdentity(options);
|
|
167
|
+
// Layer 2: cross-process/cross-host lock around the init itself
|
|
168
|
+
// (proper-lockfile: heartbeat-refreshed while the holder lives, so a
|
|
169
|
+
// slow EFS clone is not mistaken for a crash, and patient retries so a
|
|
170
|
+
// loser WAITS instead of racing into a concurrent clone).
|
|
171
|
+
//
|
|
172
|
+
// This replaced a bespoke O_CREAT|O_EXCL + 30s-mtime scheme whose
|
|
173
|
+
// return value was only ever used to decide whether to release: the
|
|
174
|
+
// loser proceeded into initializeWorkspace anyway, concurrently with
|
|
175
|
+
// the holder, where it could see a half-written .git, classify it
|
|
176
|
+
// corrupt, and `rm -rf` it out from under the in-flight clone.
|
|
177
|
+
const releaseLock = await acquireProvisioningLock(settingsInitLockTarget(options.settingsRoot), SETTINGS_INIT_LOCK_NAME);
|
|
105
178
|
try {
|
|
179
|
+
// Re-run the guard on the now-stable state. If we waited above, the
|
|
180
|
+
// previous holder may have created the workspace (or moved it onto
|
|
181
|
+
// its own settings branch) after we sampled it, and acting on that
|
|
182
|
+
// stale sample is exactly the destructive path the guard exists to
|
|
183
|
+
// stop. Still not gated on any "did I win the race" flag — there is
|
|
184
|
+
// no such flag by design; every process either holds the lock here
|
|
185
|
+
// or has already thrown.
|
|
186
|
+
await assertSettingsWorkspaceIdentity(options);
|
|
106
187
|
// GitManager.initializeWorkspace is idempotent (checks for .git),
|
|
107
188
|
// so it's safe to call even if another process just finished init.
|
|
108
189
|
await GitManager.initializeWorkspace({
|
|
@@ -121,8 +202,11 @@ export class SettingsWorkspaceManager {
|
|
|
121
202
|
});
|
|
122
203
|
}
|
|
123
204
|
finally {
|
|
124
|
-
|
|
125
|
-
await
|
|
205
|
+
try {
|
|
206
|
+
await releaseLock();
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
log.debug('workspace', 'Failed to release settings-init lock', { err });
|
|
126
210
|
}
|
|
127
211
|
}
|
|
128
212
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-workspace.js","sourceRoot":"","sources":["../src/settings-workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;AAG5B,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"settings-workspace.js","sourceRoot":"","sources":["../src/settings-workspace.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { ContentId } from './paths/types';
|
|
2
2
|
export type CanopyUserId = string;
|
|
3
3
|
export type CanopyGroupId = string;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Branch workflow state. Anything other than 'editing' locks content writes --
|
|
6
|
+
* see authorization/protected-branch.ts `writeBlocked`, the single place that
|
|
7
|
+
* rule is expressed.
|
|
8
|
+
*
|
|
9
|
+
* There is deliberately no separate 'locked' state: 'submitted' already means
|
|
10
|
+
* "locked while a reviewer looks at the PR", and request-changes/withdraw are
|
|
11
|
+
* the unlocks. A future admin-freeze feature should reintroduce a status only
|
|
12
|
+
* alongside real semantics (worker rebase skip list, a set/unset endpoint, UI).
|
|
13
|
+
*/
|
|
14
|
+
export type BranchStatus = 'editing' | 'submitted' | 'approved' | 'archived';
|
|
5
15
|
export type SyncStatus = 'synced' | 'pending-sync' | 'sync-failed';
|
|
6
16
|
export type ConflictStatus = 'clean' | 'conflicts-detected';
|
|
7
17
|
export type PullRequestState = 'open' | 'closed' | 'merged';
|
|
@@ -9,7 +19,6 @@ export interface BranchAccessControl {
|
|
|
9
19
|
allowedUsers?: CanopyUserId[];
|
|
10
20
|
allowedGroups?: CanopyGroupId[];
|
|
11
21
|
managerOrAdminAllowed?: boolean;
|
|
12
|
-
adminOnly?: boolean;
|
|
13
22
|
}
|
|
14
23
|
export interface BranchMetadata {
|
|
15
24
|
name: string;
|
|
@@ -52,6 +61,33 @@ export interface BranchMetadata {
|
|
|
52
61
|
firstAt: string;
|
|
53
62
|
lastAt: string;
|
|
54
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Set by the worker's rebase loop when it rewrote history this deployment
|
|
66
|
+
* had ALREADY published: the commit `remote.git` (and therefore GitHub)
|
|
67
|
+
* held for this branch, which the rebase replaced.
|
|
68
|
+
*
|
|
69
|
+
* Used as the `--force-with-lease` expected value on both hops, so a forced
|
|
70
|
+
* push can only ever move a ref off the exact commit our own rebase rewrote
|
|
71
|
+
* away -- never over anyone else's work.
|
|
72
|
+
*
|
|
73
|
+
* Set once per rewrite episode and never advanced while still set: across
|
|
74
|
+
* two rebases before any push lands, GitHub still holds the ORIGINAL
|
|
75
|
+
* commit, so advancing the marker would aim the lease at a commit GitHub
|
|
76
|
+
* never had. Cleared only once GitHub is confirmed to hold something else
|
|
77
|
+
* (CmsWorker.pushBranchToGitHub) -- while it is set, it is the sole trigger
|
|
78
|
+
* for the rebase loop's self-heal pass.
|
|
79
|
+
*/
|
|
80
|
+
historyRewrittenFrom?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Short, sanitized reason the worker's last GitHub sync task failed
|
|
83
|
+
* permanently (set alongside `syncStatus: 'sync-failed'` by
|
|
84
|
+
* CmsWorker.updateBranchMetadataOnFailure) -- e.g. a non-fast-forward push
|
|
85
|
+
* rejection naming the branch. Absent until a task has failed permanently.
|
|
86
|
+
* Cleared on the next successful sync task (CmsWorker.updateBranchMetadata
|
|
87
|
+
* explicitly resets it to undefined) so a stale reason never survives a
|
|
88
|
+
* later successful push.
|
|
89
|
+
*/
|
|
90
|
+
syncFailureReason?: string;
|
|
55
91
|
}
|
|
56
92
|
export interface BranchPaths {
|
|
57
93
|
/** Root where all branches live (e.g., /mnt/efs/site, ~/.canopycms/branches) */
|
|
@@ -88,10 +124,45 @@ export interface WorkerStatusReport {
|
|
|
88
124
|
durationMs: number;
|
|
89
125
|
rebased: string[];
|
|
90
126
|
skippedDirty: string[];
|
|
127
|
+
/**
|
|
128
|
+
* [SYNC-C1] Branches skipped because a content write held the branch's
|
|
129
|
+
* cross-host content-write lock (utils/content-write-lock.ts) -- the
|
|
130
|
+
* worker yields and retries next cycle.
|
|
131
|
+
*
|
|
132
|
+
* Optional for the same reason as `tracked` below: absent from a
|
|
133
|
+
* worker-status.json written by a worker predating that lock. Readers must
|
|
134
|
+
* tolerate its absence.
|
|
135
|
+
*/
|
|
136
|
+
skippedLocked?: string[];
|
|
91
137
|
failed: {
|
|
92
138
|
branch: string;
|
|
93
139
|
error: string;
|
|
94
140
|
}[];
|
|
141
|
+
/**
|
|
142
|
+
* Outcome of reconciling remote.git's `refs/heads/*` against GitHub's
|
|
143
|
+
* fetched tips (worker/cms-worker.ts's `reconcileTrackedBranches`) --
|
|
144
|
+
* the non-destructive replacement for the old fetch refspec that used
|
|
145
|
+
* to write GitHub's refs directly into `refs/heads/*`.
|
|
146
|
+
*/
|
|
147
|
+
/**
|
|
148
|
+
* Optional: absent from a worker-status.json written by a worker that
|
|
149
|
+
* predates the tracked-branch reconcile (PR #168). Readers must tolerate
|
|
150
|
+
* its absence rather than assume every status file has it.
|
|
151
|
+
*/
|
|
152
|
+
tracked?: {
|
|
153
|
+
created: string[];
|
|
154
|
+
fastForwarded: string[];
|
|
155
|
+
ahead: string[];
|
|
156
|
+
diverged: string[];
|
|
157
|
+
/**
|
|
158
|
+
* Optional for the same reason as `tracked` itself: absent from a
|
|
159
|
+
* status file written before the rebase loop began publishing rewritten
|
|
160
|
+
* history. Branches here diverged from GitHub because THIS worker
|
|
161
|
+
* rebased them and the GitHub push is still queued -- expected and
|
|
162
|
+
* self-resolving, unlike `diverged`.
|
|
163
|
+
*/
|
|
164
|
+
rewritten?: string[];
|
|
165
|
+
};
|
|
95
166
|
};
|
|
96
167
|
lastFatalError?: {
|
|
97
168
|
message: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAE9C,MAAM,MAAM,YAAY,GAAG,MAAM,CAAA;AACjC,MAAM,MAAM,aAAa,GAAG,MAAM,CAAA;AAElC,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [SYNC-C1] Cross-host mutual exclusion between the worker's rebase loop and
|
|
3
|
+
* content writes against the same branch working tree.
|
|
4
|
+
*
|
|
5
|
+
* Production runs a Lambda (API) and an EC2 worker over one EFS filesystem.
|
|
6
|
+
* `ContentStore`'s write/delete/rename critical sections were guarded only by
|
|
7
|
+
* the in-process mutex (`utils/async-mutex`), which does not cross that
|
|
8
|
+
* boundary, while `CmsWorker.rebaseActiveBranches()` rewrites the very same
|
|
9
|
+
* working tree. A write landing after `git rebase` started is destroyed two
|
|
10
|
+
* ways -- `git checkout --theirs <file>` overwrites it with the branch's
|
|
11
|
+
* committed version (and the rebase then SUCCEEDS, logging nothing), and
|
|
12
|
+
* `git rebase --abort` hard-resets the tree. Either way the editor already got
|
|
13
|
+
* its 200. This lock is what serializes the two.
|
|
14
|
+
*
|
|
15
|
+
* **Asymmetric by design.** The worker retries every branch on its next sync
|
|
16
|
+
* cycle (~5 minutes) while the editor is a person waiting on a save, so the
|
|
17
|
+
* worker yields and the writer waits:
|
|
18
|
+
*
|
|
19
|
+
* - Worker: {@link tryAcquireContentWriteLock} -- zero retries. On contention
|
|
20
|
+
* it skips that branch this cycle (an extension of the existing
|
|
21
|
+
* skip-dirty-branches behavior) and picks it up next cycle.
|
|
22
|
+
* - Writers: {@link withContentWriteLock} -- a short bounded wait, then a
|
|
23
|
+
* retriable failure the API surfaces as a 409.
|
|
24
|
+
*
|
|
25
|
+
* **Reads never take this lock.** An extra EFS round-trip on every read is not
|
|
26
|
+
* an acceptable price, and a read racing a rebase gets a consistent-enough
|
|
27
|
+
* older or newer file, never a destroyed one.
|
|
28
|
+
*
|
|
29
|
+
* ## Why the lock is anchored on `{branchRoot}/.canopy-meta`
|
|
30
|
+
*
|
|
31
|
+
* proper-lockfile keys its module-level `locks{}` bookkeeping (refresh timer,
|
|
32
|
+
* release function) by the **target path** passed to `lock()`, NOT by
|
|
33
|
+
* `lockfilePath`. The provisioning lock targets `path.dirname(branchRoot)` --
|
|
34
|
+
* the shared content-branches directory -- so anchoring here on that same
|
|
35
|
+
* directory with a different lock NAME would land on the same registry key:
|
|
36
|
+
* one process holding both (a Lambda container provisioning a workspace and
|
|
37
|
+
* writing content) would have the second acquisition clobber the first's
|
|
38
|
+
* bookkeeping. Targeting `{branchRoot}/.canopy-meta` instead gives:
|
|
39
|
+
*
|
|
40
|
+
* - a distinct in-process registry key from the provisioning lock (and from
|
|
41
|
+
* every other branch's content lock -- the provisioning locks already share
|
|
42
|
+
* one key across branches);
|
|
43
|
+
* - a distinct on-disk lock directory, so the server-enforced mkdir cannot
|
|
44
|
+
* collide either; and
|
|
45
|
+
* - no deadlock potential: the two locks are never both required, and the only
|
|
46
|
+
* possible acquisition order (provision, then write) is consistent.
|
|
47
|
+
*
|
|
48
|
+
* `.canopy-meta/` is git-excluded in every branch clone (`ensureGitExclude`),
|
|
49
|
+
* so the lock directory can never dirty the working tree or be swept into
|
|
50
|
+
* `git add .` at publish time.
|
|
51
|
+
*
|
|
52
|
+
* ## Honest caveat (do not oversell this)
|
|
53
|
+
*
|
|
54
|
+
* `proper-lockfile` decides a holder is dead by reading the lock directory's
|
|
55
|
+
* mtime with `fs.stat`, which on EFS is served through the NFS attribute
|
|
56
|
+
* cache. A live holder refreshes every `stale/2`, but a waiter can read a
|
|
57
|
+
* cached mtime and conclude the lock is stale when it is not, taking it over.
|
|
58
|
+
* The failure mode of a bad takeover is exactly today's behavior (two
|
|
59
|
+
* unsynchronized writers), so this is a strict improvement -- not a proof of
|
|
60
|
+
* mutual exclusion. See docs/concurrency.md.
|
|
61
|
+
*/
|
|
62
|
+
/** On-disk name of the lock marker (a directory, created by mkdir). */
|
|
63
|
+
export declare const CONTENT_WRITE_LOCK_NAME = "content-write.lock";
|
|
64
|
+
/**
|
|
65
|
+
* Default bounded wait for a content write.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately short. A rebase holds this lock for a fetch plus a replay plus
|
|
68
|
+
* N conflict rounds of git subprocesses on EFS -- far longer than any wait an
|
|
69
|
+
* interactive save can absorb, and longer than the API's own latency budget.
|
|
70
|
+
* So the wait is not sized to outlast a rebase (nothing reasonable could); it
|
|
71
|
+
* is sized to absorb lock handoff and the short holds of other writers, and to
|
|
72
|
+
* turn everything longer into a fast, explicit "retry" the editor can act on
|
|
73
|
+
* rather than a request that hangs toward the Lambda timeout. The worker
|
|
74
|
+
* revisits the branch next cycle, so the blocked state is transient.
|
|
75
|
+
*
|
|
76
|
+
* This is ALSO the writer-vs-writer budget, not only the rebase one. The lock
|
|
77
|
+
* is per-branch-root, so every write to one branch now serializes behind it
|
|
78
|
+
* where in-process serialization used to be per-entry -- bounded on Lambda
|
|
79
|
+
* (one invocation per container), but real under `next dev` and build-time
|
|
80
|
+
* provisioning across worker processes. The case to watch is a write whose
|
|
81
|
+
* in-lock path triggers a full `idIndex()` rescan of a large tree over EFS:
|
|
82
|
+
* that can plausibly exceed this budget and start 409ing unrelated saves.
|
|
83
|
+
* Making it configurable for prod tuning is tracked in
|
|
84
|
+
* .claude/future-tasks/content-write-lock-tuning-and-granularity.md.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2000;
|
|
87
|
+
/**
|
|
88
|
+
* Thrown when the bounded wait expires with the branch's content lock still
|
|
89
|
+
* held. Retriable -- callers translate this into a 409 with a message that
|
|
90
|
+
* says so.
|
|
91
|
+
*
|
|
92
|
+
* The message says "syncing OR another save" because BOTH produce it. The
|
|
93
|
+
* lock is taken by `write`/`delete`/`renameEntry` and by the admin
|
|
94
|
+
* repair-content-duplicates action as well as by the worker's rebase, so a
|
|
95
|
+
* message naming only the rebase was a confident claim about something that
|
|
96
|
+
* may not be happening. `api/content.ts` routes this error ahead of the
|
|
97
|
+
* generic conflict specifically so the editor sees this wording, which makes
|
|
98
|
+
* the wording load-bearing rather than cosmetic.
|
|
99
|
+
*/
|
|
100
|
+
export declare class ContentWriteLockBusyError extends Error {
|
|
101
|
+
constructor(message?: string);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Acquire the branch's content-write lock WITHOUT waiting.
|
|
105
|
+
*
|
|
106
|
+
* Throws an error with `code === 'ELOCKED'` when a live holder has it. Used by
|
|
107
|
+
* the worker's rebase loop (which skips the branch and retries next cycle) and
|
|
108
|
+
* by tests standing in for a writer.
|
|
109
|
+
*/
|
|
110
|
+
export declare function tryAcquireContentWriteLock(branchRoot: string): Promise<() => Promise<void>>;
|
|
111
|
+
/**
|
|
112
|
+
* Acquire the branch's content-write lock with a short bounded wait.
|
|
113
|
+
*
|
|
114
|
+
* Retries ONLY on genuine contention (`ELOCKED`), the same discipline
|
|
115
|
+
* `withOccFileLock` uses: proper-lockfile's own retry loop retries blindly on
|
|
116
|
+
* any error, which would burn the whole budget re-hitting e.g. ENOENT after
|
|
117
|
+
* the branch directory was deleted out from under the caller.
|
|
118
|
+
*
|
|
119
|
+
* @throws ContentWriteLockBusyError when the budget expires under contention.
|
|
120
|
+
*/
|
|
121
|
+
export declare function acquireContentWriteLock(branchRoot: string, waitMs?: number): Promise<() => Promise<void>>;
|
|
122
|
+
/**
|
|
123
|
+
* Run `fn` holding the branch's content-write lock, releasing it in a
|
|
124
|
+
* `finally` so a throw can never strand it.
|
|
125
|
+
*/
|
|
126
|
+
export declare function withContentWriteLock<T>(branchRoot: string, fn: () => Promise<T>, waitMs?: number): Promise<T>;
|
|
127
|
+
//# sourceMappingURL=content-write-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-write-lock.d.ts","sourceRoot":"","sources":["../../src/utils/content-write-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAUH,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,uBAAuB,CAAA;AAE3D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,kCAAkC,OAAO,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;gBAEhD,OAAO,SAAuI;CAKjJ;AAYD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAE3F;AAED;;;;;;;;;GASG;AACH,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAA2C,GAClD,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAe9B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,MAAM,GAAE,MAA2C,GAClD,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|