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