amalgm 0.1.245 → 0.1.246
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/README.md +12 -0
- package/lib/cli.js +146 -8
- package/lib/layout.js +89 -4
- package/lib/shared-realtime-tunnel.js +384 -43
- package/lib/supervisor.js +0 -3
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
- package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
- package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
- package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
- package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
- package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
- package/runtime/scripts/amalgm-mcp/index.js +4 -0
- package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
- package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
- package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
- package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
- package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
- package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
- package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
- package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
- package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
- package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
- package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
- package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
- package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
- package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
- package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
- package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
- package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
- package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
- package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
- package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
- package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
- package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
- package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
- package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
- package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
- package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
- package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
- package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
- package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
- package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
- package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
- package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
- package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
- package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
- package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
- package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
- package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
- package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
- package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
- package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
- package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
- package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
- package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
- package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
- package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
|
@@ -57,13 +57,7 @@ function isPrivateFile(name) {
|
|
|
57
57
|
// enrolled as leaf nodes and are never followed by the scanner.
|
|
58
58
|
function shouldEnroll(relativePath) {
|
|
59
59
|
const parts = partsFor(relativePath);
|
|
60
|
-
if (
|
|
61
|
-
parts.length === 0
|
|
62
|
-
|| parts.some((part) => (
|
|
63
|
-
EXCLUDED_DIRECTORIES.has(part)
|
|
64
|
-
|| part.startsWith('.amalgm-tree-apply-')
|
|
65
|
-
))
|
|
66
|
-
) return false;
|
|
60
|
+
if (parts.length === 0 || parts.some((part) => EXCLUDED_DIRECTORIES.has(part))) return false;
|
|
67
61
|
const name = parts.at(-1);
|
|
68
62
|
if (EXCLUDED_FILES.has(name) || isPrivateFile(name)) return false;
|
|
69
63
|
return !name.startsWith('.#') && !name.endsWith('~') && !/\.(?:swp|swo)$/i.test(name);
|
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { AMALGM_DIR } = require('../../config');
|
|
6
6
|
const { openLocalDb } = require('../../state/db');
|
|
7
|
+
const { descendantsInclude } = require('./reducer');
|
|
7
8
|
|
|
8
9
|
function invalid(message, statusCode = 400) {
|
|
9
10
|
return Object.assign(new Error(message), { statusCode });
|
|
@@ -22,7 +23,6 @@ function rowToNode(row) {
|
|
|
22
23
|
deviceId: row.device_id || null,
|
|
23
24
|
inode: row.inode || null,
|
|
24
25
|
trashedAt: row.trashed_at || null,
|
|
25
|
-
trashComponentId: row.trash_component_id || null,
|
|
26
26
|
purgedAt: row.purged_at || null,
|
|
27
27
|
missingAt: row.missing_at || null,
|
|
28
28
|
createdAt: row.created_at,
|
|
@@ -53,24 +53,58 @@ function relativePathForNode(nodes, nodeId, memo = new Map()) {
|
|
|
53
53
|
return relativePath;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function trashPath(workspaceId, nodeId
|
|
57
|
-
if (workspacePath) {
|
|
58
|
-
// Tree trash participates in the same rename-only WAL as the visible
|
|
59
|
-
// workspace. Keep it beside that workspace so arbitrary registered
|
|
60
|
-
// folders (including external volumes) never turn trash/restore into an
|
|
61
|
-
// impossible cross-device rename.
|
|
62
|
-
return path.join(
|
|
63
|
-
path.dirname(path.resolve(workspacePath)),
|
|
64
|
-
`.amalgm-tree-trash-${workspaceId}`,
|
|
65
|
-
nodeId,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
// Compatibility for callers that only need the historical locator. New
|
|
69
|
-
// materialization plans always pass workspacePath and persist the exact
|
|
70
|
-
// same-filesystem location in their action.
|
|
56
|
+
function trashPath(workspaceId, nodeId) {
|
|
71
57
|
return path.join(AMALGM_DIR, 'trash', 'workspaces', workspaceId, nodeId);
|
|
72
58
|
}
|
|
73
59
|
|
|
60
|
+
function requireMissing(targetPath, message) {
|
|
61
|
+
try {
|
|
62
|
+
fs.lstatSync(targetPath);
|
|
63
|
+
throw invalid(message, 409);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function materializeOperation(workspace, before, after, operation) {
|
|
70
|
+
const oldNode = before.get(operation.nodeId) || null;
|
|
71
|
+
const newNode = after.get(operation.nodeId) || null;
|
|
72
|
+
const oldRelative = oldNode ? relativePathForNode(before, oldNode.id) : null;
|
|
73
|
+
const newRelative = newNode ? relativePathForNode(after, newNode.id) : null;
|
|
74
|
+
const oldPath = oldRelative ? path.join(workspace.path, ...oldRelative.split('/')) : null;
|
|
75
|
+
const newPath = newRelative ? path.join(workspace.path, ...newRelative.split('/')) : null;
|
|
76
|
+
|
|
77
|
+
if (operation.kind === 'tree.create') {
|
|
78
|
+
requireMissing(newPath, 'create destination already exists');
|
|
79
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
80
|
+
if (operation.nodeKind === 'folder') fs.mkdirSync(newPath);
|
|
81
|
+
else if (operation.nodeKind === 'symlink') fs.symlinkSync(operation.symlinkTarget, newPath);
|
|
82
|
+
else fs.writeFileSync(newPath, '');
|
|
83
|
+
} else if (operation.kind === 'tree.rename' || operation.kind === 'tree.move') {
|
|
84
|
+
requireMissing(newPath, 'move destination already exists');
|
|
85
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
86
|
+
fs.renameSync(oldPath, newPath);
|
|
87
|
+
} else if (operation.kind === 'tree.trash') {
|
|
88
|
+
const destination = trashPath(workspace.id, operation.nodeId);
|
|
89
|
+
requireMissing(destination, 'trash destination already exists');
|
|
90
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
91
|
+
fs.renameSync(oldPath, destination);
|
|
92
|
+
} else if (operation.kind === 'tree.restore') {
|
|
93
|
+
const source = trashPath(workspace.id, operation.nodeId);
|
|
94
|
+
requireMissing(newPath, 'restore destination already exists');
|
|
95
|
+
fs.mkdirSync(path.dirname(newPath), { recursive: true });
|
|
96
|
+
fs.renameSync(source, newPath);
|
|
97
|
+
} else if (operation.kind === 'tree.purge') {
|
|
98
|
+
for (const node of before.values()) {
|
|
99
|
+
if (node.id === operation.nodeId || descendantsInclude(before, operation.nodeId, node.id)) {
|
|
100
|
+
fs.rmSync(trashPath(workspace.id, node.id), { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { oldPath, newPath };
|
|
106
|
+
}
|
|
107
|
+
|
|
74
108
|
function absolutePathForWorkspace(workspaceId, relativePath, database) {
|
|
75
109
|
const workspace = database.prepare('SELECT path FROM workspaces WHERE id = ?').get(workspaceId);
|
|
76
110
|
if (!workspace) throw invalid('workspace not found', 404);
|
|
@@ -81,78 +115,32 @@ function persistNodes(database, before, after, workspaceId) {
|
|
|
81
115
|
const now = new Date().toISOString();
|
|
82
116
|
const pathMemo = new Map();
|
|
83
117
|
const changedPaths = [];
|
|
84
|
-
const persisted = new Map(
|
|
85
|
-
database.prepare(
|
|
86
|
-
'SELECT * FROM workspace_nodes WHERE workspace_id = ?',
|
|
87
|
-
).all(workspaceId)
|
|
88
|
-
.map(rowToNode)
|
|
89
|
-
.map((node) => [node.id, node]),
|
|
90
|
-
);
|
|
91
118
|
for (const node of after.values()) {
|
|
92
119
|
const relativePath = relativePathForNode(after, node.id, pathMemo);
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
? relativePathForNode(before, previousFromBefore.id)
|
|
97
|
-
: previous?.relativePath || null;
|
|
98
|
-
if (!persisted.has(node.id)) {
|
|
120
|
+
const previous = before.get(node.id);
|
|
121
|
+
const previousRelative = previous ? relativePathForNode(before, previous.id) : null;
|
|
122
|
+
if (!previous) {
|
|
99
123
|
const absolutePath = relativePath ? absolutePathForWorkspace(workspaceId, relativePath, database) : null;
|
|
100
|
-
const stats = absolutePath
|
|
101
|
-
? fs.lstatSync(absolutePath)
|
|
102
|
-
: null;
|
|
124
|
+
const stats = absolutePath ? fs.lstatSync(absolutePath) : null;
|
|
103
125
|
database.prepare(`
|
|
104
126
|
INSERT INTO workspace_nodes (
|
|
105
127
|
id, workspace_id, parent_id, kind, name, relative_path, symlink_target,
|
|
106
|
-
device_id, inode, trashed_at,
|
|
107
|
-
|
|
108
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
109
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
110
|
-
workspace_id = excluded.workspace_id,
|
|
111
|
-
parent_id = excluded.parent_id,
|
|
112
|
-
kind = excluded.kind,
|
|
113
|
-
name = excluded.name,
|
|
114
|
-
relative_path = excluded.relative_path,
|
|
115
|
-
symlink_target = excluded.symlink_target,
|
|
116
|
-
device_id = excluded.device_id,
|
|
117
|
-
inode = excluded.inode,
|
|
118
|
-
trashed_at = excluded.trashed_at,
|
|
119
|
-
trash_component_id = excluded.trash_component_id,
|
|
120
|
-
purged_at = excluded.purged_at,
|
|
121
|
-
missing_at = NULL,
|
|
122
|
-
updated_at = excluded.updated_at
|
|
128
|
+
device_id, inode, trashed_at, purged_at, missing_at, created_at, updated_at
|
|
129
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
123
130
|
`).run(
|
|
124
131
|
node.id, node.workspaceId, node.parentId, node.kind, node.name,
|
|
125
132
|
relativePath, node.symlinkTarget, stats ? String(stats.dev) : null,
|
|
126
|
-
stats ? String(stats.ino) : null, node.trashedAt,
|
|
127
|
-
node.trashComponentId, node.purgedAt,
|
|
133
|
+
stats ? String(stats.ino) : null, node.trashedAt, node.purgedAt,
|
|
128
134
|
node.createdAt, now,
|
|
129
135
|
);
|
|
130
136
|
} else {
|
|
131
137
|
database.prepare(`
|
|
132
138
|
UPDATE workspace_nodes
|
|
133
139
|
SET parent_id = ?, name = ?, relative_path = ?, trashed_at = ?,
|
|
134
|
-
|
|
135
|
-
updated_at = ?
|
|
140
|
+
purged_at = ?, missing_at = NULL, updated_at = ?
|
|
136
141
|
WHERE id = ?
|
|
137
|
-
`).run(
|
|
138
|
-
|
|
139
|
-
node.name,
|
|
140
|
-
relativePath,
|
|
141
|
-
node.trashedAt,
|
|
142
|
-
node.trashComponentId,
|
|
143
|
-
node.purgedAt,
|
|
144
|
-
now,
|
|
145
|
-
node.id,
|
|
146
|
-
);
|
|
147
|
-
// Trashed and purged rows are UUID tombstones, not live document
|
|
148
|
-
// locations. Multiple tombstones may legally retain the same logical
|
|
149
|
-
// path, so only a resulting live node may participate in document path
|
|
150
|
-
// rebinding.
|
|
151
|
-
if (
|
|
152
|
-
previousRelative !== relativePath
|
|
153
|
-
&& !node.trashedAt
|
|
154
|
-
&& !node.purgedAt
|
|
155
|
-
) {
|
|
142
|
+
`).run(node.parentId, node.name, relativePath, node.trashedAt, node.purgedAt, now, node.id);
|
|
143
|
+
if (previousRelative !== relativePath) {
|
|
156
144
|
changedPaths.push({
|
|
157
145
|
nodeId: node.id,
|
|
158
146
|
oldPath: absolutePathForWorkspace(workspaceId, previousRelative, database),
|
|
@@ -177,9 +165,9 @@ function hasAncestorInSet(node, nodesById, ids) {
|
|
|
177
165
|
|
|
178
166
|
module.exports = {
|
|
179
167
|
hasAncestorInSet,
|
|
168
|
+
materializeOperation,
|
|
180
169
|
persistNodes,
|
|
181
170
|
relativePathForNode,
|
|
182
171
|
rowToNode,
|
|
183
|
-
trashPath,
|
|
184
172
|
workspaceRow,
|
|
185
173
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { normalizeTreeOperation } = require('./operations');
|
|
4
|
-
const { validateLiveSymlinkTargets } = require('./symlinks');
|
|
5
4
|
|
|
6
5
|
function toMap(nodes) {
|
|
7
6
|
if (nodes instanceof Map) return new Map(nodes);
|
|
@@ -20,25 +19,6 @@ function descendantsInclude(nodes, ancestorId, candidateId) {
|
|
|
20
19
|
return false;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
function descendantIds(nodes, ancestorId) {
|
|
24
|
-
const children = new Map();
|
|
25
|
-
for (const node of nodes.values()) {
|
|
26
|
-
if (!node.parentId) continue;
|
|
27
|
-
const siblings = children.get(node.parentId) || [];
|
|
28
|
-
siblings.push(node.id);
|
|
29
|
-
children.set(node.parentId, siblings);
|
|
30
|
-
}
|
|
31
|
-
const descendants = [];
|
|
32
|
-
const queue = [ancestorId];
|
|
33
|
-
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
34
|
-
for (const childId of children.get(queue[cursor]) || []) {
|
|
35
|
-
descendants.push(childId);
|
|
36
|
-
queue.push(childId);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return descendants;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
22
|
function ensureAvailableName(nodes, workspaceId, parentId, name, exceptId = null) {
|
|
43
23
|
const duplicate = Array.from(nodes.values()).find((node) => (
|
|
44
24
|
node.workspaceId === workspaceId
|
|
@@ -71,12 +51,10 @@ function applyTreeOperation(inputNodes, inputOperation) {
|
|
|
71
51
|
name: operation.name,
|
|
72
52
|
symlinkTarget: operation.symlinkTarget || null,
|
|
73
53
|
trashedAt: null,
|
|
74
|
-
trashComponentId: null,
|
|
75
54
|
purgedAt: null,
|
|
76
55
|
createdAt: operation.createdAt,
|
|
77
56
|
updatedAt: operation.createdAt,
|
|
78
57
|
});
|
|
79
|
-
validateLiveSymlinkTargets(nodes, operation.workspaceId);
|
|
80
58
|
return nodes;
|
|
81
59
|
}
|
|
82
60
|
|
|
@@ -94,7 +72,6 @@ function applyTreeOperation(inputNodes, inputOperation) {
|
|
|
94
72
|
}
|
|
95
73
|
|
|
96
74
|
const next = { ...existing, updatedAt: operation.changedAt };
|
|
97
|
-
const existingTrashComponentId = existing.trashComponentId || existing.id;
|
|
98
75
|
if (operation.kind === 'tree.rename') {
|
|
99
76
|
ensureAvailableName(nodes, operation.workspaceId, existing.parentId, operation.name, existing.id);
|
|
100
77
|
next.name = operation.name;
|
|
@@ -115,7 +92,6 @@ function applyTreeOperation(inputNodes, inputOperation) {
|
|
|
115
92
|
throw Object.assign(new Error('a node is already trashed'), { statusCode: 409 });
|
|
116
93
|
}
|
|
117
94
|
next.trashedAt = operation.trashedAt;
|
|
118
|
-
next.trashComponentId = operation.nodeId;
|
|
119
95
|
} else if (operation.kind === 'tree.restore') {
|
|
120
96
|
if (!existing.trashedAt) {
|
|
121
97
|
throw Object.assign(new Error('a node must be trashed before it can be restored'), { statusCode: 409 });
|
|
@@ -133,47 +109,26 @@ function applyTreeOperation(inputNodes, inputOperation) {
|
|
|
133
109
|
next.parentId = parentId;
|
|
134
110
|
next.name = name;
|
|
135
111
|
next.trashedAt = null;
|
|
136
|
-
next.trashComponentId = null;
|
|
137
112
|
} else if (operation.kind === 'tree.purge') {
|
|
138
113
|
if (!existing.trashedAt) {
|
|
139
114
|
throw Object.assign(new Error('a node must be trashed before permanent removal'), { statusCode: 409 });
|
|
140
115
|
}
|
|
141
116
|
next.purgedAt = operation.purgedAt;
|
|
142
|
-
next.trashComponentId = null;
|
|
143
117
|
}
|
|
144
118
|
|
|
145
119
|
nodes.set(next.id, next);
|
|
146
120
|
if (operation.kind === 'tree.trash' || operation.kind === 'tree.restore' || operation.kind === 'tree.purge') {
|
|
147
|
-
for (const
|
|
148
|
-
|
|
121
|
+
for (const child of nodes.values()) {
|
|
122
|
+
if (child.id === next.id || !descendantsInclude(nodes, next.id, child.id)) continue;
|
|
149
123
|
if (operation.kind === 'tree.trash' && !child.trashedAt) {
|
|
150
|
-
nodes.set(child.id, {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
trashComponentId: operation.nodeId,
|
|
154
|
-
updatedAt: operation.changedAt,
|
|
155
|
-
});
|
|
156
|
-
} else if (
|
|
157
|
-
operation.kind === 'tree.restore'
|
|
158
|
-
&& (child.trashComponentId || child.id) === existingTrashComponentId
|
|
159
|
-
) {
|
|
160
|
-
nodes.set(child.id, {
|
|
161
|
-
...child,
|
|
162
|
-
trashedAt: null,
|
|
163
|
-
trashComponentId: null,
|
|
164
|
-
updatedAt: operation.changedAt,
|
|
165
|
-
});
|
|
124
|
+
nodes.set(child.id, { ...child, trashedAt: operation.trashedAt, updatedAt: operation.changedAt });
|
|
125
|
+
} else if (operation.kind === 'tree.restore' && child.trashedAt === existing.trashedAt) {
|
|
126
|
+
nodes.set(child.id, { ...child, trashedAt: null, updatedAt: operation.changedAt });
|
|
166
127
|
} else if (operation.kind === 'tree.purge') {
|
|
167
|
-
nodes.set(child.id, {
|
|
168
|
-
...child,
|
|
169
|
-
trashComponentId: null,
|
|
170
|
-
purgedAt: operation.purgedAt,
|
|
171
|
-
updatedAt: operation.changedAt,
|
|
172
|
-
});
|
|
128
|
+
nodes.set(child.id, { ...child, purgedAt: operation.purgedAt, updatedAt: operation.changedAt });
|
|
173
129
|
}
|
|
174
130
|
}
|
|
175
131
|
}
|
|
176
|
-
validateLiveSymlinkTargets(nodes, operation.workspaceId);
|
|
177
132
|
return nodes;
|
|
178
133
|
}
|
|
179
134
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const { isUuid } = require('./identity');
|
|
4
4
|
const { KINDS, normalizeNodeName } = require('./operations');
|
|
5
|
-
const { validateLiveSymlinkTargets } = require('./symlinks');
|
|
6
5
|
|
|
7
6
|
function invalid(message) {
|
|
8
7
|
throw Object.assign(new Error(message), { statusCode: 400 });
|
|
@@ -36,12 +35,6 @@ function normalizeTreeSnapshot(input) {
|
|
|
36
35
|
}
|
|
37
36
|
const parentId = value.parentId == null ? null : String(value.parentId).toLowerCase();
|
|
38
37
|
if (parentId && !isUuid(parentId)) invalid('workspace tree parent identity is invalid');
|
|
39
|
-
const trashComponentId = value.trashComponentId == null
|
|
40
|
-
? null
|
|
41
|
-
: String(value.trashComponentId).toLowerCase();
|
|
42
|
-
if (trashComponentId && !isUuid(trashComponentId)) {
|
|
43
|
-
invalid('workspace tree trash component identity is invalid');
|
|
44
|
-
}
|
|
45
38
|
const createdAt = timestamp(value.createdAt, new Date().toISOString());
|
|
46
39
|
const updatedAt = timestamp(value.updatedAt, createdAt);
|
|
47
40
|
if (value.kind === 'symlink' && (typeof value.symlinkTarget !== 'string' || !value.symlinkTarget)) {
|
|
@@ -55,7 +48,6 @@ function normalizeTreeSnapshot(input) {
|
|
|
55
48
|
name: normalizeNodeName(value.name),
|
|
56
49
|
symlinkTarget: value.kind === 'symlink' ? value.symlinkTarget : null,
|
|
57
50
|
trashedAt: value.trashedAt ? timestamp(value.trashedAt) : null,
|
|
58
|
-
trashComponentId,
|
|
59
51
|
purgedAt: value.purgedAt ? timestamp(value.purgedAt) : null,
|
|
60
52
|
createdAt,
|
|
61
53
|
updatedAt,
|
|
@@ -66,30 +58,17 @@ function normalizeTreeSnapshot(input) {
|
|
|
66
58
|
if (!root || root.parentId !== null || root.kind !== 'folder') {
|
|
67
59
|
invalid('workspace tree requires one folder root whose id is the workspace id');
|
|
68
60
|
}
|
|
69
|
-
const ancestry = buildAncestryIndex(nodes, workspaceId);
|
|
70
61
|
const siblingNames = new Set();
|
|
71
62
|
for (const node of nodes.values()) {
|
|
72
63
|
if (node.id === workspaceId) continue;
|
|
73
|
-
if (node.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const component = nodes.get(node.trashComponentId);
|
|
82
|
-
if (
|
|
83
|
-
!component
|
|
84
|
-
|| !component.trashedAt
|
|
85
|
-
|| component.purgedAt
|
|
86
|
-
|| component.trashComponentId !== component.id
|
|
87
|
-
|| !isAncestor(ancestry, component.id, node.id)
|
|
88
|
-
) {
|
|
89
|
-
invalid('workspace tree trash component is not an ancestor');
|
|
90
|
-
}
|
|
91
|
-
} else if (node.trashComponentId) {
|
|
92
|
-
invalid('a live workspace tree node cannot name a trash component');
|
|
64
|
+
if (!node.parentId || !nodes.has(node.parentId)) invalid('workspace tree node parent is missing');
|
|
65
|
+
const seen = new Set([node.id]);
|
|
66
|
+
let current = node;
|
|
67
|
+
while (current.parentId) {
|
|
68
|
+
if (seen.has(current.parentId)) invalid('workspace tree contains a cycle');
|
|
69
|
+
seen.add(current.parentId);
|
|
70
|
+
current = nodes.get(current.parentId);
|
|
71
|
+
if (!current) invalid('workspace tree node parent is missing');
|
|
93
72
|
}
|
|
94
73
|
if (!node.trashedAt && !node.purgedAt) {
|
|
95
74
|
const key = `${node.parentId}\0${node.name}`;
|
|
@@ -97,57 +76,7 @@ function normalizeTreeSnapshot(input) {
|
|
|
97
76
|
siblingNames.add(key);
|
|
98
77
|
}
|
|
99
78
|
}
|
|
100
|
-
validateLiveSymlinkTargets(nodes, workspaceId);
|
|
101
79
|
return { schemaVersion: 1, workspaceId, version: input.version, nodes };
|
|
102
80
|
}
|
|
103
81
|
|
|
104
|
-
function buildAncestryIndex(nodes, rootId) {
|
|
105
|
-
const children = new Map();
|
|
106
|
-
for (const node of nodes.values()) {
|
|
107
|
-
if (node.id === rootId) continue;
|
|
108
|
-
if (!node.parentId || !nodes.has(node.parentId)) invalid('workspace tree node parent is missing');
|
|
109
|
-
const siblings = children.get(node.parentId) || [];
|
|
110
|
-
siblings.push(node.id);
|
|
111
|
-
children.set(node.parentId, siblings);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const entered = new Map();
|
|
115
|
-
const exited = new Map();
|
|
116
|
-
const stack = [{ id: rootId, exit: false }];
|
|
117
|
-
let clock = 0;
|
|
118
|
-
while (stack.length) {
|
|
119
|
-
const frame = stack.pop();
|
|
120
|
-
if (frame.exit) {
|
|
121
|
-
exited.set(frame.id, clock);
|
|
122
|
-
clock += 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (entered.has(frame.id)) invalid('workspace tree contains a cycle');
|
|
126
|
-
entered.set(frame.id, clock);
|
|
127
|
-
clock += 1;
|
|
128
|
-
stack.push({ id: frame.id, exit: true });
|
|
129
|
-
const descendants = children.get(frame.id) || [];
|
|
130
|
-
for (let index = descendants.length - 1; index >= 0; index -= 1) {
|
|
131
|
-
stack.push({ id: descendants[index], exit: false });
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (entered.size !== nodes.size) invalid('workspace tree contains a cycle');
|
|
135
|
-
return { entered, exited };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function isAncestor(index, ancestorId, nodeId) {
|
|
139
|
-
const ancestorEntered = index.entered.get(ancestorId);
|
|
140
|
-
const ancestorExited = index.exited.get(ancestorId);
|
|
141
|
-
const nodeEntered = index.entered.get(nodeId);
|
|
142
|
-
const nodeExited = index.exited.get(nodeId);
|
|
143
|
-
return (
|
|
144
|
-
ancestorEntered !== undefined
|
|
145
|
-
&& ancestorExited !== undefined
|
|
146
|
-
&& nodeEntered !== undefined
|
|
147
|
-
&& nodeExited !== undefined
|
|
148
|
-
&& ancestorEntered <= nodeEntered
|
|
149
|
-
&& nodeExited <= ancestorExited
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
82
|
module.exports = { normalizeTreeSnapshot };
|