amalgm 0.1.198 → 0.1.200
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
|
@@ -31,6 +31,7 @@ const { refreshRepoCards } = require('../workspace/cards');
|
|
|
31
31
|
const { checkpointFile, repoKeyOfCard } = require('../workspace/checkpoint');
|
|
32
32
|
const {
|
|
33
33
|
bindProject,
|
|
34
|
+
compactWallText,
|
|
34
35
|
hasLocalContribution,
|
|
35
36
|
isCommittedCard,
|
|
36
37
|
projectForRepo,
|
|
@@ -352,6 +353,7 @@ test('peer records and earlier local history survive later publishes', async ()
|
|
|
352
353
|
wall: 'code-project@3',
|
|
353
354
|
project: repoKey,
|
|
354
355
|
machine: 'machine-b',
|
|
356
|
+
mutationId: 'mut_project_peer',
|
|
355
357
|
publishedAt: '2026-07-19T00:00:00.000Z',
|
|
356
358
|
cards: [{ card: 'repo-card@2', machine: 'machine-b', replica: 'rpl_peer', path: '/work/app', head: { branch: 'preview' } }],
|
|
357
359
|
});
|
|
@@ -401,6 +403,103 @@ test('three concurrent append records stay whole under Yjs ordering', () => {
|
|
|
401
403
|
);
|
|
402
404
|
});
|
|
403
405
|
|
|
406
|
+
test('wall compaction deletes only records superseded for every machine', () => {
|
|
407
|
+
const line = (machine, value) => JSON.stringify({
|
|
408
|
+
wall: 'code-project@3',
|
|
409
|
+
project: repoKey,
|
|
410
|
+
machine,
|
|
411
|
+
mutationId: `mut_project_${machine}_${value}`,
|
|
412
|
+
cards: [{ value }],
|
|
413
|
+
});
|
|
414
|
+
const oldA = line('machine-a', 'old-a');
|
|
415
|
+
const oldB = line('machine-b', 'old-b');
|
|
416
|
+
const latestA = line('machine-a', 'latest-a');
|
|
417
|
+
const torn = '{"wall":"code-project@3","machine":"torn"';
|
|
418
|
+
const latestB = line('machine-b', 'latest-b');
|
|
419
|
+
const current = `${oldA}\n${oldB}\n${latestA}\n${torn}\n${latestB}\n`;
|
|
420
|
+
const compacted = compactWallText(current, repoKey);
|
|
421
|
+
|
|
422
|
+
assert.equal(compacted, `${latestA}\n${torn}\n${latestB}\n`);
|
|
423
|
+
assert.equal(compacted.includes(oldA), false);
|
|
424
|
+
assert.equal(compacted.includes(oldB), false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('an unordered legacy record cannot pin wall compaction', () => {
|
|
428
|
+
const unordered = JSON.stringify({
|
|
429
|
+
wall: 'code-project@3',
|
|
430
|
+
project: repoKey,
|
|
431
|
+
machine: '16.612Z',
|
|
432
|
+
cards: [{ value: 'corrupted-legacy-record' }],
|
|
433
|
+
});
|
|
434
|
+
const ordered = (machine) => JSON.stringify({
|
|
435
|
+
wall: 'code-project@3',
|
|
436
|
+
project: repoKey,
|
|
437
|
+
machine,
|
|
438
|
+
mutationId: `mut_project_${machine}`,
|
|
439
|
+
cards: [{ value: machine }],
|
|
440
|
+
});
|
|
441
|
+
const latestA = ordered('machine-a');
|
|
442
|
+
const latestB = ordered('machine-b');
|
|
443
|
+
|
|
444
|
+
assert.equal(
|
|
445
|
+
compactWallText(`${unordered}\n${latestA}\n${latestB}\n`, repoKey),
|
|
446
|
+
`${latestA}\n${latestB}\n`,
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('a concurrent deletion-only compaction and append commute in Yjs', () => {
|
|
451
|
+
const Y = require('yjs');
|
|
452
|
+
const line = (machine, value) => JSON.stringify({
|
|
453
|
+
wall: 'code-project@3',
|
|
454
|
+
project: repoKey,
|
|
455
|
+
machine,
|
|
456
|
+
mutationId: `mut_project_${machine}_${value}`,
|
|
457
|
+
cards: [{ value }],
|
|
458
|
+
});
|
|
459
|
+
const oldA = line('machine-a', 'old-a');
|
|
460
|
+
const oldB = line('machine-b', 'old-b');
|
|
461
|
+
const latestA = line('machine-a', 'latest-a');
|
|
462
|
+
const latestB = line('machine-b', 'latest-b');
|
|
463
|
+
const baseText = `${oldA}\n${oldB}\n${latestA}\n${latestB}\n`;
|
|
464
|
+
const compacted = compactWallText(baseText, repoKey);
|
|
465
|
+
const base = new Y.Doc();
|
|
466
|
+
base.getText('content').insert(0, baseText);
|
|
467
|
+
const baseUpdate = Y.encodeStateAsUpdate(base);
|
|
468
|
+
|
|
469
|
+
const writerUpdate = (change) => {
|
|
470
|
+
const writer = new Y.Doc();
|
|
471
|
+
Y.applyUpdate(writer, baseUpdate);
|
|
472
|
+
let update = null;
|
|
473
|
+
writer.on('update', (value) => {
|
|
474
|
+
update = value;
|
|
475
|
+
});
|
|
476
|
+
change(writer.getText('content'));
|
|
477
|
+
return update;
|
|
478
|
+
};
|
|
479
|
+
const compactUpdate = writerUpdate((text) => {
|
|
480
|
+
text.delete(0, baseText.length - compacted.length);
|
|
481
|
+
});
|
|
482
|
+
const appendLine = `${line('machine-c', 'latest-c')}\n`;
|
|
483
|
+
const appendUpdate = writerUpdate((text) => {
|
|
484
|
+
text.insert(text.length, appendLine);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const replicas = [
|
|
488
|
+
[compactUpdate, appendUpdate],
|
|
489
|
+
[appendUpdate, compactUpdate],
|
|
490
|
+
].map((order) => {
|
|
491
|
+
const replica = new Y.Doc();
|
|
492
|
+
Y.applyUpdate(replica, baseUpdate);
|
|
493
|
+
for (const update of order) Y.applyUpdate(replica, update);
|
|
494
|
+
return replica.getText('content').toString();
|
|
495
|
+
});
|
|
496
|
+
assert.equal(replicas[0], replicas[1]);
|
|
497
|
+
assert.deepEqual(
|
|
498
|
+
replicas[0].trim().split('\n').map((entry) => JSON.parse(entry).cards[0].value),
|
|
499
|
+
['latest-a', 'latest-b', 'latest-c'],
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
|
|
404
503
|
test('a torn line is ignored by readers, never fatal', () => {
|
|
405
504
|
writeDocText(
|
|
406
505
|
project.file,
|
|
@@ -409,6 +508,23 @@ test('a torn line is ignored by readers, never fatal', () => {
|
|
|
409
508
|
assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
|
|
410
509
|
});
|
|
411
510
|
|
|
511
|
+
test('publishing compacts an oversized history before appending the new record', async () => {
|
|
512
|
+
const before = getDocText(project.file);
|
|
513
|
+
writeDocText(project.file, `${'obsolete\n'.repeat(132_000)}${before}`);
|
|
514
|
+
assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') > 1024 * 1024);
|
|
515
|
+
|
|
516
|
+
const repo = path.join(tempRoot, 'projects/app');
|
|
517
|
+
fs.writeFileSync(path.join(repo, 'after-compaction.txt'), 'bounded\n');
|
|
518
|
+
await run(repo, 'add', '.');
|
|
519
|
+
await run(repo, 'commit', '-m', 'publish after compaction');
|
|
520
|
+
await refreshRepoCards();
|
|
521
|
+
const result = publishWall(repoKey);
|
|
522
|
+
|
|
523
|
+
assert.equal(result.published, true);
|
|
524
|
+
assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') < 1024 * 1024);
|
|
525
|
+
assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
|
|
526
|
+
});
|
|
527
|
+
|
|
412
528
|
test('project stats name every project resource, binding, and publish counter', () => {
|
|
413
529
|
const stats = wallStats();
|
|
414
530
|
const row = stats.projects.find((entry) => entry.repoKey === repoKey);
|
|
@@ -23,6 +23,7 @@ const WALL_CONTRACT = 'code-project@3';
|
|
|
23
23
|
const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects');
|
|
24
24
|
const REGISTRY_FILE = path.join(PROJECT_DIR, 'projects.json');
|
|
25
25
|
const PUBLISH_QUIET_MS = 25;
|
|
26
|
+
const COMPACT_AT_BYTES = 1024 * 1024;
|
|
26
27
|
const ENSURE_STEP_TIMEOUT_MS = 20_000;
|
|
27
28
|
const ENSURE_INTERVAL_MS = 15_000;
|
|
28
29
|
const ATTACH_REJECTED = /shared_resource_read_forbidden/;
|
|
@@ -134,6 +135,7 @@ function parseWallLine(line) {
|
|
|
134
135
|
entry?.wall !== WALL_CONTRACT
|
|
135
136
|
|| !entry.project
|
|
136
137
|
|| !entry.machine
|
|
138
|
+
|| !entry.mutationId
|
|
137
139
|
|| !Array.isArray(entry.cards)
|
|
138
140
|
) return null;
|
|
139
141
|
return entry;
|
|
@@ -442,6 +444,29 @@ function withoutPublishedAt(line) {
|
|
|
442
444
|
return JSON.stringify(rest);
|
|
443
445
|
}
|
|
444
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Keep the newest complete record for every machine, plus anything after
|
|
449
|
+
* the earliest of those records. Everything before that point is obsolete:
|
|
450
|
+
* every machine represented there has a newer complete value later.
|
|
451
|
+
*
|
|
452
|
+
* Compaction is deletion-only. Concurrent deletions and appends commute in
|
|
453
|
+
* Yjs, so this bounds the wall without bringing back whole-document races.
|
|
454
|
+
*/
|
|
455
|
+
function compactWallText(current, repoKey) {
|
|
456
|
+
const latest = new Map();
|
|
457
|
+
const lines = current.split('\n');
|
|
458
|
+
let offset = 0;
|
|
459
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
460
|
+
const line = lines[index];
|
|
461
|
+
const entry = line ? parseWallLine(line) : null;
|
|
462
|
+
if (entry?.project === repoKey) latest.set(entry.machine, offset);
|
|
463
|
+
offset += line.length + (index < lines.length - 1 ? 1 : 0);
|
|
464
|
+
}
|
|
465
|
+
if (latest.size === 0) return current;
|
|
466
|
+
const cutoff = Math.min(...latest.values());
|
|
467
|
+
return cutoff > 0 ? current.slice(cutoff) : current;
|
|
468
|
+
}
|
|
469
|
+
|
|
445
470
|
function publishProject(project, rawCards) {
|
|
446
471
|
const replica = wallReplica(project);
|
|
447
472
|
if (!replica) {
|
|
@@ -449,7 +474,7 @@ function publishProject(project, rawCards) {
|
|
|
449
474
|
return { published: false, reason: 'project-not-bound', repoKey: project.repoKey };
|
|
450
475
|
}
|
|
451
476
|
const docs = require('../state/docs');
|
|
452
|
-
|
|
477
|
+
let current = docs.getDocText(project.file);
|
|
453
478
|
let previous = null;
|
|
454
479
|
let previousEntry = null;
|
|
455
480
|
for (const line of current.split('\n')) {
|
|
@@ -501,9 +526,20 @@ function publishProject(project, rawCards) {
|
|
|
501
526
|
}
|
|
502
527
|
|
|
503
528
|
const separator = current && !current.endsWith('\n') ? '\n' : '';
|
|
529
|
+
if (Buffer.byteLength(`${current}${separator}${mine}\n`, 'utf8') > COMPACT_AT_BYTES) {
|
|
530
|
+
const compacted = compactWallText(current, project.repoKey);
|
|
531
|
+
if (compacted !== current) {
|
|
532
|
+
docs.writeDocText(project.file, compacted, {
|
|
533
|
+
baseHash: sha256(current),
|
|
534
|
+
mutationId: `mut_project_compact_${crypto.randomBytes(16).toString('base64url')}`,
|
|
535
|
+
});
|
|
536
|
+
current = docs.getDocText(project.file);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const nextSeparator = current && !current.endsWith('\n') ? '\n' : '';
|
|
504
540
|
const result = docs.writeDocText(
|
|
505
541
|
project.file,
|
|
506
|
-
`${current}${
|
|
542
|
+
`${current}${nextSeparator}${mine}\n`,
|
|
507
543
|
{ baseHash: sha256(current), mutationId },
|
|
508
544
|
);
|
|
509
545
|
service.counters.publishes += 1;
|
|
@@ -686,6 +722,7 @@ function wallStats() {
|
|
|
686
722
|
module.exports = {
|
|
687
723
|
WALL_CONTRACT,
|
|
688
724
|
bindProject,
|
|
725
|
+
compactWallText,
|
|
689
726
|
hasLocalContribution,
|
|
690
727
|
isCommittedCard,
|
|
691
728
|
projectFile,
|