amalgm 0.1.238 → 0.1.240
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/lib/shared-realtime-tunnel.js +22 -14
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
- package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
- package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
- package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
- package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
- package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
- package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
- package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
- package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
- package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
- package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
- package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
- package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
- package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
- package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
- package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
- package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
- package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
- package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
- package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
- package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
- package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
- package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
- package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
- package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
- package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
- package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
- 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 +72 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
- package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
- package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
- package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
- package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
- package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
- package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
- package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
- package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
- package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
- package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
- package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
- package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
- package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
- package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
- package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Physical boundary for live-document files.
|
|
5
|
+
*
|
|
6
|
+
* The Yjs mutation journal proves semantic order. This table and the native
|
|
7
|
+
* file receipt prove which exact disk predecessor a materialization replaced.
|
|
8
|
+
* Machine-local disk evidence never enters the cloud operation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { cachedStatement, ensureDocStatesSchema, openLocalDb } = require('./db');
|
|
15
|
+
const nativeFileTransaction = require('../workspace/native-file-transaction/client');
|
|
16
|
+
|
|
17
|
+
const CONTRACT = 'doc-disk-materialization@1';
|
|
18
|
+
|
|
19
|
+
function byteIdentity(content, stat = null) {
|
|
20
|
+
if (content === null) return null;
|
|
21
|
+
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content, 'utf8');
|
|
22
|
+
return {
|
|
23
|
+
kind: 'file',
|
|
24
|
+
exec: stat ? (stat.mode & 0o100n) !== 0n : false,
|
|
25
|
+
bytes: bytes.length,
|
|
26
|
+
sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
27
|
+
...(stat ? {
|
|
28
|
+
dev: stat.dev.toString(),
|
|
29
|
+
ino: stat.ino.toString(),
|
|
30
|
+
} : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function observe(filePath, testHooks = {}) {
|
|
35
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
36
|
+
const parent = fs.realpathSync(path.dirname(filePath));
|
|
37
|
+
const rootStat = fs.statSync(parent, { bigint: true });
|
|
38
|
+
const relativePath = path.basename(filePath);
|
|
39
|
+
const result = nativeFileTransaction.observe({
|
|
40
|
+
root: parent,
|
|
41
|
+
rootDev: rootStat.dev.toString(),
|
|
42
|
+
rootIno: rootStat.ino.toString(),
|
|
43
|
+
path: relativePath,
|
|
44
|
+
}, testHooks);
|
|
45
|
+
if (result.status === 'changed') continue;
|
|
46
|
+
if (result.status !== 'observed') {
|
|
47
|
+
throw new Error(`unexpected native document observation: ${result.status}`);
|
|
48
|
+
}
|
|
49
|
+
const content = result.content === undefined
|
|
50
|
+
? null
|
|
51
|
+
: Buffer.from(result.content, 'base64');
|
|
52
|
+
const identity = result.value
|
|
53
|
+
? {
|
|
54
|
+
...result.value,
|
|
55
|
+
exec: result.value.exec === true,
|
|
56
|
+
bytes: Number(result.value.bytes || 0),
|
|
57
|
+
}
|
|
58
|
+
: null;
|
|
59
|
+
return {
|
|
60
|
+
root: parent,
|
|
61
|
+
rootDev: result.rootDev,
|
|
62
|
+
rootIno: result.rootIno,
|
|
63
|
+
relativePath: result.path,
|
|
64
|
+
text: content === null ? null : content.toString('utf8'),
|
|
65
|
+
identity,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`live document kept changing during observation: ${filePath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sameIdentity(left, right) {
|
|
72
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sameObservation(left, right) {
|
|
76
|
+
return Boolean(
|
|
77
|
+
left
|
|
78
|
+
&& right
|
|
79
|
+
&& left.rootDev === right.rootDev
|
|
80
|
+
&& left.rootIno === right.rootIno
|
|
81
|
+
&& left.relativePath === right.relativePath
|
|
82
|
+
&& sameIdentity(left.identity, right.identity),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function transactionId(base, channelId, mutationId) {
|
|
87
|
+
return crypto.createHash('sha256')
|
|
88
|
+
.update(
|
|
89
|
+
`${CONTRACT}\0${base.rootDev}\0${base.rootIno}\0${base.relativePath}`
|
|
90
|
+
+ `\0${channelId}\0${mutationId}`,
|
|
91
|
+
)
|
|
92
|
+
.digest('hex');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateBaseObservation(base) {
|
|
96
|
+
if (base.text === null) {
|
|
97
|
+
if (base.identity !== null) {
|
|
98
|
+
throw new Error('missing document base has a physical identity');
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (typeof base.text !== 'string' || base.identity?.kind !== 'file') {
|
|
103
|
+
throw new Error('document disk base is incomplete');
|
|
104
|
+
}
|
|
105
|
+
const contentIdentity = byteIdentity(Buffer.from(base.text, 'utf8'));
|
|
106
|
+
if (
|
|
107
|
+
contentIdentity.bytes !== base.identity.bytes
|
|
108
|
+
|| contentIdentity.sha256 !== base.identity.sha256
|
|
109
|
+
) {
|
|
110
|
+
throw new Error('document disk base text does not match its physical identity');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function insertBase(database, mutation, base, mutationSource = 'doc:recovery') {
|
|
115
|
+
ensureDocStatesSchema(database);
|
|
116
|
+
validateBaseObservation(base);
|
|
117
|
+
const id = transactionId(base, mutation.channelId, mutation.mutationId);
|
|
118
|
+
cachedStatement(database, `
|
|
119
|
+
INSERT INTO doc_disk_materializations (
|
|
120
|
+
channel_id,
|
|
121
|
+
mutation_id,
|
|
122
|
+
local_resource,
|
|
123
|
+
root,
|
|
124
|
+
root_dev,
|
|
125
|
+
root_ino,
|
|
126
|
+
relative_path,
|
|
127
|
+
transaction_id,
|
|
128
|
+
operation_id,
|
|
129
|
+
expected_json,
|
|
130
|
+
observed_text,
|
|
131
|
+
mutation_source
|
|
132
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
133
|
+
ON CONFLICT(channel_id, mutation_id) DO NOTHING
|
|
134
|
+
`).run(
|
|
135
|
+
mutation.channelId,
|
|
136
|
+
mutation.mutationId,
|
|
137
|
+
mutation.localResource,
|
|
138
|
+
base.root,
|
|
139
|
+
base.rootDev,
|
|
140
|
+
base.rootIno,
|
|
141
|
+
base.relativePath,
|
|
142
|
+
id,
|
|
143
|
+
`${mutation.channelId}:${mutation.mutationId}`,
|
|
144
|
+
base.identity === null ? null : JSON.stringify(base.identity),
|
|
145
|
+
base.text,
|
|
146
|
+
mutationSource,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalize(row) {
|
|
151
|
+
if (!row) return null;
|
|
152
|
+
return {
|
|
153
|
+
channelId: row.channel_id,
|
|
154
|
+
mutationId: row.mutation_id,
|
|
155
|
+
localResource: row.local_resource,
|
|
156
|
+
root: row.root,
|
|
157
|
+
rootDev: row.root_dev,
|
|
158
|
+
rootIno: row.root_ino,
|
|
159
|
+
relativePath: row.relative_path,
|
|
160
|
+
transactionId: row.transaction_id,
|
|
161
|
+
operationId: row.operation_id,
|
|
162
|
+
expected: row.expected_json === null ? null : JSON.parse(row.expected_json),
|
|
163
|
+
baseText: row.observed_text,
|
|
164
|
+
mutationSource: row.mutation_source || (
|
|
165
|
+
row.observed_text === null ? 'doc:recovery' : 'doc:disk'
|
|
166
|
+
),
|
|
167
|
+
target: row.target_json === null ? null : JSON.parse(row.target_json),
|
|
168
|
+
targetContent: row.target_content === null ? null : Buffer.from(row.target_content),
|
|
169
|
+
planKind: row.plan_kind,
|
|
170
|
+
snapshotPayload: row.snapshot_payload_json === null
|
|
171
|
+
? null
|
|
172
|
+
: JSON.parse(row.snapshot_payload_json),
|
|
173
|
+
snapshotEventSeq: row.snapshot_event_seq === null
|
|
174
|
+
? null
|
|
175
|
+
: Number(row.snapshot_event_seq),
|
|
176
|
+
state: row.materialization_state,
|
|
177
|
+
successorChannelId: row.successor_channel_id,
|
|
178
|
+
successorMutationId: row.successor_mutation_id,
|
|
179
|
+
createdAt: row.created_at,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function snapshotMutation(payload, base) {
|
|
184
|
+
const digest = crypto.createHash('sha256')
|
|
185
|
+
.update(
|
|
186
|
+
`${CONTRACT}\0snapshot\0${payload.resourceId}\0${payload.snapshotVersion}`
|
|
187
|
+
+ `\0${payload.snapshotChecksum}\0${base.rootDev}\0${base.rootIno}`
|
|
188
|
+
+ `\0${base.relativePath}\0${JSON.stringify(base.identity)}`,
|
|
189
|
+
)
|
|
190
|
+
.digest('hex');
|
|
191
|
+
return {
|
|
192
|
+
channelId: `snapshot:${payload.resourceId}`,
|
|
193
|
+
mutationId: `snapshot_${digest}`,
|
|
194
|
+
localResource: payload.localResource,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function insertSnapshotBase(database, payload, base) {
|
|
199
|
+
const mutation = snapshotMutation(payload, base);
|
|
200
|
+
insertBase(database, mutation, base, 'doc:cloud-snapshot');
|
|
201
|
+
cachedStatement(database, `
|
|
202
|
+
UPDATE doc_disk_materializations
|
|
203
|
+
SET plan_kind = 'snapshot',
|
|
204
|
+
snapshot_payload_json = ?,
|
|
205
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
206
|
+
WHERE channel_id = ?
|
|
207
|
+
AND mutation_id = ?
|
|
208
|
+
AND plan_kind = 'mutation'
|
|
209
|
+
AND materialization_state = 'based'
|
|
210
|
+
`).run(JSON.stringify(payload), mutation.channelId, mutation.mutationId);
|
|
211
|
+
const plan = get(mutation.channelId, mutation.mutationId, database);
|
|
212
|
+
if (!plan || plan.planKind !== 'snapshot') {
|
|
213
|
+
throw new Error(`snapshot ${payload.resourceId}@${payload.snapshotVersion} changed its disk plan`);
|
|
214
|
+
}
|
|
215
|
+
return plan;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function firstBase(localResource, database = openLocalDb()) {
|
|
219
|
+
ensureDocStatesSchema(database);
|
|
220
|
+
return normalize(cachedStatement(database, `
|
|
221
|
+
SELECT * FROM doc_disk_materializations
|
|
222
|
+
WHERE local_resource = ?
|
|
223
|
+
ORDER BY created_at ASC, mutation_id ASC
|
|
224
|
+
LIMIT 1
|
|
225
|
+
`).get(localResource) || null);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function get(channelId, mutationId, database = openLocalDb()) {
|
|
229
|
+
ensureDocStatesSchema(database);
|
|
230
|
+
return normalize(cachedStatement(database, `
|
|
231
|
+
SELECT * FROM doc_disk_materializations
|
|
232
|
+
WHERE channel_id = ? AND mutation_id = ?
|
|
233
|
+
`).get(channelId, mutationId) || null);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function listSnapshots(localResource, database = openLocalDb()) {
|
|
237
|
+
ensureDocStatesSchema(database);
|
|
238
|
+
return cachedStatement(database, `
|
|
239
|
+
SELECT * FROM doc_disk_materializations
|
|
240
|
+
WHERE local_resource = ? AND plan_kind = 'snapshot'
|
|
241
|
+
ORDER BY created_at ASC, mutation_id ASC
|
|
242
|
+
`).all(localResource).map(normalize);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function listMutations(localResource, database = openLocalDb()) {
|
|
246
|
+
ensureDocStatesSchema(database);
|
|
247
|
+
return cachedStatement(database, `
|
|
248
|
+
SELECT * FROM doc_disk_materializations
|
|
249
|
+
WHERE local_resource = ? AND plan_kind = 'mutation'
|
|
250
|
+
ORDER BY created_at ASC, mutation_id ASC
|
|
251
|
+
`).all(localResource).map(normalize);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function prepare(channelId, mutationId, targetText, database = openLocalDb()) {
|
|
255
|
+
const held = get(channelId, mutationId, database);
|
|
256
|
+
if (!held) throw new Error(`document mutation ${mutationId} has no disk base`);
|
|
257
|
+
const content = Buffer.from(targetText, 'utf8');
|
|
258
|
+
const target = byteIdentity(content);
|
|
259
|
+
target.exec = held.expected?.exec === true;
|
|
260
|
+
if (held.target) {
|
|
261
|
+
if (!sameIdentity(held.target, target) || !held.targetContent.equals(content)) {
|
|
262
|
+
throw new Error(`document mutation ${mutationId} changed its disk target`);
|
|
263
|
+
}
|
|
264
|
+
return held;
|
|
265
|
+
}
|
|
266
|
+
cachedStatement(database, `
|
|
267
|
+
UPDATE doc_disk_materializations
|
|
268
|
+
SET target_json = ?,
|
|
269
|
+
target_content = ?,
|
|
270
|
+
materialization_state = 'prepared',
|
|
271
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
272
|
+
WHERE channel_id = ? AND mutation_id = ? AND materialization_state = 'based'
|
|
273
|
+
`).run(JSON.stringify(target), content, channelId, mutationId);
|
|
274
|
+
return get(channelId, mutationId, database);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function transaction(plan) {
|
|
278
|
+
return {
|
|
279
|
+
root: plan.root,
|
|
280
|
+
rootDev: plan.rootDev,
|
|
281
|
+
rootIno: plan.rootIno,
|
|
282
|
+
path: plan.relativePath,
|
|
283
|
+
id: plan.transactionId,
|
|
284
|
+
operation: plan.operationId,
|
|
285
|
+
expected: plan.expected,
|
|
286
|
+
target: plan.target,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function scratchPath(plan) {
|
|
291
|
+
return path.join(plan.root, `.amalgm-apply-${plan.transactionId}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function cross(plan, database = openLocalDb(), testHooks = {}) {
|
|
295
|
+
if (!plan?.target || !plan.targetContent) {
|
|
296
|
+
throw new Error(`document mutation ${plan?.mutationId || 'unknown'} has no prepared target`);
|
|
297
|
+
}
|
|
298
|
+
let result;
|
|
299
|
+
if (fs.existsSync(scratchPath(plan))) {
|
|
300
|
+
result = nativeFileTransaction.run(transaction(plan), 'recover');
|
|
301
|
+
} else {
|
|
302
|
+
result = nativeFileTransaction.run(
|
|
303
|
+
transaction(plan),
|
|
304
|
+
'apply',
|
|
305
|
+
plan.targetContent,
|
|
306
|
+
testHooks,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (result.status === 'applied') {
|
|
310
|
+
cachedStatement(database, `
|
|
311
|
+
UPDATE doc_disk_materializations
|
|
312
|
+
SET materialization_state = 'crossed',
|
|
313
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
314
|
+
WHERE channel_id = ? AND mutation_id = ?
|
|
315
|
+
`).run(plan.channelId, plan.mutationId);
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function markSuperseded(
|
|
321
|
+
database,
|
|
322
|
+
channelId,
|
|
323
|
+
mutationId,
|
|
324
|
+
successorChannelId,
|
|
325
|
+
successorMutationId,
|
|
326
|
+
) {
|
|
327
|
+
const result = cachedStatement(database, `
|
|
328
|
+
UPDATE doc_disk_materializations
|
|
329
|
+
SET materialization_state = 'superseded',
|
|
330
|
+
successor_channel_id = ?,
|
|
331
|
+
successor_mutation_id = ?,
|
|
332
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
333
|
+
WHERE channel_id = ?
|
|
334
|
+
AND mutation_id = ?
|
|
335
|
+
AND materialization_state IN ('based', 'prepared')
|
|
336
|
+
`).run(successorChannelId, successorMutationId, channelId, mutationId);
|
|
337
|
+
if (result.changes !== 1) {
|
|
338
|
+
throw new Error(`document mutation ${mutationId} could not retain its disk successor`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function linkCrossedSuccessor(
|
|
343
|
+
database,
|
|
344
|
+
channelId,
|
|
345
|
+
mutationId,
|
|
346
|
+
successorChannelId,
|
|
347
|
+
successorMutationId,
|
|
348
|
+
) {
|
|
349
|
+
const result = cachedStatement(database, `
|
|
350
|
+
UPDATE doc_disk_materializations
|
|
351
|
+
SET successor_channel_id = ?,
|
|
352
|
+
successor_mutation_id = ?,
|
|
353
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
354
|
+
WHERE channel_id = ?
|
|
355
|
+
AND mutation_id = ?
|
|
356
|
+
AND materialization_state = 'crossed'
|
|
357
|
+
AND (
|
|
358
|
+
successor_channel_id IS NULL
|
|
359
|
+
OR (
|
|
360
|
+
successor_channel_id = ?
|
|
361
|
+
AND successor_mutation_id = ?
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
`).run(
|
|
365
|
+
successorChannelId,
|
|
366
|
+
successorMutationId,
|
|
367
|
+
channelId,
|
|
368
|
+
mutationId,
|
|
369
|
+
successorChannelId,
|
|
370
|
+
successorMutationId,
|
|
371
|
+
);
|
|
372
|
+
if (result.changes !== 1) {
|
|
373
|
+
throw new Error(`crossed document mutation ${mutationId} changed its disk successor`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function rebaseExpected(channelId, mutationId, observation, database = openLocalDb()) {
|
|
378
|
+
validateBaseObservation(observation);
|
|
379
|
+
const held = get(channelId, mutationId, database);
|
|
380
|
+
if (!held || held.state !== 'prepared') {
|
|
381
|
+
throw new Error(`document mutation ${mutationId} cannot refresh its disk predecessor`);
|
|
382
|
+
}
|
|
383
|
+
if (fs.existsSync(scratchPath(held))) {
|
|
384
|
+
throw new Error(`document mutation ${mutationId} still has a physical receipt`);
|
|
385
|
+
}
|
|
386
|
+
const id = transactionId(observation, channelId, mutationId);
|
|
387
|
+
cachedStatement(database, `
|
|
388
|
+
UPDATE doc_disk_materializations
|
|
389
|
+
SET root = ?,
|
|
390
|
+
root_dev = ?,
|
|
391
|
+
root_ino = ?,
|
|
392
|
+
relative_path = ?,
|
|
393
|
+
transaction_id = ?,
|
|
394
|
+
expected_json = ?,
|
|
395
|
+
observed_text = ?,
|
|
396
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
397
|
+
WHERE channel_id = ? AND mutation_id = ? AND materialization_state = 'prepared'
|
|
398
|
+
`).run(
|
|
399
|
+
observation.root,
|
|
400
|
+
observation.rootDev,
|
|
401
|
+
observation.rootIno,
|
|
402
|
+
observation.relativePath,
|
|
403
|
+
id,
|
|
404
|
+
observation.identity === null ? null : JSON.stringify(observation.identity),
|
|
405
|
+
observation.text,
|
|
406
|
+
channelId,
|
|
407
|
+
mutationId,
|
|
408
|
+
);
|
|
409
|
+
return get(channelId, mutationId, database);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function rebindLocalResource(
|
|
413
|
+
database,
|
|
414
|
+
{
|
|
415
|
+
oldLocalResource,
|
|
416
|
+
newLocalResource,
|
|
417
|
+
newPath,
|
|
418
|
+
},
|
|
419
|
+
) {
|
|
420
|
+
const root = fs.realpathSync(path.dirname(newPath));
|
|
421
|
+
const rootStat = fs.statSync(root, { bigint: true });
|
|
422
|
+
const rootDev = rootStat.dev.toString();
|
|
423
|
+
const rootIno = rootStat.ino.toString();
|
|
424
|
+
const relativePath = path.basename(newPath);
|
|
425
|
+
cachedStatement(database, `
|
|
426
|
+
UPDATE doc_disk_materializations
|
|
427
|
+
SET local_resource = ?,
|
|
428
|
+
root = CASE
|
|
429
|
+
WHEN root_dev = ? AND root_ino = ? THEN ?
|
|
430
|
+
ELSE root
|
|
431
|
+
END,
|
|
432
|
+
relative_path = CASE
|
|
433
|
+
WHEN root_dev = ? AND root_ino = ? THEN ?
|
|
434
|
+
ELSE relative_path
|
|
435
|
+
END,
|
|
436
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
437
|
+
WHERE local_resource = ? OR local_resource = ?
|
|
438
|
+
`).run(
|
|
439
|
+
newLocalResource,
|
|
440
|
+
rootDev,
|
|
441
|
+
rootIno,
|
|
442
|
+
root,
|
|
443
|
+
rootDev,
|
|
444
|
+
rootIno,
|
|
445
|
+
relativePath,
|
|
446
|
+
oldLocalResource,
|
|
447
|
+
newLocalResource,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function rehomeMutation(
|
|
452
|
+
database,
|
|
453
|
+
{
|
|
454
|
+
mutationId,
|
|
455
|
+
oldChannelId,
|
|
456
|
+
newChannelId,
|
|
457
|
+
},
|
|
458
|
+
) {
|
|
459
|
+
if (oldChannelId === newChannelId) return;
|
|
460
|
+
cachedStatement(database, `
|
|
461
|
+
UPDATE doc_disk_materializations
|
|
462
|
+
SET channel_id = ?,
|
|
463
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
464
|
+
WHERE channel_id = ? AND mutation_id = ?
|
|
465
|
+
`).run(newChannelId, oldChannelId, mutationId);
|
|
466
|
+
cachedStatement(database, `
|
|
467
|
+
UPDATE doc_disk_materializations
|
|
468
|
+
SET successor_channel_id = ?,
|
|
469
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
470
|
+
WHERE successor_channel_id = ? AND successor_mutation_id = ?
|
|
471
|
+
`).run(newChannelId, oldChannelId, mutationId);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function cancel(channelId, mutationId, database = openLocalDb()) {
|
|
475
|
+
const plan = get(channelId, mutationId, database);
|
|
476
|
+
if (!plan) return;
|
|
477
|
+
if (plan.state !== 'based' && plan.state !== 'prepared') {
|
|
478
|
+
throw new Error(`document disk plan ${mutationId} cannot cancel from ${plan.state}`);
|
|
479
|
+
}
|
|
480
|
+
if (fs.existsSync(scratchPath(plan))) {
|
|
481
|
+
throw new Error(`document disk plan ${mutationId} still has a physical receipt`);
|
|
482
|
+
}
|
|
483
|
+
cachedStatement(database, `
|
|
484
|
+
DELETE FROM doc_disk_materializations
|
|
485
|
+
WHERE channel_id = ? AND mutation_id = ?
|
|
486
|
+
`).run(channelId, mutationId);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function markSnapshotCommitted(channelId, mutationId, eventSeq, database = openLocalDb()) {
|
|
490
|
+
const result = cachedStatement(database, `
|
|
491
|
+
UPDATE doc_disk_materializations
|
|
492
|
+
SET snapshot_event_seq = ?,
|
|
493
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
494
|
+
WHERE channel_id = ?
|
|
495
|
+
AND mutation_id = ?
|
|
496
|
+
AND plan_kind = 'snapshot'
|
|
497
|
+
AND materialization_state = 'crossed'
|
|
498
|
+
AND snapshot_event_seq IS NULL
|
|
499
|
+
`).run(eventSeq, channelId, mutationId);
|
|
500
|
+
if (result.changes !== 1) {
|
|
501
|
+
const held = get(channelId, mutationId, database);
|
|
502
|
+
if (!held || held.snapshotEventSeq !== Number(eventSeq)) {
|
|
503
|
+
throw new Error(`snapshot disk plan ${mutationId} did not commit its event`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function finish(channelId, mutationId, database = openLocalDb()) {
|
|
509
|
+
const plan = get(channelId, mutationId, database);
|
|
510
|
+
if (!plan) return;
|
|
511
|
+
if (plan.state === 'crossed') {
|
|
512
|
+
if (plan.planKind === 'snapshot' && plan.snapshotEventSeq === null) {
|
|
513
|
+
throw new Error(`snapshot disk plan ${mutationId} has not committed its cursor`);
|
|
514
|
+
}
|
|
515
|
+
const result = nativeFileTransaction.run(transaction(plan), 'release');
|
|
516
|
+
if (result.status !== 'released') {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`document disk receipt ${mutationId} did not release: ${result.reason || result.status}`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
} else if (plan.state !== 'superseded') {
|
|
522
|
+
throw new Error(`document disk plan ${mutationId} cannot finish from ${plan.state}`);
|
|
523
|
+
}
|
|
524
|
+
cachedStatement(database, `
|
|
525
|
+
DELETE FROM doc_disk_materializations
|
|
526
|
+
WHERE channel_id = ? AND mutation_id = ?
|
|
527
|
+
`).run(channelId, mutationId);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
module.exports = {
|
|
531
|
+
byteIdentity,
|
|
532
|
+
cancel,
|
|
533
|
+
cross,
|
|
534
|
+
finish,
|
|
535
|
+
firstBase,
|
|
536
|
+
get,
|
|
537
|
+
insertBase,
|
|
538
|
+
insertSnapshotBase,
|
|
539
|
+
linkCrossedSuccessor,
|
|
540
|
+
listMutations,
|
|
541
|
+
listSnapshots,
|
|
542
|
+
markSuperseded,
|
|
543
|
+
markSnapshotCommitted,
|
|
544
|
+
observe,
|
|
545
|
+
prepare,
|
|
546
|
+
rebindLocalResource,
|
|
547
|
+
rebaseExpected,
|
|
548
|
+
rehomeMutation,
|
|
549
|
+
sameObservation,
|
|
550
|
+
};
|