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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.198",
3
+ "version": "0.1.199",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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
- const current = docs.getDocText(project.file);
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}${separator}${mine}\n`,
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,