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.
Files changed (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. 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
+ };