@loro-extended/change 5.3.0 → 5.4.1
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/README.md +85 -28
- package/dist/index.d.ts +291 -107
- package/dist/index.js +587 -36
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/change.test.ts +1 -1
- package/src/conversion.ts +40 -4
- package/src/diff-overlay.test.ts +95 -0
- package/src/diff-overlay.ts +10 -0
- package/src/discriminated-union-tojson.test.ts +2 -2
- package/src/fork-at.test.ts +1 -1
- package/src/functional-helpers.test.ts +50 -1
- package/src/functional-helpers.ts +152 -8
- package/src/index.ts +46 -18
- package/src/loro.ts +2 -1
- package/src/nested-container-materialization.test.ts +336 -0
- package/src/overlay-recursion.test.ts +8 -8
- package/src/replay-diff.test.ts +389 -0
- package/src/replay-diff.ts +229 -0
- package/src/shallow-fork.test.ts +302 -0
- package/src/shape.ts +7 -7
- package/src/typed-doc-ownkeys.test.ts +116 -0
- package/src/typed-doc.ts +33 -10
- package/src/typed-refs/base.ts +40 -4
- package/src/typed-refs/counter-ref-internals.ts +16 -2
- package/src/typed-refs/doc-ref-internals.ts +1 -0
- package/src/typed-refs/doc-ref-ownkeys.test.ts +78 -0
- package/src/typed-refs/index.ts +17 -0
- package/src/typed-refs/json-compatibility.test.ts +1 -1
- package/src/typed-refs/list-ref-base-internals.ts +2 -1
- package/src/typed-refs/list-ref-base.ts +79 -3
- package/src/typed-refs/record-ref-internals.ts +116 -2
- package/src/typed-refs/record-ref.test.ts +522 -1
- package/src/typed-refs/record-ref.ts +72 -3
- package/src/typed-refs/struct-ref-internals.ts +40 -3
- package/src/typed-refs/text-ref-internals.ts +70 -4
- package/src/typed-refs/tree-node-ref-internals.ts +14 -2
- package/src/typed-refs/tree-ref-internals.ts +2 -1
- package/src/typed-refs/utils.ts +65 -8
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ const schema = Shape.doc({
|
|
|
37
37
|
users: Shape.record(
|
|
38
38
|
Shape.plain.struct({
|
|
39
39
|
name: Shape.plain.string(),
|
|
40
|
-
})
|
|
40
|
+
}),
|
|
41
41
|
),
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -103,7 +103,7 @@ const blogSchema = Shape.doc({
|
|
|
103
103
|
heading: Shape.text(), // Collaborative headings
|
|
104
104
|
content: Shape.text(), // Collaborative content
|
|
105
105
|
order: Shape.plain.number(), // Plain metadata
|
|
106
|
-
})
|
|
106
|
+
}),
|
|
107
107
|
),
|
|
108
108
|
});
|
|
109
109
|
```
|
|
@@ -141,7 +141,7 @@ const blogSchemaWithDefaults = Shape.doc({
|
|
|
141
141
|
heading: Shape.text(),
|
|
142
142
|
content: Shape.text(),
|
|
143
143
|
order: Shape.plain.number(),
|
|
144
|
-
})
|
|
144
|
+
}),
|
|
145
145
|
),
|
|
146
146
|
});
|
|
147
147
|
|
|
@@ -228,7 +228,7 @@ import { change } from "@loro-extended/change";
|
|
|
228
228
|
// Library code - expose only the ref, not the doc
|
|
229
229
|
class StateMachine {
|
|
230
230
|
private doc: TypedDoc<...>;
|
|
231
|
-
|
|
231
|
+
|
|
232
232
|
get states(): TreeRef<StateNodeShape> {
|
|
233
233
|
return this.doc.states;
|
|
234
234
|
}
|
|
@@ -239,7 +239,7 @@ function addStates(states: TreeRef<StateNodeShape>) {
|
|
|
239
239
|
change(states, draft => {
|
|
240
240
|
const idle = draft.createNode();
|
|
241
241
|
idle.data.name.insert(0, "idle");
|
|
242
|
-
|
|
242
|
+
|
|
243
243
|
const running = draft.createNode();
|
|
244
244
|
running.data.name.insert(0, "running");
|
|
245
245
|
});
|
|
@@ -251,6 +251,7 @@ addStates(machine.states); // No access to the underlying doc needed!
|
|
|
251
251
|
```
|
|
252
252
|
|
|
253
253
|
This pattern is useful for:
|
|
254
|
+
|
|
254
255
|
- **Library APIs**: Expose typed refs without leaking document structure
|
|
255
256
|
- **Component isolation**: Pass refs to components that only need partial access
|
|
256
257
|
- **Testing**: Mock or stub individual refs without full document setup
|
|
@@ -259,37 +260,37 @@ All ref types support `change()`:
|
|
|
259
260
|
|
|
260
261
|
```typescript
|
|
261
262
|
// ListRef
|
|
262
|
-
change(doc.items, draft => {
|
|
263
|
+
change(doc.items, (draft) => {
|
|
263
264
|
draft.push("item1");
|
|
264
265
|
draft.push("item2");
|
|
265
266
|
});
|
|
266
267
|
|
|
267
268
|
// TextRef
|
|
268
|
-
change(doc.title, draft => {
|
|
269
|
+
change(doc.title, (draft) => {
|
|
269
270
|
draft.insert(0, "Hello ");
|
|
270
271
|
draft.insert(6, "World");
|
|
271
272
|
});
|
|
272
273
|
|
|
273
274
|
// CounterRef
|
|
274
|
-
change(doc.count, draft => {
|
|
275
|
+
change(doc.count, (draft) => {
|
|
275
276
|
draft.increment(5);
|
|
276
277
|
draft.decrement(2);
|
|
277
278
|
});
|
|
278
279
|
|
|
279
280
|
// StructRef
|
|
280
|
-
change(doc.profile, draft => {
|
|
281
|
+
change(doc.profile, (draft) => {
|
|
281
282
|
draft.bio.insert(0, "Hello");
|
|
282
283
|
draft.age.increment(1);
|
|
283
284
|
});
|
|
284
285
|
|
|
285
286
|
// RecordRef
|
|
286
|
-
change(doc.users, draft => {
|
|
287
|
+
change(doc.users, (draft) => {
|
|
287
288
|
draft.set("alice", { name: "Alice" });
|
|
288
289
|
draft.set("bob", { name: "Bob" });
|
|
289
290
|
});
|
|
290
291
|
|
|
291
292
|
// TreeRef
|
|
292
|
-
change(doc.tree, draft => {
|
|
293
|
+
change(doc.tree, (draft) => {
|
|
293
294
|
const node = draft.createNode();
|
|
294
295
|
node.data.name.insert(0, "root");
|
|
295
296
|
});
|
|
@@ -298,14 +299,14 @@ change(doc.tree, draft => {
|
|
|
298
299
|
Nested `change()` calls are safe - Loro's commit is idempotent:
|
|
299
300
|
|
|
300
301
|
```typescript
|
|
301
|
-
change(doc.items, outer => {
|
|
302
|
+
change(doc.items, (outer) => {
|
|
302
303
|
outer.push("from outer");
|
|
303
|
-
|
|
304
|
+
|
|
304
305
|
// Nested change on a different ref - works correctly
|
|
305
|
-
change(doc.count, inner => {
|
|
306
|
+
change(doc.count, (inner) => {
|
|
306
307
|
inner.increment(10);
|
|
307
308
|
});
|
|
308
|
-
|
|
309
|
+
|
|
309
310
|
outer.push("still in outer");
|
|
310
311
|
});
|
|
311
312
|
// All mutations are committed
|
|
@@ -336,7 +337,7 @@ const ServerPresenceShape = Shape.plain.struct({
|
|
|
336
337
|
Shape.plain.struct({
|
|
337
338
|
x: Shape.plain.number(),
|
|
338
339
|
y: Shape.plain.number(),
|
|
339
|
-
})
|
|
340
|
+
}),
|
|
340
341
|
),
|
|
341
342
|
tick: Shape.plain.number(),
|
|
342
343
|
});
|
|
@@ -516,7 +517,7 @@ handle.subscribe(
|
|
|
516
517
|
(titles, prev) => {
|
|
517
518
|
// titles: string[], prev: string[] | undefined
|
|
518
519
|
console.log("Titles changed:", titles);
|
|
519
|
-
}
|
|
520
|
+
},
|
|
520
521
|
);
|
|
521
522
|
|
|
522
523
|
// DSL constructs:
|
|
@@ -605,15 +606,18 @@ loro(doc).rawValue; // Unmerged CRDT value
|
|
|
605
606
|
|
|
606
607
|
**RecordRef** (Map-like interface)
|
|
607
608
|
|
|
608
|
-
| Direct Access
|
|
609
|
-
|
|
|
610
|
-
| `get(key)`
|
|
611
|
-
| `set(key, value)`
|
|
612
|
-
| `delete(key)`
|
|
613
|
-
| `has(key)`
|
|
614
|
-
| `keys()`, `values()` | |
|
|
615
|
-
| `size`
|
|
616
|
-
| `
|
|
609
|
+
| Direct Access | Only via `loro()` |
|
|
610
|
+
| --------------------------------- | ------------------------------ |
|
|
611
|
+
| `get(key)` | `setContainer(key, container)` |
|
|
612
|
+
| `set(key, value)` | `subscribe(callback)` |
|
|
613
|
+
| `delete(key)` | `doc` |
|
|
614
|
+
| `has(key)` | `container` |
|
|
615
|
+
| `keys()`, `values()`, `entries()` | |
|
|
616
|
+
| `size` | |
|
|
617
|
+
| `replace(values)` | |
|
|
618
|
+
| `merge(values)` | |
|
|
619
|
+
| `clear()` | |
|
|
620
|
+
| `toJSON()` | |
|
|
617
621
|
|
|
618
622
|
**TextRef**
|
|
619
623
|
|
|
@@ -665,6 +669,25 @@ function TextEditor({ textRef }: { textRef: TextRef }) {
|
|
|
665
669
|
}
|
|
666
670
|
```
|
|
667
671
|
|
|
672
|
+
### Subscribing to Document Transitions
|
|
673
|
+
|
|
674
|
+
Use `getTransition()` to build `{ before, after }` TypedDocs from a
|
|
675
|
+
subscription event using the diff overlay (no checkout or fork required):
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
import { getTransition, loro } from "@loro-extended/change";
|
|
679
|
+
|
|
680
|
+
const unsubscribe = loro(doc).subscribe((event) => {
|
|
681
|
+
if (event.by === "checkout") return;
|
|
682
|
+
|
|
683
|
+
const { before, after } = getTransition(doc, event);
|
|
684
|
+
|
|
685
|
+
if (!before.users.has("alice") && after.users.has("alice")) {
|
|
686
|
+
console.log("Alice just joined");
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
```
|
|
690
|
+
|
|
668
691
|
### Schema Builders
|
|
669
692
|
|
|
670
693
|
#### `Shape.doc(shape)`
|
|
@@ -906,6 +929,40 @@ draft.metadata.values();
|
|
|
906
929
|
const value = draft.metadata.get("key");
|
|
907
930
|
```
|
|
908
931
|
|
|
932
|
+
### Record Bulk Update Operations
|
|
933
|
+
|
|
934
|
+
Records support bulk update methods for efficient batch operations:
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
// Replace entire contents - keys not in the new object are removed
|
|
938
|
+
draft.players.replace({
|
|
939
|
+
alice: { name: "Alice", score: 100 },
|
|
940
|
+
bob: { name: "Bob", score: 50 },
|
|
941
|
+
});
|
|
942
|
+
// Result: only alice and bob exist, any previous entries are removed
|
|
943
|
+
|
|
944
|
+
// Merge values - existing keys not in the new object are kept
|
|
945
|
+
draft.scores.merge({
|
|
946
|
+
alice: 150, // updates alice
|
|
947
|
+
charlie: 25, // adds charlie
|
|
948
|
+
});
|
|
949
|
+
// Result: alice=150, bob=50 (unchanged), charlie=25
|
|
950
|
+
|
|
951
|
+
// Clear all entries
|
|
952
|
+
draft.history.clear();
|
|
953
|
+
// Result: empty record
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Method semantics:**
|
|
957
|
+
|
|
958
|
+
| Method | Adds new | Updates existing | Removes absent |
|
|
959
|
+
| ----------------- | -------- | ---------------- | -------------- |
|
|
960
|
+
| `replace(values)` | ✅ | ✅ | ✅ |
|
|
961
|
+
| `merge(values)` | ✅ | ✅ | ❌ |
|
|
962
|
+
| `clear()` | ❌ | ❌ | ✅ (all) |
|
|
963
|
+
|
|
964
|
+
These methods batch all operations into a single commit, avoiding multiple subscription notifications.
|
|
965
|
+
|
|
909
966
|
### Tree Operations
|
|
910
967
|
|
|
911
968
|
Trees are hierarchical structures where each node has typed metadata. Perfect for state machines, file systems, org charts, and nested data.
|
|
@@ -920,7 +977,7 @@ const StateNodeDataShape = Shape.struct({
|
|
|
920
977
|
name: Shape.plain.string(),
|
|
921
978
|
rego: Shape.plain.string(),
|
|
922
979
|
description: Shape.plain.string().nullable(),
|
|
923
|
-
})
|
|
980
|
+
}),
|
|
924
981
|
),
|
|
925
982
|
});
|
|
926
983
|
|
|
@@ -1062,7 +1119,7 @@ const todoSchema = Shape.doc({
|
|
|
1062
1119
|
id: Shape.plain.string(),
|
|
1063
1120
|
text: Shape.plain.string(),
|
|
1064
1121
|
done: Shape.plain.boolean(),
|
|
1065
|
-
})
|
|
1122
|
+
}),
|
|
1066
1123
|
),
|
|
1067
1124
|
});
|
|
1068
1125
|
|