@loro-extended/change 1.0.0 → 1.0.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 +58 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,13 +28,13 @@ pnpm add @loro-extended/change loro-crdt
|
|
|
28
28
|
## Quick Start
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import { createTypedDoc, Shape,
|
|
31
|
+
import { createTypedDoc, Shape, change } from "@loro-extended/change";
|
|
32
32
|
|
|
33
33
|
// Define your document schema
|
|
34
34
|
const schema = Shape.doc({
|
|
35
35
|
title: Shape.text().placeholder("My Todo List"),
|
|
36
36
|
count: Shape.counter(),
|
|
37
|
-
users: Shape.record(Shape.plain.
|
|
37
|
+
users: Shape.record(Shape.plain.struct({
|
|
38
38
|
name: Shape.plain.string(),
|
|
39
39
|
})),
|
|
40
40
|
});
|
|
@@ -57,8 +57,8 @@ if ("alice" in doc.users) {
|
|
|
57
57
|
|
|
58
58
|
// Batched mutations - commit together (optional, for performance)
|
|
59
59
|
// Using functional helper (recommended)
|
|
60
|
-
|
|
61
|
-
draft.title.insert(0, "
|
|
60
|
+
change(doc, draft => {
|
|
61
|
+
draft.title.insert(0, "Change: ");
|
|
62
62
|
draft.count.increment(10);
|
|
63
63
|
draft.users.set("bob", { name: "Bob" });
|
|
64
64
|
});
|
|
@@ -66,7 +66,7 @@ batch(doc, draft => {
|
|
|
66
66
|
|
|
67
67
|
// Get JSON snapshot using functional helper
|
|
68
68
|
console.log(doc.toJSON());
|
|
69
|
-
// { title: "
|
|
69
|
+
// { title: "Change: 📝 Todo", count: 15, users: { alice: { name: "Alice" }, bob: { name: "Bob" } } }
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Note that this is even more useful in combination with `@loro-extended/react` (if your app uses React) and `@loro-extended/repo` for syncing between client/server or among peers.
|
|
@@ -88,8 +88,8 @@ const blogSchema = Shape.doc({
|
|
|
88
88
|
// Lists for ordered data
|
|
89
89
|
tags: Shape.list(Shape.plain.string()), // List of strings
|
|
90
90
|
|
|
91
|
-
//
|
|
92
|
-
metadata: Shape.
|
|
91
|
+
// Structs for structured data with fixed keys
|
|
92
|
+
metadata: Shape.struct({
|
|
93
93
|
author: Shape.plain.string(), // Plain values (POJOs)
|
|
94
94
|
publishedAt: Shape.plain.string(), // ISO date string
|
|
95
95
|
featured: Shape.plain.boolean(),
|
|
@@ -97,7 +97,7 @@ const blogSchema = Shape.doc({
|
|
|
97
97
|
|
|
98
98
|
// Movable lists for reorderable content
|
|
99
99
|
sections: Shape.movableList(
|
|
100
|
-
Shape.
|
|
100
|
+
Shape.struct({
|
|
101
101
|
heading: Shape.text(), // Collaborative headings
|
|
102
102
|
content: Shape.text(), // Collaborative content
|
|
103
103
|
order: Shape.plain.number(), // Plain metadata
|
|
@@ -106,7 +106,7 @@ const blogSchema = Shape.doc({
|
|
|
106
106
|
});
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
**NOTE:** Use `Shape.*` for collaborative containers and `Shape.plain.*` for plain values. Only put plain values inside Loro containers - a Loro container inside a plain JS
|
|
109
|
+
**NOTE:** Use `Shape.*` for collaborative containers and `Shape.plain.*` for plain values. Only put plain values inside Loro containers - a Loro container inside a plain JS struct or array won't work.
|
|
110
110
|
|
|
111
111
|
### Empty State Overlay
|
|
112
112
|
|
|
@@ -118,13 +118,13 @@ const blogSchemaWithDefaults = Shape.doc({
|
|
|
118
118
|
title: Shape.text().placeholder("Untitled Document"),
|
|
119
119
|
viewCount: Shape.counter(), // defaults to 0
|
|
120
120
|
tags: Shape.list(Shape.plain.string()), // defaults to []
|
|
121
|
-
metadata: Shape.
|
|
121
|
+
metadata: Shape.struct({
|
|
122
122
|
author: Shape.plain.string().placeholder("Anonymous"),
|
|
123
123
|
publishedAt: Shape.plain.string(), // defaults to ""
|
|
124
124
|
featured: Shape.plain.boolean(), // defaults to false
|
|
125
125
|
}),
|
|
126
126
|
sections: Shape.movableList(
|
|
127
|
-
Shape.
|
|
127
|
+
Shape.struct({
|
|
128
128
|
heading: Shape.text(),
|
|
129
129
|
content: Shape.text(),
|
|
130
130
|
order: Shape.plain.number(),
|
|
@@ -139,7 +139,7 @@ console.log(doc.toJSON());
|
|
|
139
139
|
// { title: "Untitled Document", viewCount: 0, ... }
|
|
140
140
|
|
|
141
141
|
// After changes, CRDT values take priority over empty state
|
|
142
|
-
|
|
142
|
+
change(doc, (draft) => {
|
|
143
143
|
draft.title.insert(0, "My Blog Post");
|
|
144
144
|
draft.viewCount.increment(10);
|
|
145
145
|
});
|
|
@@ -160,12 +160,12 @@ doc.viewCount.increment(1);
|
|
|
160
160
|
doc.tags.push("typescript");
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
For batched operations (better performance, atomic undo), use `
|
|
163
|
+
For batched operations (better performance, atomic undo), use `change()`:
|
|
164
164
|
|
|
165
165
|
```typescript
|
|
166
|
-
import {
|
|
166
|
+
import { change } from "@loro-extended/change";
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
change(doc, (draft) => {
|
|
169
169
|
// Text operations
|
|
170
170
|
draft.title.insert(0, "📝");
|
|
171
171
|
draft.title.delete(5, 3);
|
|
@@ -179,7 +179,7 @@ batch(doc, (draft) => {
|
|
|
179
179
|
draft.tags.insert(0, "loro");
|
|
180
180
|
draft.tags.delete(1, 1);
|
|
181
181
|
|
|
182
|
-
//
|
|
182
|
+
// Struct operations (POJO values)
|
|
183
183
|
draft.metadata.set("author", "John Doe");
|
|
184
184
|
draft.metadata.delete("featured");
|
|
185
185
|
|
|
@@ -193,21 +193,21 @@ batch(doc, (draft) => {
|
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
// All changes are committed atomically as one transaction
|
|
196
|
-
//
|
|
196
|
+
// change() returns the doc for chaining
|
|
197
197
|
console.log(doc.toJSON()); // Updated document state
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
### When to Use `
|
|
200
|
+
### When to Use `change()` vs Direct Mutations
|
|
201
201
|
|
|
202
202
|
| Use Case | Approach |
|
|
203
203
|
|----------|----------|
|
|
204
204
|
| Single mutation | Direct: `doc.count.increment(1)` |
|
|
205
|
-
| Multiple related mutations | Batched: `
|
|
206
|
-
| Atomic undo/redo | Batched: `
|
|
207
|
-
| Performance-critical bulk updates | Batched: `
|
|
205
|
+
| Multiple related mutations | Batched: `change(doc, d => { ... })` |
|
|
206
|
+
| Atomic undo/redo | Batched: `change(doc, d => { ... })` |
|
|
207
|
+
| Performance-critical bulk updates | Batched: `change(doc, d => { ... })` |
|
|
208
208
|
| Simple reads + writes | Direct: `doc.users.set(...)` |
|
|
209
209
|
|
|
210
|
-
> **Note:** The `$.change()`
|
|
210
|
+
> **Note:** The `$.change()` method is available as an escape hatch, but the functional `change()` helper is recommended for cleaner code.
|
|
211
211
|
|
|
212
212
|
## Advanced Usage
|
|
213
213
|
|
|
@@ -219,19 +219,19 @@ For type-safe tagged unions (like different message types or presence states), u
|
|
|
219
219
|
import { Shape, mergeValue } from "@loro-extended/change";
|
|
220
220
|
|
|
221
221
|
// Define variant shapes - each must have the discriminant key
|
|
222
|
-
const ClientPresenceShape = Shape.plain.
|
|
222
|
+
const ClientPresenceShape = Shape.plain.struct({
|
|
223
223
|
type: Shape.plain.string("client"), // Literal type for discrimination
|
|
224
224
|
name: Shape.plain.string(),
|
|
225
|
-
input: Shape.plain.
|
|
225
|
+
input: Shape.plain.struct({
|
|
226
226
|
force: Shape.plain.number(),
|
|
227
227
|
angle: Shape.plain.number(),
|
|
228
228
|
}),
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
const ServerPresenceShape = Shape.plain.
|
|
231
|
+
const ServerPresenceShape = Shape.plain.struct({
|
|
232
232
|
type: Shape.plain.string("server"), // Literal type for discrimination
|
|
233
233
|
cars: Shape.plain.record(
|
|
234
|
-
Shape.plain.
|
|
234
|
+
Shape.plain.struct({
|
|
235
235
|
x: Shape.plain.number(),
|
|
236
236
|
y: Shape.plain.number(),
|
|
237
237
|
})
|
|
@@ -287,11 +287,11 @@ Handle complex nested documents with ease:
|
|
|
287
287
|
|
|
288
288
|
```typescript
|
|
289
289
|
const complexSchema = Shape.doc({
|
|
290
|
-
article: Shape.
|
|
290
|
+
article: Shape.struct({
|
|
291
291
|
title: Shape.text(),
|
|
292
|
-
metadata: Shape.
|
|
292
|
+
metadata: Shape.struct({
|
|
293
293
|
views: Shape.counter(),
|
|
294
|
-
author: Shape.
|
|
294
|
+
author: Shape.struct({
|
|
295
295
|
name: Shape.plain.string(),
|
|
296
296
|
email: Shape.plain.string(),
|
|
297
297
|
}),
|
|
@@ -314,7 +314,7 @@ const emptyState = {
|
|
|
314
314
|
|
|
315
315
|
const doc = createTypedDoc(complexSchema);
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
change(doc, (draft) => {
|
|
318
318
|
draft.article.title.insert(0, "Deep Nesting Example");
|
|
319
319
|
draft.article.metadata.views.increment(5);
|
|
320
320
|
draft.article.metadata.author.name = "Alice"; // plain string update is captured and applied after closure
|
|
@@ -322,20 +322,20 @@ batch(doc, (draft) => {
|
|
|
322
322
|
});
|
|
323
323
|
```
|
|
324
324
|
|
|
325
|
-
###
|
|
325
|
+
### Struct Operations
|
|
326
326
|
|
|
327
|
-
For
|
|
327
|
+
For struct containers (fixed-key objects), use direct property access:
|
|
328
328
|
|
|
329
329
|
```typescript
|
|
330
330
|
const schema = Shape.doc({
|
|
331
|
-
settings: Shape.
|
|
331
|
+
settings: Shape.struct({
|
|
332
332
|
theme: Shape.plain.string(),
|
|
333
333
|
collapsed: Shape.plain.boolean(),
|
|
334
334
|
width: Shape.plain.number(),
|
|
335
335
|
}),
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
change(doc, (draft) => {
|
|
339
339
|
// Set individual values
|
|
340
340
|
draft.settings.theme = "dark";
|
|
341
341
|
draft.settings.collapsed = true;
|
|
@@ -350,11 +350,11 @@ Create lists containing CRDT containers for collaborative nested structures:
|
|
|
350
350
|
```typescript
|
|
351
351
|
const collaborativeSchema = Shape.doc({
|
|
352
352
|
articles: Shape.list(
|
|
353
|
-
Shape.
|
|
353
|
+
Shape.struct({
|
|
354
354
|
title: Shape.text(), // Collaborative title
|
|
355
355
|
content: Shape.text(), // Collaborative content
|
|
356
356
|
tags: Shape.list(Shape.plain.string()), // Collaborative tag list
|
|
357
|
-
metadata: Shape.plain.
|
|
357
|
+
metadata: Shape.plain.struct({
|
|
358
358
|
// Static metadata
|
|
359
359
|
authorId: Shape.plain.string(),
|
|
360
360
|
publishedAt: Shape.plain.string(),
|
|
@@ -363,7 +363,7 @@ const collaborativeSchema = Shape.doc({
|
|
|
363
363
|
),
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
-
|
|
366
|
+
change(doc, (draft) => {
|
|
367
367
|
// Push creates and configures nested containers automatically
|
|
368
368
|
draft.articles.push({
|
|
369
369
|
title: "Collaborative Article",
|
|
@@ -410,30 +410,28 @@ const doc = new TypedDoc(schema);
|
|
|
410
410
|
|
|
411
411
|
These functional helpers provide a cleaner API and are the recommended way to work with TypedDoc:
|
|
412
412
|
|
|
413
|
-
#### `
|
|
413
|
+
#### `change(doc, mutator)`
|
|
414
414
|
|
|
415
415
|
Batches multiple mutations into a single transaction. Returns the doc for chaining.
|
|
416
416
|
|
|
417
417
|
```typescript
|
|
418
|
-
import {
|
|
418
|
+
import { change } from "@loro-extended/change";
|
|
419
419
|
|
|
420
|
-
|
|
420
|
+
change(doc, (draft) => {
|
|
421
421
|
draft.title.insert(0, "Hello");
|
|
422
422
|
draft.count.increment(5);
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
-
// Chainable -
|
|
426
|
-
|
|
425
|
+
// Chainable - change returns the doc
|
|
426
|
+
change(doc, d => d.count.increment(1)).count.increment(2);
|
|
427
427
|
```
|
|
428
428
|
|
|
429
|
-
#### `toJSON(
|
|
429
|
+
#### `doc.toJSON()`
|
|
430
430
|
|
|
431
431
|
Returns the full plain JavaScript object representation of the document.
|
|
432
432
|
|
|
433
433
|
```typescript
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const snapshot = toJSON(doc);
|
|
434
|
+
const snapshot = doc.toJSON();
|
|
437
435
|
// { title: "Hello", count: 5, ... }
|
|
438
436
|
```
|
|
439
437
|
|
|
@@ -452,12 +450,12 @@ loroDoc.subscribe((event) => console.log("Changed:", event));
|
|
|
452
450
|
|
|
453
451
|
The `$` namespace provides access to meta-operations. While functional helpers are recommended, the `$` namespace is available for advanced use cases:
|
|
454
452
|
|
|
455
|
-
#### `doc.$.
|
|
453
|
+
#### `doc.$.change(mutator)`
|
|
456
454
|
|
|
457
|
-
Same as `
|
|
455
|
+
Same as `change(doc, mutator)`.
|
|
458
456
|
|
|
459
457
|
```typescript
|
|
460
|
-
doc.$.
|
|
458
|
+
doc.$.change((draft) => {
|
|
461
459
|
// Make changes to draft - all commit together
|
|
462
460
|
});
|
|
463
461
|
```
|
|
@@ -481,7 +479,7 @@ const schema = Shape.doc({
|
|
|
481
479
|
- `Shape.counter()` - Collaborative increment/decrement counters
|
|
482
480
|
- `Shape.list(itemSchema)` - Collaborative ordered lists
|
|
483
481
|
- `Shape.movableList(itemSchema)` - Collaborative reorderable lists
|
|
484
|
-
- `Shape.
|
|
482
|
+
- `Shape.struct(shape)` - Collaborative structs with fixed keys (uses LoroMap internally)
|
|
485
483
|
- `Shape.record(valueSchema)` - Collaborative key-value maps with dynamic string keys
|
|
486
484
|
- `Shape.tree(shape)` - Collaborative hierarchical tree structures (Note: incomplete implementation)
|
|
487
485
|
|
|
@@ -493,7 +491,7 @@ const schema = Shape.doc({
|
|
|
493
491
|
- `Shape.plain.null()` - Null values
|
|
494
492
|
- `Shape.plain.undefined()` - Undefined values
|
|
495
493
|
- `Shape.plain.uint8Array()` - Binary data values
|
|
496
|
-
- `Shape.plain.
|
|
494
|
+
- `Shape.plain.struct(shape)` - Struct values with fixed keys
|
|
497
495
|
- `Shape.plain.record(valueShape)` - Object values with dynamic string keys
|
|
498
496
|
- `Shape.plain.array(itemShape)` - Array values
|
|
499
497
|
- `Shape.plain.union(shapes)` - Union of value types (e.g., `string | null`)
|
|
@@ -522,7 +520,7 @@ doc.users.has("alice"); // true
|
|
|
522
520
|
"alice" in doc.users; // true
|
|
523
521
|
```
|
|
524
522
|
|
|
525
|
-
For batched mutations, use `$.
|
|
523
|
+
For batched mutations, use `$.change()` instead.
|
|
526
524
|
|
|
527
525
|
#### `doc.$.toJSON()`
|
|
528
526
|
|
|
@@ -611,7 +609,7 @@ draft.todos.forEach((todo, index) => {
|
|
|
611
609
|
**Important**: Methods like `find()` and `filter()` return **mutable draft objects** that you can modify directly:
|
|
612
610
|
|
|
613
611
|
```typescript
|
|
614
|
-
|
|
612
|
+
change(doc, (draft) => {
|
|
615
613
|
// Find and mutate pattern - very common!
|
|
616
614
|
const todo = draft.todos.find((t) => t.id === "123");
|
|
617
615
|
if (todo) {
|
|
@@ -627,7 +625,7 @@ batch(doc, (draft) => {
|
|
|
627
625
|
});
|
|
628
626
|
```
|
|
629
627
|
|
|
630
|
-
This dual interface ensures predicates work with current data (including previous mutations in the same `
|
|
628
|
+
This dual interface ensures predicates work with current data (including previous mutations in the same `change()` block) while returned objects remain mutable.
|
|
631
629
|
|
|
632
630
|
### Movable List Operations
|
|
633
631
|
|
|
@@ -690,7 +688,7 @@ interface TodoDoc {
|
|
|
690
688
|
const todoSchema = Shape.doc({
|
|
691
689
|
title: Shape.text(),
|
|
692
690
|
todos: Shape.list(
|
|
693
|
-
Shape.plain.
|
|
691
|
+
Shape.plain.struct({
|
|
694
692
|
id: Shape.plain.string(),
|
|
695
693
|
text: Shape.plain.string(),
|
|
696
694
|
done: Shape.plain.boolean(),
|
|
@@ -699,10 +697,10 @@ const todoSchema = Shape.doc({
|
|
|
699
697
|
});
|
|
700
698
|
|
|
701
699
|
// TypeScript will ensure the schema produces the correct type
|
|
702
|
-
const doc =
|
|
700
|
+
const doc = createTypedDoc(todoSchema);
|
|
703
701
|
|
|
704
702
|
// Mutations are type-safe
|
|
705
|
-
|
|
703
|
+
change(doc, (draft) => {
|
|
706
704
|
draft.title.insert(0, "Hello"); // ✅ Valid - TypeScript knows this is LoroText
|
|
707
705
|
draft.todos.push({
|
|
708
706
|
// ✅ Valid - TypeScript knows the expected shape
|
|
@@ -754,8 +752,8 @@ The `TypedPresence` class provides type-safe access to ephemeral presence data w
|
|
|
754
752
|
import { TypedPresence, Shape } from "@loro-extended/change";
|
|
755
753
|
|
|
756
754
|
// Define a presence schema with placeholders
|
|
757
|
-
const PresenceSchema = Shape.plain.
|
|
758
|
-
cursor: Shape.plain.
|
|
755
|
+
const PresenceSchema = Shape.plain.struct({
|
|
756
|
+
cursor: Shape.plain.struct({
|
|
759
757
|
x: Shape.plain.number(),
|
|
760
758
|
y: Shape.plain.number(),
|
|
761
759
|
}),
|
|
@@ -806,7 +804,7 @@ This is typically provided by `UntypedDocHandle.presence` in `@loro-extended/rep
|
|
|
806
804
|
|
|
807
805
|
## Performance Considerations
|
|
808
806
|
|
|
809
|
-
- All changes within a `
|
|
807
|
+
- All changes within a `change()` call are batched into a single transaction
|
|
810
808
|
- Empty state overlay is computed on-demand, not stored
|
|
811
809
|
- Container creation is lazy - containers are only created when accessed
|
|
812
810
|
- Type validation occurs at development time, not runtime
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-extended/change",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A schema-driven, type-safe wrapper for Loro CRDT that provides natural JavaScript syntax for collaborative data mutations",
|
|
5
5
|
"author": "Duane Johnson",
|
|
6
6
|
"license": "MIT",
|