@storacha/md-merge 0.7.0 → 0.8.0

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/dist/diff.js CHANGED
@@ -109,14 +109,19 @@ function diffGap(oldNodes, newNodes, oldStartIdx, newStartIdx, pathPrefix) {
109
109
  });
110
110
  oi++;
111
111
  }
112
- // Remaining new = inserts
113
- while (ni < newNodes.length) {
112
+ // Remaining new = inserts (batched into a single change)
113
+ if (ni < newNodes.length) {
114
+ const insertNodes = [];
115
+ const insertIdx = newStartIdx + ni;
116
+ while (ni < newNodes.length) {
117
+ insertNodes.push(newNodes[ni]);
118
+ ni++;
119
+ }
114
120
  changes.push({
115
121
  type: "insert",
116
- path: [...pathPrefix, newStartIdx + ni],
117
- nodes: [newNodes[ni]],
122
+ path: [...pathPrefix, insertIdx],
123
+ nodes: insertNodes,
118
124
  });
119
- ni++;
120
125
  }
121
126
  return changes;
122
127
  }
package/dist/rga-tree.js CHANGED
@@ -189,18 +189,20 @@ export function applyRGAChangeSet(root, changeset, compareEvents) {
189
189
  break;
190
190
  }
191
191
  case "insert": {
192
+ let afterId = change.afterId;
192
193
  for (const node of change.nodes ?? []) {
193
194
  const rgaNode = convertNode(node, changeset.event, compareEvents);
194
- currentRGA.insert(change.afterId, rgaNode, changeset.event);
195
+ afterId = currentRGA.insert(afterId, rgaNode, changeset.event);
195
196
  }
196
197
  break;
197
198
  }
198
199
  case "modify": {
199
200
  if (change.targetId)
200
201
  currentRGA.delete(change.targetId);
202
+ let afterId = change.afterId;
201
203
  for (const node of change.nodes ?? []) {
202
204
  const rgaNode = convertNode(node, changeset.event, compareEvents);
203
- currentRGA.insert(change.afterId, rgaNode, changeset.event);
205
+ afterId = currentRGA.insert(afterId, rgaNode, changeset.event);
204
206
  }
205
207
  break;
206
208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storacha/md-merge",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/diff.ts CHANGED
@@ -131,14 +131,19 @@ function diffGap(
131
131
  });
132
132
  oi++;
133
133
  }
134
- // Remaining new = inserts
135
- while (ni < newNodes.length) {
134
+ // Remaining new = inserts (batched into a single change)
135
+ if (ni < newNodes.length) {
136
+ const insertNodes: RootContent[] = [];
137
+ const insertIdx = newStartIdx + ni;
138
+ while (ni < newNodes.length) {
139
+ insertNodes.push(newNodes[ni] as RootContent);
140
+ ni++;
141
+ }
136
142
  changes.push({
137
143
  type: "insert",
138
- path: [...pathPrefix, newStartIdx + ni],
139
- nodes: [newNodes[ni] as RootContent],
144
+ path: [...pathPrefix, insertIdx],
145
+ nodes: insertNodes,
140
146
  });
141
- ni++;
142
147
  }
143
148
  return changes;
144
149
  }
package/src/rga-tree.ts CHANGED
@@ -272,25 +272,27 @@ export function applyRGAChangeSet<E extends RGAEvent>(
272
272
  break;
273
273
  }
274
274
  case "insert": {
275
+ let afterId = change.afterId;
275
276
  for (const node of change.nodes ?? []) {
276
277
  const rgaNode = convertNode(
277
278
  node as Node,
278
279
  changeset.event,
279
280
  compareEvents,
280
281
  );
281
- currentRGA.insert(change.afterId, rgaNode, changeset.event);
282
+ afterId = currentRGA.insert(afterId, rgaNode, changeset.event);
282
283
  }
283
284
  break;
284
285
  }
285
286
  case "modify": {
286
287
  if (change.targetId) currentRGA.delete(change.targetId);
288
+ let afterId = change.afterId;
287
289
  for (const node of change.nodes ?? []) {
288
290
  const rgaNode = convertNode(
289
291
  node as Node,
290
292
  changeset.event,
291
293
  compareEvents,
292
294
  );
293
- currentRGA.insert(change.afterId, rgaNode, changeset.event);
295
+ afterId = currentRGA.insert(afterId, rgaNode, changeset.event);
294
296
  }
295
297
  break;
296
298
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { parse, stringify } from "../src/parse.js";
3
- import { toRGATree, toMdast, applyMdastToRGATree, mergeRGATrees } from "../src/rga-tree.js";
3
+ import { toRGATree, toMdast, applyMdastToRGATree, mergeRGATrees, applyRGAChangeSet } from "../src/rga-tree.js";
4
4
  import {
5
5
  RGA,
6
6
  type RGAEvent,
@@ -321,3 +321,129 @@ describe("mergeRGATrees", () => {
321
321
  expect(tree2.children.nodes.size).toBe(origSize2);
322
322
  });
323
323
  });
324
+
325
+ describe("multi-append", () => {
326
+ it("handles appending multiple nodes at once", () => {
327
+ const oldMd = "# Hello\n\nOriginal.\n";
328
+ const newMd = "# Hello\n\nOriginal.\n\nSecond.\n\nThird.\n";
329
+ const oldRoot = parse(oldMd);
330
+ const newRoot = parse(newMd);
331
+ const tree = toRGATree(oldRoot, r1, cmp);
332
+
333
+ const r2 = new TestEvent("r2");
334
+ const updated = applyMdastToRGATree(tree, newRoot, r2, cmp);
335
+ const result = stringify(toMdast(updated));
336
+
337
+ expect(result).toContain("Original.");
338
+ expect(result).toContain("Second.");
339
+ expect(result).toContain("Third.");
340
+ // Verify order is preserved
341
+ const origIdx = result.indexOf("Original.");
342
+ const secIdx = result.indexOf("Second.");
343
+ const thirdIdx = result.indexOf("Third.");
344
+ expect(origIdx).toBeLessThan(secIdx);
345
+ expect(secIdx).toBeLessThan(thirdIdx);
346
+ });
347
+
348
+ it("handles appending to an empty children array", () => {
349
+ const oldMd = "";
350
+ const newMd = "# Hello\n\nWorld.\n";
351
+ const oldRoot = parse(oldMd);
352
+ const newRoot = parse(newMd);
353
+ const tree = toRGATree(oldRoot, r1, cmp);
354
+
355
+ const r2 = new TestEvent("r2");
356
+ const updated = applyMdastToRGATree(tree, newRoot, r2, cmp);
357
+ const result = stringify(toMdast(updated));
358
+
359
+ expect(result).toContain("Hello");
360
+ expect(result).toContain("World.");
361
+ });
362
+ });
363
+
364
+ describe("applyRGAChangeSet multi-node", () => {
365
+ const r2 = new TestEvent("r2");
366
+
367
+ it("insert with multiple nodes preserves order", () => {
368
+ const tree = toRGATree(parse("# Hello\n"), r1, cmp);
369
+ const afterId = tree.children.toNodes()[0].id; // after the heading
370
+
371
+ const changeset = {
372
+ event: r2,
373
+ changes: [{
374
+ type: "insert" as const,
375
+ parentPath: [],
376
+ afterId,
377
+ nodes: [
378
+ { type: "paragraph", children: [{ type: "text", value: "First" }] },
379
+ { type: "paragraph", children: [{ type: "text", value: "Second" }] },
380
+ { type: "paragraph", children: [{ type: "text", value: "Third" }] },
381
+ ] as any,
382
+ }],
383
+ };
384
+
385
+ const result = applyRGAChangeSet(tree, changeset, cmp);
386
+ const md = stringify(toMdast(result));
387
+
388
+ expect(md).toContain("First");
389
+ expect(md).toContain("Second");
390
+ expect(md).toContain("Third");
391
+ expect(md.indexOf("First")).toBeLessThan(md.indexOf("Second"));
392
+ expect(md.indexOf("Second")).toBeLessThan(md.indexOf("Third"));
393
+ });
394
+
395
+ it("modify with multiple replacement nodes preserves order", () => {
396
+ const tree = toRGATree(parse("# Hello\n\nOriginal.\n"), r1, cmp);
397
+ const nodes = tree.children.toNodes();
398
+ const targetId = nodes[1].id; // the paragraph
399
+ const afterId = nodes[0].id; // after the heading
400
+
401
+ const changeset = {
402
+ event: r2,
403
+ changes: [{
404
+ type: "modify" as const,
405
+ parentPath: [],
406
+ targetId,
407
+ afterId,
408
+ nodes: [
409
+ { type: "paragraph", children: [{ type: "text", value: "Replacement A" }] },
410
+ { type: "paragraph", children: [{ type: "text", value: "Replacement B" }] },
411
+ ] as any,
412
+ }],
413
+ };
414
+
415
+ const result = applyRGAChangeSet(tree, changeset, cmp);
416
+ const md = stringify(toMdast(result));
417
+
418
+ expect(md).not.toContain("Original.");
419
+ expect(md).toContain("Replacement A");
420
+ expect(md).toContain("Replacement B");
421
+ expect(md.indexOf("Replacement A")).toBeLessThan(md.indexOf("Replacement B"));
422
+ });
423
+
424
+ it("insert at start (no afterId) with multiple nodes preserves relative order", () => {
425
+ // Insert two nodes at root with no afterId. RGA ordering means they
426
+ // appear after existing r1 nodes (r2 > r1), but their relative order
427
+ // (A before B) is preserved via chained afterIds.
428
+ const tree = toRGATree(parse("# Existing\n"), r1, cmp);
429
+
430
+ const changeset = {
431
+ event: r2,
432
+ changes: [{
433
+ type: "insert" as const,
434
+ parentPath: [],
435
+ afterId: undefined,
436
+ nodes: [
437
+ { type: "paragraph", children: [{ type: "text", value: "Added A" }] },
438
+ { type: "paragraph", children: [{ type: "text", value: "Added B" }] },
439
+ ] as any,
440
+ }],
441
+ };
442
+
443
+ const result = applyRGAChangeSet(tree, changeset, cmp);
444
+ const md = stringify(toMdast(result));
445
+
446
+ // Relative order of the inserted nodes is preserved
447
+ expect(md.indexOf("Added A")).toBeLessThan(md.indexOf("Added B"));
448
+ });
449
+ });