amalgm 0.1.155 → 0.1.156
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 +518 -0
- package/lib/tunnel-events.js +27 -0
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/server/routes/state.js +75 -0
- package/runtime/scripts/amalgm-mcp/state/attachments.js +187 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +144 -0
- package/runtime/scripts/amalgm-mcp/state/docs.js +230 -24
- package/runtime/scripts/amalgm-mcp/state/events.js +20 -2
- package/runtime/scripts/amalgm-mcp/state/mutations.js +800 -0
- package/runtime/scripts/amalgm-mcp/state/promotions.js +271 -0
- package/runtime/scripts/amalgm-mcp/state/replicas.js +200 -0
- package/runtime/scripts/amalgm-mcp/state/rest.js +4 -0
- package/runtime/scripts/amalgm-mcp/state/shared-rest.js +267 -0
- package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +337 -0
- package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +283 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-shared-replica-test-'));
|
|
10
|
+
process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
|
|
11
|
+
process.env.AMALGM_DEVICE_ID = 'device-shared-replica-test';
|
|
12
|
+
process.env.AMALGM_EVENT_LOG_PRUNE_INTERVAL_MS = '0';
|
|
13
|
+
|
|
14
|
+
const Y = require('yjs');
|
|
15
|
+
const { closeLocalDb } = require('../state/db');
|
|
16
|
+
const docs = require('../state/docs');
|
|
17
|
+
const { listEventsAfter, subscribeStateEvents } = require('../state/events');
|
|
18
|
+
const {
|
|
19
|
+
acknowledgeCloudMutation,
|
|
20
|
+
claimCloudMutations,
|
|
21
|
+
getMutation,
|
|
22
|
+
journalMutation,
|
|
23
|
+
} = require('../state/mutations');
|
|
24
|
+
const {
|
|
25
|
+
acknowledgePromotion,
|
|
26
|
+
claimPromotions,
|
|
27
|
+
enqueuePromotion,
|
|
28
|
+
} = require('../state/promotions');
|
|
29
|
+
const { getReplicaByResourceId } = require('../state/replicas');
|
|
30
|
+
const {
|
|
31
|
+
acknowledgeAttachment,
|
|
32
|
+
claimAttachments,
|
|
33
|
+
enqueueAttachment,
|
|
34
|
+
} = require('../state/attachments');
|
|
35
|
+
|
|
36
|
+
const resourceId = 'shr_abcdefghijklmnop';
|
|
37
|
+
const docDir = path.join(tempRoot, 'workspace');
|
|
38
|
+
fs.mkdirSync(docDir, { recursive: true });
|
|
39
|
+
const target = path.join(docDir, 'shared.md');
|
|
40
|
+
fs.writeFileSync(target, '# Shared\n', 'utf8');
|
|
41
|
+
|
|
42
|
+
function clientFromState(stateBase64) {
|
|
43
|
+
const doc = new Y.Doc();
|
|
44
|
+
Y.applyUpdate(doc, new Uint8Array(Buffer.from(stateBase64, 'base64')));
|
|
45
|
+
return doc;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function captureEdit(doc, mutate) {
|
|
49
|
+
let captured = null;
|
|
50
|
+
const listener = (update) => { captured = update; };
|
|
51
|
+
doc.on('update', listener);
|
|
52
|
+
mutate(doc.getText('content'));
|
|
53
|
+
doc.off('update', listener);
|
|
54
|
+
assert.ok(captured);
|
|
55
|
+
return Buffer.from(captured).toString('base64');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
test.after(() => {
|
|
59
|
+
docs.closeAllDocs();
|
|
60
|
+
closeLocalDb();
|
|
61
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('promotion is durable, idempotent, and installs the runtime-owned cloud binding', () => {
|
|
65
|
+
const opened = docs.openDoc(target);
|
|
66
|
+
const queued = enqueuePromotion({
|
|
67
|
+
promotionId: 'prm_abcdefghijklmnop',
|
|
68
|
+
resourceId,
|
|
69
|
+
localResource: opened.resource,
|
|
70
|
+
localPath: opened.path,
|
|
71
|
+
contract: 'text-yjs@1',
|
|
72
|
+
schemaVersion: 1,
|
|
73
|
+
snapshotBase64: opened.state,
|
|
74
|
+
contentBytes: fs.statSync(target).size,
|
|
75
|
+
});
|
|
76
|
+
assert.equal(queued.duplicate, false);
|
|
77
|
+
assert.equal(enqueuePromotion({
|
|
78
|
+
promotionId: 'prm_abcdefghijklmnop',
|
|
79
|
+
resourceId,
|
|
80
|
+
localResource: opened.resource,
|
|
81
|
+
localPath: opened.path,
|
|
82
|
+
contract: 'text-yjs@1',
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
snapshotBase64: opened.state,
|
|
85
|
+
contentBytes: fs.statSync(target).size,
|
|
86
|
+
}).duplicate, true);
|
|
87
|
+
|
|
88
|
+
const claims = claimPromotions();
|
|
89
|
+
assert.equal(claims.length, 1);
|
|
90
|
+
assert.equal(claims[0].frame.resourceId, resourceId);
|
|
91
|
+
assert.equal(claims[0].frame.snapshotChecksum.length, 64);
|
|
92
|
+
|
|
93
|
+
const acknowledged = acknowledgePromotion('prm_abcdefghijklmnop', {
|
|
94
|
+
resourceId,
|
|
95
|
+
authorityEpoch: 1,
|
|
96
|
+
headVersion: 0,
|
|
97
|
+
snapshotVersion: 0,
|
|
98
|
+
snapshotKey: `shared/${resourceId}/snapshots/v0/${claims[0].frame.snapshotChecksum}.bin`,
|
|
99
|
+
snapshotChecksum: claims[0].frame.snapshotChecksum,
|
|
100
|
+
});
|
|
101
|
+
assert.equal(acknowledged.promotion.state, 'promoted');
|
|
102
|
+
assert.equal(acknowledgePromotion('prm_abcdefghijklmnop', {
|
|
103
|
+
resourceId,
|
|
104
|
+
authorityEpoch: 1,
|
|
105
|
+
headVersion: 0,
|
|
106
|
+
snapshotVersion: 0,
|
|
107
|
+
snapshotKey: acknowledged.promotion.snapshotKey,
|
|
108
|
+
snapshotChecksum: claims[0].frame.snapshotChecksum,
|
|
109
|
+
}).duplicate, true);
|
|
110
|
+
|
|
111
|
+
const replica = getReplicaByResourceId(resourceId);
|
|
112
|
+
assert.equal(replica.localPath, target);
|
|
113
|
+
assert.equal(replica.appliedVersion, 0);
|
|
114
|
+
assert.equal(replica.contract, 'text-yjs@1');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('batched surface edits retain separate IDs/versions and recipient file precedes UI event', () => {
|
|
118
|
+
const opened = docs.openDoc(target);
|
|
119
|
+
const client = clientFromState(opened.state);
|
|
120
|
+
const updateA = captureEdit(client, (text) => text.insert(text.length, 'A\n'));
|
|
121
|
+
const updateB = captureEdit(client, (text) => text.insert(text.length, 'B\n'));
|
|
122
|
+
const before = listEventsAfter(0).length;
|
|
123
|
+
|
|
124
|
+
const local = docs.applyDocMutations(target, [
|
|
125
|
+
{ mutationId: 'mut_shared_batch_001', update: updateA },
|
|
126
|
+
{ mutationId: 'mut_shared_batch_002', update: updateB },
|
|
127
|
+
]);
|
|
128
|
+
assert.equal(local.applied, 2);
|
|
129
|
+
assert.deepEqual(local.mutationIds, ['mut_shared_batch_001', 'mut_shared_batch_002']);
|
|
130
|
+
assert.equal(listEventsAfter(0).length, before + 2, 'one projected event per surface edit');
|
|
131
|
+
assert.equal(fs.readFileSync(target, 'utf8'), '# Shared\nA\nB\n', 'shared file is materialized before return');
|
|
132
|
+
|
|
133
|
+
const claims = claimCloudMutations();
|
|
134
|
+
assert.equal(claims.length, 2, 'one cloud envelope per edit even though transport was batched');
|
|
135
|
+
assert.deepEqual(claims.map((claim) => claim.envelope.mutationId), [
|
|
136
|
+
'mut_shared_batch_001',
|
|
137
|
+
'mut_shared_batch_002',
|
|
138
|
+
]);
|
|
139
|
+
acknowledgeCloudMutation(resourceId, 'mut_shared_batch_001', {
|
|
140
|
+
version: 1,
|
|
141
|
+
committedAt: '2026-07-14T20:00:00.000Z',
|
|
142
|
+
});
|
|
143
|
+
acknowledgeCloudMutation(resourceId, 'mut_shared_batch_002', {
|
|
144
|
+
version: 2,
|
|
145
|
+
committedAt: '2026-07-14T20:00:01.000Z',
|
|
146
|
+
});
|
|
147
|
+
assert.equal(getReplicaByResourceId(resourceId).appliedVersion, 2);
|
|
148
|
+
|
|
149
|
+
const remoteUpdate = captureEdit(client, (text) => text.insert(text.length, 'Remote\n'));
|
|
150
|
+
const remoteEnvelope = {
|
|
151
|
+
resourceId,
|
|
152
|
+
authorityEpoch: 1,
|
|
153
|
+
mutationId: 'mut_remote_shared_001',
|
|
154
|
+
deviceId: 'device-remote-test',
|
|
155
|
+
baseVersion: 2,
|
|
156
|
+
contract: 'text-yjs@1',
|
|
157
|
+
schemaVersion: 1,
|
|
158
|
+
operationKind: 'yjs.update',
|
|
159
|
+
operation: { kind: 'yjs.update', update: remoteUpdate },
|
|
160
|
+
};
|
|
161
|
+
let observedFile = null;
|
|
162
|
+
const unsubscribe = subscribeStateEvents((event) => {
|
|
163
|
+
if (event.mutationId === remoteEnvelope.mutationId) {
|
|
164
|
+
observedFile = fs.readFileSync(target, 'utf8');
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
const remote = docs.applyCloudDocMutation({
|
|
168
|
+
envelope: remoteEnvelope,
|
|
169
|
+
version: 3,
|
|
170
|
+
committedAt: '2026-07-14T20:00:02.000Z',
|
|
171
|
+
});
|
|
172
|
+
unsubscribe();
|
|
173
|
+
assert.equal(remote.mutation.state, 'synced');
|
|
174
|
+
assert.equal(observedFile, '# Shared\nA\nB\nRemote\n');
|
|
175
|
+
assert.equal(getReplicaByResourceId(resourceId).appliedVersion, 3);
|
|
176
|
+
|
|
177
|
+
const countAfterRemote = listEventsAfter(0).length;
|
|
178
|
+
assert.equal(docs.applyCloudDocMutation({
|
|
179
|
+
envelope: remoteEnvelope,
|
|
180
|
+
version: 3,
|
|
181
|
+
committedAt: '2026-07-14T20:00:02.000Z',
|
|
182
|
+
}).duplicate, true);
|
|
183
|
+
assert.equal(listEventsAfter(0).length, countAfterRemote, 'duplicate cloud delivery is invisible');
|
|
184
|
+
assert.equal(getMutation(`resource:${resourceId}`, remoteEnvelope.mutationId).officialCloudVersion, 3);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('a cloud version gap is rejected before file or UI materialization', () => {
|
|
188
|
+
const opened = docs.openDoc(target);
|
|
189
|
+
const client = clientFromState(opened.state);
|
|
190
|
+
const update = captureEdit(client, (text) => text.insert(text.length, 'Gap\n'));
|
|
191
|
+
const beforeFile = fs.readFileSync(target, 'utf8');
|
|
192
|
+
const beforeEvents = listEventsAfter(0).length;
|
|
193
|
+
assert.throws(() => docs.applyCloudDocMutation({
|
|
194
|
+
envelope: {
|
|
195
|
+
resourceId,
|
|
196
|
+
authorityEpoch: 1,
|
|
197
|
+
mutationId: 'mut_remote_gap_0001',
|
|
198
|
+
deviceId: 'device-remote-test',
|
|
199
|
+
baseVersion: 3,
|
|
200
|
+
contract: 'text-yjs@1',
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
operationKind: 'yjs.update',
|
|
203
|
+
operation: { kind: 'yjs.update', update },
|
|
204
|
+
},
|
|
205
|
+
version: 5,
|
|
206
|
+
committedAt: '2026-07-14T20:00:03.000Z',
|
|
207
|
+
}), /expected 4, received 5/);
|
|
208
|
+
assert.equal(fs.readFileSync(target, 'utf8'), beforeFile);
|
|
209
|
+
assert.equal(listEventsAfter(0).length, beforeEvents);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('reopen recovers a journaled shared edit that died before materialization', () => {
|
|
213
|
+
const opened = docs.openDoc(target);
|
|
214
|
+
const client = clientFromState(opened.state);
|
|
215
|
+
const update = captureEdit(client, (text) => text.insert(text.length, 'Recovered\n'));
|
|
216
|
+
const mutationId = 'mut_recovery_shared_1';
|
|
217
|
+
journalMutation({
|
|
218
|
+
channelId: `resource:${resourceId}`,
|
|
219
|
+
localResource: opened.resource,
|
|
220
|
+
sharedResourceId: resourceId,
|
|
221
|
+
mutationId,
|
|
222
|
+
deviceId: process.env.AMALGM_DEVICE_ID,
|
|
223
|
+
authorityEpoch: 1,
|
|
224
|
+
baseCloudVersion: 3,
|
|
225
|
+
contract: 'text-yjs@1',
|
|
226
|
+
schemaVersion: 1,
|
|
227
|
+
operationKind: 'yjs.update',
|
|
228
|
+
operation: { kind: 'yjs.update', update },
|
|
229
|
+
});
|
|
230
|
+
assert.equal(getMutation(`resource:${resourceId}`, mutationId).state, 'saving-local');
|
|
231
|
+
|
|
232
|
+
docs.closeAllDocs();
|
|
233
|
+
const reopened = docs.openDoc(target);
|
|
234
|
+
const recovered = clientFromState(reopened.state).getText('content').toString();
|
|
235
|
+
assert.match(recovered, /Recovered\n$/);
|
|
236
|
+
assert.match(fs.readFileSync(target, 'utf8'), /Recovered\n$/);
|
|
237
|
+
assert.equal(getMutation(`resource:${resourceId}`, mutationId).state, 'saved-local');
|
|
238
|
+
assert.equal(
|
|
239
|
+
listEventsAfter(0).filter((event) => event.mutationId === mutationId).length,
|
|
240
|
+
1,
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('authorized snapshot attachment creates the recipient file before its UI event', () => {
|
|
245
|
+
const recipientResourceId = 'shr_qrstuvwxyzabcdef';
|
|
246
|
+
const source = docs.openDoc(target);
|
|
247
|
+
const recipientPath = path.join(docDir, 'recipient.md');
|
|
248
|
+
fs.writeFileSync(recipientPath, '', 'utf8');
|
|
249
|
+
const queued = enqueueAttachment({
|
|
250
|
+
attachmentId: 'att_abcdefghijklmnop',
|
|
251
|
+
resourceId: recipientResourceId,
|
|
252
|
+
localPath: recipientPath,
|
|
253
|
+
});
|
|
254
|
+
assert.equal(queued.duplicate, false);
|
|
255
|
+
assert.equal(claimAttachments().length, 1);
|
|
256
|
+
|
|
257
|
+
const snapshot = Buffer.from(source.state, 'base64');
|
|
258
|
+
const checksum = require('crypto').createHash('sha256').update(snapshot).digest('hex');
|
|
259
|
+
let observedFile = null;
|
|
260
|
+
const unsubscribe = subscribeStateEvents((event) => {
|
|
261
|
+
if (event.sharedResourceId === recipientResourceId && event.source === 'doc:cloud-snapshot') {
|
|
262
|
+
observedFile = fs.readFileSync(recipientPath, 'utf8');
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
const attached = acknowledgeAttachment('att_abcdefghijklmnop', {
|
|
266
|
+
resourceId: recipientResourceId,
|
|
267
|
+
authorityEpoch: 1,
|
|
268
|
+
headVersion: 0,
|
|
269
|
+
snapshotVersion: 0,
|
|
270
|
+
snapshotChecksum: checksum,
|
|
271
|
+
snapshotBase64: source.state,
|
|
272
|
+
contract: 'text-yjs@1',
|
|
273
|
+
schemaVersion: 1,
|
|
274
|
+
});
|
|
275
|
+
unsubscribe();
|
|
276
|
+
assert.equal(attached.attachment.state, 'attached');
|
|
277
|
+
assert.equal(observedFile, fs.readFileSync(target, 'utf8'));
|
|
278
|
+
assert.equal(fs.readFileSync(recipientPath, 'utf8'), fs.readFileSync(target, 'utf8'));
|
|
279
|
+
assert.equal(getReplicaByResourceId(recipientResourceId).localPath, recipientPath);
|
|
280
|
+
assert.equal(acknowledgeAttachment('att_abcdefghijklmnop', {
|
|
281
|
+
resourceId: recipientResourceId,
|
|
282
|
+
}).duplicate, true);
|
|
283
|
+
});
|