amalgm 0.1.220 → 0.1.234

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.
@@ -24,13 +24,14 @@ const {
24
24
  readCheckpoint,
25
25
  repoKeyOfCard,
26
26
  sameTransition,
27
+ stampCheckpointMutation,
27
28
  } = require('./checkpoint');
29
+ const projectStream = require('./project-stream');
28
30
 
29
- const WALL_CONTRACT = 'code-project@4';
31
+ const { WALL_CONTRACT } = projectStream;
30
32
  const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects');
31
33
  const REGISTRY_FILE = path.join(PROJECT_DIR, 'projects.json');
32
34
  const PUBLISH_QUIET_MS = 25;
33
- const COMPACT_AT_BYTES = 1024 * 1024;
34
35
  const ENSURE_STEP_TIMEOUT_MS = 20_000;
35
36
  const ENSURE_INTERVAL_MS = 15_000;
36
37
  const ATTACH_REJECTED = /shared_resource_read_forbidden/;
@@ -46,10 +47,6 @@ const service = {
46
47
  lastPublishedAt: null,
47
48
  };
48
49
 
49
- function sha256(text) {
50
- return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
51
- }
52
-
53
50
  function readRegistry() {
54
51
  try {
55
52
  const parsed = JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
@@ -67,14 +64,14 @@ function writeRegistry(registry) {
67
64
  function projectResourceId(repoKey, ownerUserId = userId()) {
68
65
  if (!repoKey || !ownerUserId) return null;
69
66
  const digest = crypto.createHash('sha256')
70
- .update(`amalgm-code-project@1|${ownerUserId}|${repoKey}`)
67
+ .update(`amalgm-code-project@2|${ownerUserId}|${repoKey}`)
71
68
  .digest('base64url');
72
69
  return `shr_cp1${digest.slice(0, 40)}`;
73
70
  }
74
71
 
75
72
  function projectFile(resourceId) {
76
73
  const digest = crypto.createHash('sha256').update(resourceId).digest('hex').slice(0, 24);
77
- return path.join(PROJECT_DIR, `${digest}.jsonl`);
74
+ return path.join(PROJECT_DIR, `${digest}.json`);
78
75
  }
79
76
 
80
77
  function projectForRepo(repoKey) {
@@ -132,7 +129,8 @@ function projects() {
132
129
  }
133
130
 
134
131
  function wallReplica(project) {
135
- return require('../state/replicas').getReplicaByResourceId(project.resourceId);
132
+ const replica = require('../state/replicas').getReplicaByResourceId(project.resourceId);
133
+ return replica?.contract === projectStream.STREAM_CONTRACT ? replica : null;
136
134
  }
137
135
 
138
136
  function publicationStatus(repoKey, reduced = undefined) {
@@ -158,57 +156,38 @@ function publicationStatus(repoKey, reduced = undefined) {
158
156
  };
159
157
  }
160
158
 
161
- function parseWallLine(line) {
162
- try {
163
- const entry = JSON.parse(line);
164
- if (
165
- entry?.wall !== WALL_CONTRACT
166
- || !entry.project
167
- || !entry.machine
168
- || !entry.mutationId
169
- || !Array.isArray(entry.cards)
170
- ) return null;
171
- return entry;
172
- } catch {
173
- return null;
174
- }
175
- }
176
-
177
- function sealCardVersions(project, card) {
159
+ function sealCardVersions(project, card, record = null) {
160
+ const versionFor = (mutationId) => (
161
+ mutationId === record?.mutationId && validVersion(record?.version)
162
+ ? record.version
163
+ : officialVersion(project.resourceId, mutationId)
164
+ );
178
165
  return {
179
166
  ...card,
180
167
  positionVersion: validVersion(card.positionVersion)
181
168
  ? card.positionVersion
182
- : officialVersion(project.resourceId, card.positionMutationId),
169
+ : versionFor(card.positionMutationId),
183
170
  branches: (card.branches || []).map((ref) => ({
184
171
  ...ref,
185
172
  version: validVersion(ref.version)
186
173
  ? ref.version
187
- : officialVersion(project.resourceId, ref.mutationId),
174
+ : versionFor(ref.mutationId),
188
175
  })),
189
176
  checkpoints: (card.checkpoints || []).map((checkpoint) => ({
190
177
  ...checkpoint,
191
178
  version: validVersion(checkpoint.version)
192
179
  ? checkpoint.version
193
- : officialVersion(project.resourceId, checkpoint.mutationId),
180
+ : versionFor(checkpoint.mutationId),
194
181
  })),
195
182
  };
196
183
  }
197
184
 
198
185
  function readProjectWall(project) {
199
- if (!fs.existsSync(project.file)) return [];
200
- const entries = new Map();
201
- for (const line of require('../state/docs').getDocText(project.file).split('\n')) {
202
- if (!line) continue;
203
- const entry = parseWallLine(line);
204
- if (entry?.project !== project.repoKey) continue;
205
- entries.set(entry.machine, {
206
- ...entry,
207
- resourceId: project.resourceId,
208
- cards: entry.cards.map((card) => sealCardVersions(project, card)),
209
- });
210
- }
211
- return [...entries.values()];
186
+ return projectStream.records(project).map((entry) => ({
187
+ ...entry,
188
+ resourceId: project.resourceId,
189
+ cards: entry.cards.map((card) => sealCardVersions(project, card, entry)),
190
+ }));
212
191
  }
213
192
 
214
193
  function readWall(repoKey = null) {
@@ -228,7 +207,6 @@ function publishedCardFor(card) {
228
207
  || published.head?.branch !== card.head?.branch
229
208
  || published.head?.oid !== card.head?.oid
230
209
  || Boolean(published.head?.detached) !== Boolean(card.head?.detached)
231
- || hasObjectUpgrade(card, { card: published, branches: published.branches })
232
210
  ) return null;
233
211
  return published;
234
212
  }
@@ -304,16 +282,17 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousC
304
282
  return false;
305
283
  }
306
284
 
307
- function hasObjectUpgrade(card, plan) {
308
- if (!plan?.card || !samePosition(card, plan.card)) return false;
309
- if (card.objects?.kind === 'bundle' && plan.card.objects?.kind !== 'bundle') return true;
285
+ function hasPendingContributionObjects(card, plan) {
286
+ if (!plan?.card || !samePosition(card, plan.card)) {
287
+ return card.objects?.kind === 'pending';
288
+ }
310
289
  const shared = new Map((plan.branches || []).map((ref) => [ref.name, ref]));
311
290
  return (card.refs || []).some((ref) => {
312
- const onRail = shared.get(ref.name);
313
- return onRail
314
- && onRail.oid === ref.oid
315
- && ref.objects?.kind === 'bundle'
316
- && onRail.objects?.kind !== 'bundle';
291
+ const accepted = shared.get(ref.name);
292
+ const alreadyAvailable = accepted
293
+ && validVersion(accepted.version)
294
+ && (accepted.oid ?? null) === (ref.oid ?? null);
295
+ return !alreadyAvailable && ref.objects?.kind === 'pending';
317
296
  });
318
297
  }
319
298
 
@@ -348,16 +327,6 @@ function checkpointBaseFor(card, baseline, project, shared = null) {
348
327
  };
349
328
  }
350
329
 
351
- function hasCheckpointBaseUpgrade(card, plan, baseline, project) {
352
- const shared = plan?.card?.checkpointBase;
353
- const candidate = checkpointBaseFor(card, baseline, project, shared);
354
- return Boolean(shared && candidate) && (
355
- candidate.through === shared.through
356
- && candidate.value?.revision === shared.value?.revision
357
- && JSON.stringify(candidate.value) !== JSON.stringify(shared.value)
358
- );
359
- }
360
-
361
330
  function validVersion(value) {
362
331
  return Number.isInteger(value) && value > 0;
363
332
  }
@@ -373,6 +342,30 @@ function officialVersion(resourceId, mutationId) {
373
342
  return Number.isInteger(mutation?.officialCloudVersion) ? mutation.officialCloudVersion : null;
374
343
  }
375
344
 
345
+ function durableMutationVersion(project, mutationId) {
346
+ if (!project || !mutationId) return null;
347
+ for (const record of projectStream.records(project)) {
348
+ if (record.mutationId === mutationId && validVersion(record.version)) return record.version;
349
+ for (const card of record.cards) {
350
+ if (card.positionMutationId === mutationId) {
351
+ return validVersion(card.positionVersion) ? card.positionVersion : null;
352
+ }
353
+ const nested = [
354
+ ...(card.branches || []),
355
+ ...(card.checkpoints || []),
356
+ ].find((entry) => entry.mutationId === mutationId);
357
+ if (nested) return validVersion(nested.version) ? nested.version : null;
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+
363
+ function mutationVersion(resourceId, mutationId, repoKey = null) {
364
+ if (!resourceId) return null;
365
+ return officialVersion(resourceId, mutationId)
366
+ || durableMutationVersion(repoKey ? projectForRepo(repoKey) : null, mutationId);
367
+ }
368
+
376
369
  function sealedVersion(resourceId, mutationId, ...versions) {
377
370
  return versions.find(validVersion)
378
371
  || officialVersion(resourceId, mutationId)
@@ -537,41 +530,18 @@ function stampCards(cards, previousCards, sharedPlan, baseline, mutationId, proj
537
530
  });
538
531
  }
539
532
 
540
- function withoutPublishedAt(line) {
541
- const entry = parseWallLine(line);
542
- if (!entry) return line;
543
- const { publishedAt, mutationId, ...rest } = entry;
544
- return JSON.stringify(rest);
533
+ function cardsUseMutation(cards, mutationId) {
534
+ return cards.some((card) => (
535
+ card.positionMutationId === mutationId
536
+ || (card.branches || []).some((ref) => ref.mutationId === mutationId)
537
+ || (card.checkpoints || []).some((checkpoint) => checkpoint.mutationId === mutationId)
538
+ ));
545
539
  }
546
540
 
547
- /**
548
- * Keep every machine's newest complete record and delete the largest span
549
- * containing none of them. Repeating this as the wall grows removes all
550
- * obsolete history, even when a retired machine's last record is near the
551
- * beginning. Each pass is one deletion, so concurrent appends commute in Yjs.
552
- */
553
- function compactWallText(current, repoKey) {
554
- const latest = new Map();
555
- const lines = current.split('\n');
556
- let offset = 0;
557
- for (let index = 0; index < lines.length; index += 1) {
558
- const line = lines[index];
559
- const entry = line ? parseWallLine(line) : null;
560
- const end = offset + line.length + (index < lines.length - 1 ? 1 : 0);
561
- if (entry?.project === repoKey) latest.set(entry.machine, { start: offset, end });
562
- offset = end;
563
- }
564
- if (latest.size === 0) return current;
565
- const records = [...latest.values()].sort((left, right) => left.start - right.start);
566
- const gaps = [{ start: 0, end: records[0].start }];
567
- for (let index = 0; index < records.length - 1; index += 1) {
568
- gaps.push({ start: records[index].end, end: records[index + 1].start });
569
- }
570
- const gap = gaps.reduce((largest, candidate) => (
571
- candidate.end - candidate.start > largest.end - largest.start ? candidate : largest
572
- ));
573
- if (gap.end <= gap.start) return current;
574
- return `${current.slice(0, gap.start)}${current.slice(gap.end)}`;
541
+ function samePublishedRecord(left, right) {
542
+ if (!left || !right) return false;
543
+ const stripTransport = ({ publishedAt, mutationId, version, ...record }) => record;
544
+ return JSON.stringify(stripTransport(left)) === JSON.stringify(stripTransport(right));
575
545
  }
576
546
 
577
547
  function publishProject(project, rawCards) {
@@ -584,33 +554,26 @@ function publishProject(project, rawCards) {
584
554
  service.counters.publishSkips += 1;
585
555
  return { published: false, reason: 'repo-materializing', repoKey: project.repoKey };
586
556
  }
587
- const docs = require('../state/docs');
588
- let current = docs.getDocText(project.file);
589
- let previous = null;
590
- let previousEntry = null;
591
- for (const line of current.split('\n')) {
592
- if (!line) continue;
593
- const entry = parseWallLine(line);
594
- if (entry?.project === project.repoKey && entry.machine === machineId()) {
595
- previous = line;
596
- previousEntry = entry;
597
- }
598
- }
557
+ const previousEntry = projectStream.records(project)
558
+ .find((entry) => entry.machine === machineId()) || null;
599
559
 
600
560
  const reducer = require('./reducer');
601
561
  const sharedPlan = reducer.reduceWall(readProjectWall(project)).get(project.repoKey);
602
562
  const baseline = reducer.readRepoState(project.repoKey);
603
- const previousCards = (previousEntry?.cards || []).map((card) => sealCardVersions(project, card));
563
+ const previousCards = (previousEntry?.cards || [])
564
+ .map((card) => sealCardVersions(project, card, previousEntry));
604
565
  const previousReplicas = new Set(previousCards.map((card) => card.replica));
605
566
  const currentReplicas = new Set(rawCards.map((card) => card.replica));
606
567
  const removed = [...previousReplicas].some((replica) => !currentReplicas.has(replica));
607
568
  const contributes = rawCards.some((card) => {
608
569
  const checkpoint = readCheckpoint(machineId(), project.repoKey);
609
570
  const previousCard = previousCards.find((entry) => entry.replica === card.replica);
610
- return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard)
611
- || hasObjectUpgrade(card, sharedPlan)
612
- || hasCheckpointBaseUpgrade(card, sharedPlan, baseline, project);
571
+ return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard);
613
572
  });
573
+ if (contributes && rawCards.some((card) => hasPendingContributionObjects(card, sharedPlan))) {
574
+ service.counters.publishSkips += 1;
575
+ return { published: false, reason: 'objects-preparing', repoKey: project.repoKey };
576
+ }
614
577
  if (!removed && !contributes) {
615
578
  service.counters.publishSkips += 1;
616
579
  return { published: false, reason: 'shared-state-covered', repoKey: project.repoKey };
@@ -624,20 +587,22 @@ function publishProject(project, rawCards) {
624
587
  mutationId,
625
588
  project,
626
589
  );
627
- const mine = JSON.stringify({
590
+ const mine = {
628
591
  wall: WALL_CONTRACT,
629
592
  project: project.repoKey,
630
593
  machine: machineId(),
631
594
  mutationId,
632
595
  publishedAt: new Date().toISOString(),
633
596
  cards,
634
- });
635
- const unchanged = previous !== null
636
- && withoutPublishedAt(previous) === withoutPublishedAt(mine);
637
- if (unchanged) {
597
+ };
598
+ if (samePublishedRecord(previousEntry, mine)) {
638
599
  service.counters.publishSkips += 1;
639
600
  return { published: false, reason: 'unchanged', repoKey: project.repoKey };
640
601
  }
602
+ if (!cardsUseMutation(cards, mutationId)) {
603
+ service.counters.publishSkips += 1;
604
+ return { published: false, reason: 'no-content-change', repoKey: project.repoKey };
605
+ }
641
606
  const status = publicationStatus(project.repoKey, baseline);
642
607
  if (!status.current) {
643
608
  service.counters.publishSkips += 1;
@@ -649,31 +614,22 @@ function publishProject(project, rawCards) {
649
614
  };
650
615
  }
651
616
 
652
- const separator = current && !current.endsWith('\n') ? '\n' : '';
653
- if (Buffer.byteLength(`${current}${separator}${mine}\n`, 'utf8') > COMPACT_AT_BYTES) {
654
- const compacted = compactWallText(current, project.repoKey);
655
- if (compacted !== current) {
656
- docs.writeDocText(project.file, compacted, {
657
- baseHash: sha256(current),
658
- mutationId: `mut_project_compact_${crypto.randomBytes(16).toString('base64url')}`,
659
- });
660
- current = docs.getDocText(project.file);
617
+ for (const card of cards) {
618
+ const checkpoint = (card.checkpoints || []).find((entry) => (
619
+ entry.machine === machineId() && entry.mutationId === mutationId
620
+ ));
621
+ if (checkpoint) {
622
+ stampCheckpointMutation(machineId(), project.repoKey, checkpoint, mutationId);
661
623
  }
662
624
  }
663
- const nextSeparator = current && !current.endsWith('\n') ? '\n' : '';
664
- const result = docs.writeDocText(
665
- project.file,
666
- `${current}${nextSeparator}${mine}\n`,
667
- { baseHash: sha256(current), mutationId },
668
- );
625
+ const result = projectStream.publish(project, mine);
669
626
  service.counters.publishes += 1;
670
627
  service.lastPublishedAt = new Date().toISOString();
671
628
  return {
672
629
  published: true,
673
630
  repoKey: project.repoKey,
674
631
  resourceId: project.resourceId,
675
- mutationId: result.mutationId,
676
- merge: result.merge,
632
+ mutationId: result.mutation.mutationId,
677
633
  cards: cards.length,
678
634
  };
679
635
  }
@@ -748,6 +704,7 @@ async function ensureProject(project) {
748
704
  await callLocal(shared.handleAttachmentStart, {
749
705
  resourceId: project.resourceId,
750
706
  path: project.file,
707
+ contract: projectStream.STREAM_CONTRACT,
751
708
  });
752
709
  } else if (request.state === 'rejected') {
753
710
  if (
@@ -758,6 +715,9 @@ async function ensureProject(project) {
758
715
  await callLocal(shared.handlePromotionStart, {
759
716
  resourceId: project.resourceId,
760
717
  path: project.file,
718
+ project: project.repoKey,
719
+ contract: projectStream.STREAM_CONTRACT,
720
+ schemaVersion: 1,
761
721
  });
762
722
  await waitForSettled(project, () => promotions.getPromotionByResource(project.resourceId));
763
723
  }
@@ -802,13 +762,17 @@ function startWallService() {
802
762
  if (service.started) return;
803
763
  service.started = true;
804
764
  service.unsubscribe = subscribeStateEvents((event) => {
805
- const acknowledged = event.resource === 'shared-mutations'
806
- && projects().some((project) => project.resourceId === event.sharedResourceId);
765
+ const acknowledgedProject = event.resource === 'shared-mutations'
766
+ ? projects().find((project) => project.resourceId === event.sharedResourceId)
767
+ : null;
768
+ if (acknowledgedProject) {
769
+ projectStream.seal(acknowledgedProject, event.mutationId || event.id, event.resourceVersion);
770
+ }
807
771
  if (
808
772
  event.resource === 'repo-cards'
809
773
  || event.resource === 'repo-checkpoints'
810
774
  || event.resource === 'repo-objects'
811
- || acknowledged
775
+ || acknowledgedProject
812
776
  ) {
813
777
  ensureProjects().catch(() => {});
814
778
  schedulePublish();
@@ -865,12 +829,17 @@ function wallStats() {
865
829
  }
866
830
 
867
831
  module.exports = {
868
- _test: { publishSelected },
832
+ _test: {
833
+ cardsUseMutation,
834
+ hasPendingContributionObjects,
835
+ publishSelected,
836
+ samePublishedRecord,
837
+ },
869
838
  WALL_CONTRACT,
870
839
  bindProject,
871
- compactWallText,
872
840
  hasLocalContribution,
873
841
  isCommittedCard,
842
+ mutationVersion,
874
843
  projectFile,
875
844
  projectForRepo,
876
845
  projectResourceId,