amalgm 0.1.205 → 0.1.207
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/package.json
CHANGED
|
@@ -120,9 +120,11 @@ function hashText(text) {
|
|
|
120
120
|
function isInternalStateDoc(filePath) {
|
|
121
121
|
const resolved = path.resolve(filePath);
|
|
122
122
|
const parent = path.dirname(resolved);
|
|
123
|
+
const collection = path.basename(parent);
|
|
124
|
+
const isStateCollection = path.basename(path.dirname(parent)) === 'state';
|
|
123
125
|
return (
|
|
124
|
-
|
|
125
|
-
&&
|
|
126
|
+
isStateCollection
|
|
127
|
+
&& (collection === 'repo-checkpoints' || collection === 'code-projects')
|
|
126
128
|
) || /^machine-cards-v\d+\.jsonl$/.test(path.basename(resolved));
|
|
127
129
|
}
|
|
128
130
|
|
|
@@ -221,7 +223,7 @@ function resolveDocPath(pathInput) {
|
|
|
221
223
|
}
|
|
222
224
|
const stats = fs.statSync(resolved);
|
|
223
225
|
if (!stats.isFile()) throw invalid('path is not a file');
|
|
224
|
-
if (stats.size > MAX_DOC_BYTES) {
|
|
226
|
+
if (stats.size > MAX_DOC_BYTES && !isInternalStateDoc(resolved)) {
|
|
225
227
|
throw invalid(`file exceeds the ${MAX_DOC_BYTES}-byte live document limit`, 413);
|
|
226
228
|
}
|
|
227
229
|
return resolved;
|
|
@@ -135,19 +135,29 @@ test('external disk edits reconcile into the live doc via the watcher', async ()
|
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
test('internal protocol documents never treat their own disk flush as another writer', async () => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
const checkpointDir = path.join(process.env.AMALGM_DIR, 'state', 'repo-checkpoints');
|
|
139
|
+
const projectDir = path.join(process.env.AMALGM_DIR, 'state', 'code-projects');
|
|
140
|
+
fs.mkdirSync(checkpointDir, { recursive: true });
|
|
141
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
142
|
+
const target = path.join(checkpointDir, 'protocol.json');
|
|
143
|
+
const project = path.join(projectDir, 'project.jsonl');
|
|
141
144
|
fs.writeFileSync(target, '{"value":1}\n', 'utf8');
|
|
145
|
+
fs.writeFileSync(project, '{"value":1}\n', 'utf8');
|
|
142
146
|
const opened = docs.openDoc(target);
|
|
147
|
+
const openedProject = docs.openDoc(project);
|
|
143
148
|
const before = currentSeq();
|
|
144
149
|
|
|
145
150
|
assert.equal(docs._private.isInternalStateDoc(target), true);
|
|
151
|
+
assert.equal(docs._private.isInternalStateDoc(project), true);
|
|
146
152
|
fs.writeFileSync(target, '{"value":2}\n', 'utf8');
|
|
153
|
+
fs.writeFileSync(project, '{"value":2}\n', 'utf8');
|
|
147
154
|
await sleep(700);
|
|
148
155
|
|
|
149
156
|
assert.equal(docs.getDocText(target), '{"value":1}\n');
|
|
150
|
-
|
|
157
|
+
assert.equal(docs.getDocText(project), '{"value":1}\n');
|
|
158
|
+
const events = listEventsAfter(before).filter((event) => (
|
|
159
|
+
event.resource === opened.resource || event.resource === openedProject.resource
|
|
160
|
+
));
|
|
151
161
|
assert.equal(events.length, 0);
|
|
152
162
|
});
|
|
153
163
|
|
|
@@ -203,6 +213,11 @@ test('guards: binary, missing, oversized, and malformed inputs are rejected', ()
|
|
|
203
213
|
|
|
204
214
|
const big = writeDoc('big.md', 'x'.repeat(2 * 1024 * 1024 + 1));
|
|
205
215
|
assert.throws(() => docs.openDoc(big), /live document limit/);
|
|
216
|
+
const projectDir = path.join(process.env.AMALGM_DIR, 'state', 'code-projects');
|
|
217
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
218
|
+
const largeProtocol = path.join(projectDir, 'large-project.jsonl');
|
|
219
|
+
fs.writeFileSync(largeProtocol, 'x'.repeat(2 * 1024 * 1024 + 1));
|
|
220
|
+
assert.doesNotThrow(() => docs.openDoc(largeProtocol));
|
|
206
221
|
|
|
207
222
|
const target = writeDoc('guarded.md', 'ok\n');
|
|
208
223
|
assert.throws(() => docs.applyDocUpdates(target, [123]), /base64-encoded/);
|
|
@@ -364,6 +364,18 @@ test('side branches keep one rail operation while the checked-out card advances'
|
|
|
364
364
|
card = readWall(repoKey)[0].cards[0];
|
|
365
365
|
assert.equal(card.positionMutationId, advanced.mutationId);
|
|
366
366
|
assert.equal(card.branches.find((entry) => entry.name === branch).mutationId, created.mutationId);
|
|
367
|
+
|
|
368
|
+
acknowledgeCloudMutation(project.resourceId, advanced.mutationId, {
|
|
369
|
+
version: 4,
|
|
370
|
+
committedAt: '2026-07-23T00:00:05.000Z',
|
|
371
|
+
});
|
|
372
|
+
await run(repo, 'branch', '-D', branch);
|
|
373
|
+
await refreshRepoCards();
|
|
374
|
+
const deleted = publishWall(repoKey);
|
|
375
|
+
assert.equal(deleted.published, true);
|
|
376
|
+
card = readWall(repoKey)[0].cards[0];
|
|
377
|
+
assert.equal(card.branches.find((entry) => entry.name === branch).oid, null);
|
|
378
|
+
assert.equal(card.branches.find((entry) => entry.name === branch).mutationId, deleted.mutationId);
|
|
367
379
|
});
|
|
368
380
|
|
|
369
381
|
test('peer records and earlier local history survive later publishes', async () => {
|
|
@@ -466,6 +478,30 @@ test('an unordered legacy record cannot pin wall compaction', () => {
|
|
|
466
478
|
);
|
|
467
479
|
});
|
|
468
480
|
|
|
481
|
+
test('retired machine values stay represented without pinning obsolete active history', () => {
|
|
482
|
+
const line = (machine, value) => JSON.stringify({
|
|
483
|
+
wall: 'code-project@3',
|
|
484
|
+
project: repoKey,
|
|
485
|
+
machine,
|
|
486
|
+
mutationId: `mut_project_${machine}_${value}`,
|
|
487
|
+
cards: [{ value }],
|
|
488
|
+
});
|
|
489
|
+
const retiredA = line('retired-a', 'latest-retired-a');
|
|
490
|
+
const retiredB = line('retired-b', 'latest-retired-b');
|
|
491
|
+
const oldA = line('machine-a', 'old-a');
|
|
492
|
+
const oldB = line('machine-b', 'old-b');
|
|
493
|
+
const latestA = line('machine-a', 'latest-a');
|
|
494
|
+
const latestB = line('machine-b', 'latest-b');
|
|
495
|
+
|
|
496
|
+
assert.equal(
|
|
497
|
+
compactWallText(
|
|
498
|
+
`${retiredA}\n${retiredB}\n${oldA}\n${oldB}\n${latestA}\n${latestB}\n`,
|
|
499
|
+
repoKey,
|
|
500
|
+
),
|
|
501
|
+
`${retiredA}\n${retiredB}\n${latestA}\n${latestB}\n`,
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
469
505
|
test('a concurrent deletion-only compaction and append commute in Yjs', () => {
|
|
470
506
|
const Y = require('yjs');
|
|
471
507
|
const line = (machine, value) => JSON.stringify({
|
|
@@ -225,7 +225,7 @@ function oidMap(refs) {
|
|
|
225
225
|
* materialized. A local value equal to either that baseline or the current
|
|
226
226
|
* rail is not new work. Anything else is a local contribution.
|
|
227
227
|
*/
|
|
228
|
-
function hasLocalContribution(card, plan, baseline, checkpoint = null) {
|
|
228
|
+
function hasLocalContribution(card, plan, baseline, checkpoint = null, previousCard = null) {
|
|
229
229
|
if (!plan?.card) return true;
|
|
230
230
|
const onRail = samePosition(card, plan.card);
|
|
231
231
|
const atBaseline = baseline
|
|
@@ -236,11 +236,21 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null) {
|
|
|
236
236
|
const currentRefs = oidMap(card.refs);
|
|
237
237
|
const railRefs = oidMap(plan.branches);
|
|
238
238
|
const baselineRefs = oidMap(baseline?.branches);
|
|
239
|
-
|
|
239
|
+
const previousRefs = oidMap(previousCard?.branches || previousCard?.refs);
|
|
240
|
+
for (const name of new Set([
|
|
241
|
+
...currentRefs.keys(),
|
|
242
|
+
...baselineRefs.keys(),
|
|
243
|
+
...previousRefs.keys(),
|
|
244
|
+
])) {
|
|
240
245
|
const current = currentRefs.get(name) ?? null;
|
|
241
246
|
const shared = railRefs.get(name) ?? null;
|
|
242
247
|
const previous = baselineRefs.get(name) ?? null;
|
|
243
|
-
|
|
248
|
+
const changedFromPublished = previousRefs.has(name)
|
|
249
|
+
&& current !== (previousRefs.get(name) ?? null);
|
|
250
|
+
if (
|
|
251
|
+
current !== shared
|
|
252
|
+
&& (!baselineRefs.has(name) || current !== previous || changedFromPublished)
|
|
253
|
+
) return true;
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
if (
|
|
@@ -446,12 +456,10 @@ function withoutPublishedAt(line) {
|
|
|
446
456
|
}
|
|
447
457
|
|
|
448
458
|
/**
|
|
449
|
-
* Keep
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* Compaction is deletion-only. Concurrent deletions and appends commute in
|
|
454
|
-
* Yjs, so this bounds the wall without bringing back whole-document races.
|
|
459
|
+
* Keep every machine's newest complete record and delete the largest span
|
|
460
|
+
* containing none of them. Repeating this as the wall grows removes all
|
|
461
|
+
* obsolete history, even when a retired machine's last record is near the
|
|
462
|
+
* beginning. Each pass is one deletion, so concurrent appends commute in Yjs.
|
|
455
463
|
*/
|
|
456
464
|
function compactWallText(current, repoKey) {
|
|
457
465
|
const latest = new Map();
|
|
@@ -460,12 +468,21 @@ function compactWallText(current, repoKey) {
|
|
|
460
468
|
for (let index = 0; index < lines.length; index += 1) {
|
|
461
469
|
const line = lines[index];
|
|
462
470
|
const entry = line ? parseWallLine(line) : null;
|
|
463
|
-
|
|
464
|
-
|
|
471
|
+
const end = offset + line.length + (index < lines.length - 1 ? 1 : 0);
|
|
472
|
+
if (entry?.project === repoKey) latest.set(entry.machine, { start: offset, end });
|
|
473
|
+
offset = end;
|
|
465
474
|
}
|
|
466
475
|
if (latest.size === 0) return current;
|
|
467
|
-
const
|
|
468
|
-
|
|
476
|
+
const records = [...latest.values()].sort((left, right) => left.start - right.start);
|
|
477
|
+
const gaps = [{ start: 0, end: records[0].start }];
|
|
478
|
+
for (let index = 0; index < records.length - 1; index += 1) {
|
|
479
|
+
gaps.push({ start: records[index].end, end: records[index + 1].start });
|
|
480
|
+
}
|
|
481
|
+
const gap = gaps.reduce((largest, candidate) => (
|
|
482
|
+
candidate.end - candidate.start > largest.end - largest.start ? candidate : largest
|
|
483
|
+
));
|
|
484
|
+
if (gap.end <= gap.start) return current;
|
|
485
|
+
return `${current.slice(0, gap.start)}${current.slice(gap.end)}`;
|
|
469
486
|
}
|
|
470
487
|
|
|
471
488
|
function publishProject(project, rawCards) {
|
|
@@ -499,7 +516,8 @@ function publishProject(project, rawCards) {
|
|
|
499
516
|
const removed = [...previousReplicas].some((replica) => !currentReplicas.has(replica));
|
|
500
517
|
const contributes = rawCards.some((card) => {
|
|
501
518
|
const checkpoint = readCheckpoint(machineId(), project.repoKey);
|
|
502
|
-
|
|
519
|
+
const previousCard = previousEntry?.cards?.find((entry) => entry.replica === card.replica);
|
|
520
|
+
return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard)
|
|
503
521
|
|| hasObjectUpgrade(card, sharedPlan);
|
|
504
522
|
});
|
|
505
523
|
if (!removed && !contributes) {
|