@loro-extended/change 0.9.1 → 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 +201 -93
- package/dist/index.d.ts +361 -169
- package/dist/index.js +516 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +180 -175
- package/src/conversion.test.ts +19 -19
- package/src/conversion.ts +7 -7
- package/src/derive-placeholder.test.ts +14 -14
- package/src/derive-placeholder.ts +3 -3
- package/src/discriminated-union-assignability.test.ts +7 -7
- package/src/discriminated-union-tojson.test.ts +13 -24
- package/src/discriminated-union.test.ts +9 -8
- package/src/equality.test.ts +10 -2
- package/src/functional-helpers.test.ts +149 -0
- package/src/functional-helpers.ts +61 -0
- package/src/grand-unified-api.test.ts +423 -0
- package/src/index.ts +8 -6
- package/src/json-patch.test.ts +64 -56
- package/src/overlay-recursion.test.ts +23 -22
- package/src/overlay.ts +9 -9
- package/src/readonly.test.ts +27 -26
- package/src/shape.ts +103 -21
- package/src/typed-doc.ts +227 -58
- package/src/typed-refs/base.ts +23 -1
- package/src/typed-refs/counter.test.ts +44 -13
- package/src/typed-refs/counter.ts +40 -3
- package/src/typed-refs/doc.ts +12 -6
- package/src/typed-refs/json-compatibility.test.ts +37 -32
- package/src/typed-refs/list-base.ts +26 -22
- package/src/typed-refs/list.test.ts +4 -3
- package/src/typed-refs/movable-list.test.ts +3 -2
- package/src/typed-refs/movable-list.ts +4 -1
- package/src/typed-refs/proxy-handlers.ts +14 -1
- package/src/typed-refs/record.test.ts +107 -42
- package/src/typed-refs/record.ts +37 -19
- package/src/typed-refs/{map.ts → struct.ts} +31 -16
- package/src/typed-refs/text.ts +42 -1
- package/src/typed-refs/utils.ts +28 -6
- package/src/types.test.ts +34 -39
- package/src/types.ts +5 -40
- package/src/utils/type-guards.ts +11 -6
- package/src/validation.ts +10 -10
package/README.md
CHANGED
|
@@ -11,11 +11,11 @@ A schema-driven, type-safe wrapper for [Loro CRDT](https://github.com/loro-dev/l
|
|
|
11
11
|
Working with Loro directly involves somewhat verbose container operations and complex type management. The `change` package provides:
|
|
12
12
|
|
|
13
13
|
- **Schema-First Design**: Define your document structure with type-safe schemas
|
|
14
|
-
- **Natural Syntax**: Write `
|
|
14
|
+
- **Natural Syntax**: Write `doc.title.insert(0, "Hello")` instead of verbose CRDT operations
|
|
15
15
|
- **Empty State Overlay**: Seamlessly blend default values with CRDT state
|
|
16
16
|
- **Full Type Safety**: Complete TypeScript support with compile-time validation
|
|
17
|
-
- **Transactional Changes**: All mutations within a
|
|
18
|
-
- **Loro Compatible**: Works seamlessly with existing Loro code (`
|
|
17
|
+
- **Transactional Changes**: All mutations within a `$.batch()` block are atomic
|
|
18
|
+
- **Loro Compatible**: Works seamlessly with existing Loro code (`doc.$.loroDoc` is a familiar `LoroDoc`)
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
@@ -28,35 +28,45 @@ pnpm add @loro-extended/change loro-crdt
|
|
|
28
28
|
## Quick Start
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import {
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
completed: Shape.plain.boolean(),
|
|
41
|
-
})
|
|
42
|
-
),
|
|
36
|
+
count: Shape.counter(),
|
|
37
|
+
users: Shape.record(Shape.plain.struct({
|
|
38
|
+
name: Shape.plain.string(),
|
|
39
|
+
})),
|
|
43
40
|
});
|
|
44
41
|
|
|
45
42
|
// Create a typed document
|
|
46
|
-
const doc =
|
|
43
|
+
const doc = createTypedDoc(schema);
|
|
47
44
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
// Direct mutations - commit immediately (auto-commit mode)
|
|
46
|
+
doc.title.insert(0, "📝 Todo");
|
|
47
|
+
doc.count.increment(5);
|
|
48
|
+
doc.users.set("alice", { name: "Alice" });
|
|
49
|
+
|
|
50
|
+
// Check existence
|
|
51
|
+
if (doc.users.has("alice")) {
|
|
52
|
+
console.log("Alice exists!");
|
|
53
|
+
}
|
|
54
|
+
if ("alice" in doc.users) {
|
|
55
|
+
console.log("Also works with 'in' operator!");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Batched mutations - commit together (optional, for performance)
|
|
59
|
+
// Using functional helper (recommended)
|
|
60
|
+
change(doc, draft => {
|
|
61
|
+
draft.title.insert(0, "Change: ");
|
|
62
|
+
draft.count.increment(10);
|
|
63
|
+
draft.users.set("bob", { name: "Bob" });
|
|
56
64
|
});
|
|
65
|
+
// All changes commit as one transaction
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
// Get JSON snapshot using functional helper
|
|
68
|
+
console.log(doc.toJSON());
|
|
69
|
+
// { title: "Change: 📝 Todo", count: 15, users: { alice: { name: "Alice" }, bob: { name: "Bob" } } }
|
|
60
70
|
```
|
|
61
71
|
|
|
62
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.
|
|
@@ -78,8 +88,8 @@ const blogSchema = Shape.doc({
|
|
|
78
88
|
// Lists for ordered data
|
|
79
89
|
tags: Shape.list(Shape.plain.string()), // List of strings
|
|
80
90
|
|
|
81
|
-
//
|
|
82
|
-
metadata: Shape.
|
|
91
|
+
// Structs for structured data with fixed keys
|
|
92
|
+
metadata: Shape.struct({
|
|
83
93
|
author: Shape.plain.string(), // Plain values (POJOs)
|
|
84
94
|
publishedAt: Shape.plain.string(), // ISO date string
|
|
85
95
|
featured: Shape.plain.boolean(),
|
|
@@ -87,7 +97,7 @@ const blogSchema = Shape.doc({
|
|
|
87
97
|
|
|
88
98
|
// Movable lists for reorderable content
|
|
89
99
|
sections: Shape.movableList(
|
|
90
|
-
Shape.
|
|
100
|
+
Shape.struct({
|
|
91
101
|
heading: Shape.text(), // Collaborative headings
|
|
92
102
|
content: Shape.text(), // Collaborative content
|
|
93
103
|
order: Shape.plain.number(), // Plain metadata
|
|
@@ -96,7 +106,7 @@ const blogSchema = Shape.doc({
|
|
|
96
106
|
});
|
|
97
107
|
```
|
|
98
108
|
|
|
99
|
-
**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.
|
|
100
110
|
|
|
101
111
|
### Empty State Overlay
|
|
102
112
|
|
|
@@ -108,13 +118,13 @@ const blogSchemaWithDefaults = Shape.doc({
|
|
|
108
118
|
title: Shape.text().placeholder("Untitled Document"),
|
|
109
119
|
viewCount: Shape.counter(), // defaults to 0
|
|
110
120
|
tags: Shape.list(Shape.plain.string()), // defaults to []
|
|
111
|
-
metadata: Shape.
|
|
121
|
+
metadata: Shape.struct({
|
|
112
122
|
author: Shape.plain.string().placeholder("Anonymous"),
|
|
113
123
|
publishedAt: Shape.plain.string(), // defaults to ""
|
|
114
124
|
featured: Shape.plain.boolean(), // defaults to false
|
|
115
125
|
}),
|
|
116
126
|
sections: Shape.movableList(
|
|
117
|
-
Shape.
|
|
127
|
+
Shape.struct({
|
|
118
128
|
heading: Shape.text(),
|
|
119
129
|
content: Shape.text(),
|
|
120
130
|
order: Shape.plain.number(),
|
|
@@ -122,29 +132,40 @@ const blogSchemaWithDefaults = Shape.doc({
|
|
|
122
132
|
),
|
|
123
133
|
});
|
|
124
134
|
|
|
125
|
-
const doc =
|
|
135
|
+
const doc = createTypedDoc(blogSchemaWithDefaults);
|
|
126
136
|
|
|
127
137
|
// Initially returns empty state
|
|
128
|
-
console.log(doc.
|
|
138
|
+
console.log(doc.toJSON());
|
|
129
139
|
// { title: "Untitled Document", viewCount: 0, ... }
|
|
130
140
|
|
|
131
141
|
// After changes, CRDT values take priority over empty state
|
|
132
|
-
|
|
142
|
+
change(doc, (draft) => {
|
|
133
143
|
draft.title.insert(0, "My Blog Post");
|
|
134
144
|
draft.viewCount.increment(10);
|
|
135
145
|
});
|
|
136
146
|
|
|
137
|
-
console.log(doc.
|
|
147
|
+
console.log(doc.toJSON());
|
|
138
148
|
// { title: "My Blog Post", viewCount: 10, tags: [], ... }
|
|
139
149
|
// ↑ CRDT value ↑ CRDT value ↑ empty state preserved
|
|
140
150
|
```
|
|
141
151
|
|
|
142
|
-
###
|
|
152
|
+
### Direct Mutations vs Batched Mutations
|
|
143
153
|
|
|
144
|
-
|
|
154
|
+
With the Grand Unified API, schema properties are accessed directly on the doc. Mutations commit immediately by default:
|
|
145
155
|
|
|
146
156
|
```typescript
|
|
147
|
-
|
|
157
|
+
// Direct mutations - each commits immediately
|
|
158
|
+
doc.title.insert(0, "📝");
|
|
159
|
+
doc.viewCount.increment(1);
|
|
160
|
+
doc.tags.push("typescript");
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For batched operations (better performance, atomic undo), use `change()`:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { change } from "@loro-extended/change";
|
|
167
|
+
|
|
168
|
+
change(doc, (draft) => {
|
|
148
169
|
// Text operations
|
|
149
170
|
draft.title.insert(0, "📝");
|
|
150
171
|
draft.title.delete(5, 3);
|
|
@@ -158,7 +179,7 @@ const result = doc.change((draft) => {
|
|
|
158
179
|
draft.tags.insert(0, "loro");
|
|
159
180
|
draft.tags.delete(1, 1);
|
|
160
181
|
|
|
161
|
-
//
|
|
182
|
+
// Struct operations (POJO values)
|
|
162
183
|
draft.metadata.set("author", "John Doe");
|
|
163
184
|
draft.metadata.delete("featured");
|
|
164
185
|
|
|
@@ -171,10 +192,23 @@ const result = doc.change((draft) => {
|
|
|
171
192
|
draft.sections.move(0, 1); // Reorder sections
|
|
172
193
|
});
|
|
173
194
|
|
|
174
|
-
// All changes are committed atomically
|
|
175
|
-
|
|
195
|
+
// All changes are committed atomically as one transaction
|
|
196
|
+
// change() returns the doc for chaining
|
|
197
|
+
console.log(doc.toJSON()); // Updated document state
|
|
176
198
|
```
|
|
177
199
|
|
|
200
|
+
### When to Use `change()` vs Direct Mutations
|
|
201
|
+
|
|
202
|
+
| Use Case | Approach |
|
|
203
|
+
|----------|----------|
|
|
204
|
+
| Single mutation | Direct: `doc.count.increment(1)` |
|
|
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
|
+
| Simple reads + writes | Direct: `doc.users.set(...)` |
|
|
209
|
+
|
|
210
|
+
> **Note:** The `$.change()` method is available as an escape hatch, but the functional `change()` helper is recommended for cleaner code.
|
|
211
|
+
|
|
178
212
|
## Advanced Usage
|
|
179
213
|
|
|
180
214
|
### Discriminated Unions
|
|
@@ -185,19 +219,19 @@ For type-safe tagged unions (like different message types or presence states), u
|
|
|
185
219
|
import { Shape, mergeValue } from "@loro-extended/change";
|
|
186
220
|
|
|
187
221
|
// Define variant shapes - each must have the discriminant key
|
|
188
|
-
const ClientPresenceShape = Shape.plain.
|
|
222
|
+
const ClientPresenceShape = Shape.plain.struct({
|
|
189
223
|
type: Shape.plain.string("client"), // Literal type for discrimination
|
|
190
224
|
name: Shape.plain.string(),
|
|
191
|
-
input: Shape.plain.
|
|
225
|
+
input: Shape.plain.struct({
|
|
192
226
|
force: Shape.plain.number(),
|
|
193
227
|
angle: Shape.plain.number(),
|
|
194
228
|
}),
|
|
195
229
|
});
|
|
196
230
|
|
|
197
|
-
const ServerPresenceShape = Shape.plain.
|
|
231
|
+
const ServerPresenceShape = Shape.plain.struct({
|
|
198
232
|
type: Shape.plain.string("server"), // Literal type for discrimination
|
|
199
233
|
cars: Shape.plain.record(
|
|
200
|
-
Shape.plain.
|
|
234
|
+
Shape.plain.struct({
|
|
201
235
|
x: Shape.plain.number(),
|
|
202
236
|
y: Shape.plain.number(),
|
|
203
237
|
})
|
|
@@ -253,11 +287,11 @@ Handle complex nested documents with ease:
|
|
|
253
287
|
|
|
254
288
|
```typescript
|
|
255
289
|
const complexSchema = Shape.doc({
|
|
256
|
-
article: Shape.
|
|
290
|
+
article: Shape.struct({
|
|
257
291
|
title: Shape.text(),
|
|
258
|
-
metadata: Shape.
|
|
292
|
+
metadata: Shape.struct({
|
|
259
293
|
views: Shape.counter(),
|
|
260
|
-
author: Shape.
|
|
294
|
+
author: Shape.struct({
|
|
261
295
|
name: Shape.plain.string(),
|
|
262
296
|
email: Shape.plain.string(),
|
|
263
297
|
}),
|
|
@@ -278,9 +312,9 @@ const emptyState = {
|
|
|
278
312
|
},
|
|
279
313
|
};
|
|
280
314
|
|
|
281
|
-
const doc =
|
|
315
|
+
const doc = createTypedDoc(complexSchema);
|
|
282
316
|
|
|
283
|
-
|
|
317
|
+
change(doc, (draft) => {
|
|
284
318
|
draft.article.title.insert(0, "Deep Nesting Example");
|
|
285
319
|
draft.article.metadata.views.increment(5);
|
|
286
320
|
draft.article.metadata.author.name = "Alice"; // plain string update is captured and applied after closure
|
|
@@ -288,20 +322,20 @@ doc.change((draft) => {
|
|
|
288
322
|
});
|
|
289
323
|
```
|
|
290
324
|
|
|
291
|
-
###
|
|
325
|
+
### Struct Operations
|
|
292
326
|
|
|
293
|
-
For
|
|
327
|
+
For struct containers (fixed-key objects), use direct property access:
|
|
294
328
|
|
|
295
329
|
```typescript
|
|
296
330
|
const schema = Shape.doc({
|
|
297
|
-
settings: Shape.
|
|
331
|
+
settings: Shape.struct({
|
|
298
332
|
theme: Shape.plain.string(),
|
|
299
333
|
collapsed: Shape.plain.boolean(),
|
|
300
334
|
width: Shape.plain.number(),
|
|
301
335
|
}),
|
|
302
336
|
});
|
|
303
337
|
|
|
304
|
-
|
|
338
|
+
change(doc, (draft) => {
|
|
305
339
|
// Set individual values
|
|
306
340
|
draft.settings.theme = "dark";
|
|
307
341
|
draft.settings.collapsed = true;
|
|
@@ -316,11 +350,11 @@ Create lists containing CRDT containers for collaborative nested structures:
|
|
|
316
350
|
```typescript
|
|
317
351
|
const collaborativeSchema = Shape.doc({
|
|
318
352
|
articles: Shape.list(
|
|
319
|
-
Shape.
|
|
353
|
+
Shape.struct({
|
|
320
354
|
title: Shape.text(), // Collaborative title
|
|
321
355
|
content: Shape.text(), // Collaborative content
|
|
322
356
|
tags: Shape.list(Shape.plain.string()), // Collaborative tag list
|
|
323
|
-
metadata: Shape.plain.
|
|
357
|
+
metadata: Shape.plain.struct({
|
|
324
358
|
// Static metadata
|
|
325
359
|
authorId: Shape.plain.string(),
|
|
326
360
|
publishedAt: Shape.plain.string(),
|
|
@@ -329,7 +363,7 @@ const collaborativeSchema = Shape.doc({
|
|
|
329
363
|
),
|
|
330
364
|
});
|
|
331
365
|
|
|
332
|
-
|
|
366
|
+
change(doc, (draft) => {
|
|
333
367
|
// Push creates and configures nested containers automatically
|
|
334
368
|
draft.articles.push({
|
|
335
369
|
title: "Collaborative Article",
|
|
@@ -352,22 +386,77 @@ doc.change((draft) => {
|
|
|
352
386
|
|
|
353
387
|
### Core Functions
|
|
354
388
|
|
|
355
|
-
#### `
|
|
389
|
+
#### `createTypedDoc<T>(schema, existingDoc?)`
|
|
390
|
+
|
|
391
|
+
Creates a new typed Loro document. This is the recommended way to create documents.
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { createTypedDoc, Shape } from "@loro-extended/change";
|
|
395
|
+
|
|
396
|
+
const doc = createTypedDoc(schema);
|
|
397
|
+
const docFromExisting = createTypedDoc(schema, existingLoroDoc);
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
#### `new TypedDoc<T>(schema, existingDoc?)` *(deprecated)*
|
|
356
401
|
|
|
357
|
-
|
|
402
|
+
Constructor-style API. Use `createTypedDoc()` instead for cleaner code.
|
|
358
403
|
|
|
359
404
|
```typescript
|
|
405
|
+
// Deprecated - use createTypedDoc() instead
|
|
360
406
|
const doc = new TypedDoc(schema);
|
|
361
|
-
const docFromExisting = new TypedDoc(schema, existingLoroDoc);
|
|
362
407
|
```
|
|
363
408
|
|
|
364
|
-
|
|
409
|
+
### Functional Helpers (Recommended)
|
|
365
410
|
|
|
366
|
-
|
|
411
|
+
These functional helpers provide a cleaner API and are the recommended way to work with TypedDoc:
|
|
412
|
+
|
|
413
|
+
#### `change(doc, mutator)`
|
|
414
|
+
|
|
415
|
+
Batches multiple mutations into a single transaction. Returns the doc for chaining.
|
|
367
416
|
|
|
368
417
|
```typescript
|
|
369
|
-
|
|
370
|
-
|
|
418
|
+
import { change } from "@loro-extended/change";
|
|
419
|
+
|
|
420
|
+
change(doc, (draft) => {
|
|
421
|
+
draft.title.insert(0, "Hello");
|
|
422
|
+
draft.count.increment(5);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Chainable - change returns the doc
|
|
426
|
+
change(doc, d => d.count.increment(1)).count.increment(2);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### `doc.toJSON()`
|
|
430
|
+
|
|
431
|
+
Returns the full plain JavaScript object representation of the document.
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
const snapshot = doc.toJSON();
|
|
435
|
+
// { title: "Hello", count: 5, ... }
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
#### `getLoroDoc(doc)`
|
|
439
|
+
|
|
440
|
+
Access the underlying LoroDoc for advanced operations.
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
import { getLoroDoc } from "@loro-extended/change";
|
|
444
|
+
|
|
445
|
+
const loroDoc = getLoroDoc(doc);
|
|
446
|
+
loroDoc.subscribe((event) => console.log("Changed:", event));
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### $ Namespace (Escape Hatch)
|
|
450
|
+
|
|
451
|
+
The `$` namespace provides access to meta-operations. While functional helpers are recommended, the `$` namespace is available for advanced use cases:
|
|
452
|
+
|
|
453
|
+
#### `doc.$.change(mutator)`
|
|
454
|
+
|
|
455
|
+
Same as `change(doc, mutator)`.
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
doc.$.change((draft) => {
|
|
459
|
+
// Make changes to draft - all commit together
|
|
371
460
|
});
|
|
372
461
|
```
|
|
373
462
|
|
|
@@ -390,7 +479,7 @@ const schema = Shape.doc({
|
|
|
390
479
|
- `Shape.counter()` - Collaborative increment/decrement counters
|
|
391
480
|
- `Shape.list(itemSchema)` - Collaborative ordered lists
|
|
392
481
|
- `Shape.movableList(itemSchema)` - Collaborative reorderable lists
|
|
393
|
-
- `Shape.
|
|
482
|
+
- `Shape.struct(shape)` - Collaborative structs with fixed keys (uses LoroMap internally)
|
|
394
483
|
- `Shape.record(valueSchema)` - Collaborative key-value maps with dynamic string keys
|
|
395
484
|
- `Shape.tree(shape)` - Collaborative hierarchical tree structures (Note: incomplete implementation)
|
|
396
485
|
|
|
@@ -402,45 +491,60 @@ const schema = Shape.doc({
|
|
|
402
491
|
- `Shape.plain.null()` - Null values
|
|
403
492
|
- `Shape.plain.undefined()` - Undefined values
|
|
404
493
|
- `Shape.plain.uint8Array()` - Binary data values
|
|
405
|
-
- `Shape.plain.
|
|
494
|
+
- `Shape.plain.struct(shape)` - Struct values with fixed keys
|
|
406
495
|
- `Shape.plain.record(valueShape)` - Object values with dynamic string keys
|
|
407
496
|
- `Shape.plain.array(itemShape)` - Array values
|
|
408
497
|
- `Shape.plain.union(shapes)` - Union of value types (e.g., `string | null`)
|
|
409
498
|
- `Shape.plain.discriminatedUnion(key, variants)` - Tagged union types with a discriminant key
|
|
410
499
|
|
|
411
|
-
### TypedDoc
|
|
500
|
+
### TypedDoc API
|
|
501
|
+
|
|
502
|
+
With the proxy-based API, schema properties are accessed directly on the doc object, and meta-operations are accessed via the `$` namespace.
|
|
412
503
|
|
|
413
|
-
####
|
|
504
|
+
#### Direct Schema Access
|
|
414
505
|
|
|
415
|
-
|
|
506
|
+
Access schema properties directly on the doc. Mutations commit immediately (auto-commit mode).
|
|
416
507
|
|
|
417
508
|
```typescript
|
|
418
|
-
|
|
509
|
+
// Read values
|
|
510
|
+
const title = doc.title.toString();
|
|
511
|
+
const count = doc.count.value;
|
|
512
|
+
|
|
513
|
+
// Mutate directly - commits immediately
|
|
514
|
+
doc.title.insert(0, "Hello");
|
|
515
|
+
doc.count.increment(5);
|
|
516
|
+
doc.users.set("alice", { name: "Alice" });
|
|
517
|
+
|
|
518
|
+
// Check existence
|
|
519
|
+
doc.users.has("alice"); // true
|
|
520
|
+
"alice" in doc.users; // true
|
|
419
521
|
```
|
|
420
522
|
|
|
421
|
-
|
|
523
|
+
For batched mutations, use `$.change()` instead.
|
|
422
524
|
|
|
423
|
-
####
|
|
525
|
+
#### `doc.$.toJSON()`
|
|
424
526
|
|
|
425
|
-
Returns
|
|
527
|
+
Same as `doc.toJSON`. Returns the full plain JavaScript object representation.
|
|
426
528
|
|
|
427
529
|
```typescript
|
|
428
|
-
const
|
|
530
|
+
const snapshot = doc.$.toJSON();
|
|
429
531
|
```
|
|
430
532
|
|
|
431
|
-
####
|
|
533
|
+
#### `doc.$.rawValue`
|
|
432
534
|
|
|
433
|
-
|
|
535
|
+
Returns raw CRDT state without empty state overlay.
|
|
434
536
|
|
|
435
537
|
```typescript
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
const foods = loroDoc.getMap("foods");
|
|
439
|
-
const drinks = loroDoc.getOrCreateContainer("drinks", new LoroMap());
|
|
440
|
-
// etc.
|
|
538
|
+
const crdtState = doc.$.rawValue;
|
|
441
539
|
```
|
|
442
540
|
|
|
443
|
-
|
|
541
|
+
#### `doc.$.loroDoc`
|
|
542
|
+
|
|
543
|
+
Same as `getLoroDoc(doc)`. Access the underlying LoroDoc.
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
const loroDoc = doc.$.loroDoc;
|
|
547
|
+
```
|
|
444
548
|
|
|
445
549
|
## CRDT Container Operations
|
|
446
550
|
|
|
@@ -505,7 +609,7 @@ draft.todos.forEach((todo, index) => {
|
|
|
505
609
|
**Important**: Methods like `find()` and `filter()` return **mutable draft objects** that you can modify directly:
|
|
506
610
|
|
|
507
611
|
```typescript
|
|
508
|
-
|
|
612
|
+
change(doc, (draft) => {
|
|
509
613
|
// Find and mutate pattern - very common!
|
|
510
614
|
const todo = draft.todos.find((t) => t.id === "123");
|
|
511
615
|
if (todo) {
|
|
@@ -553,16 +657,16 @@ You can easily get a plain JavaScript object snapshot of any part of the documen
|
|
|
553
657
|
|
|
554
658
|
```typescript
|
|
555
659
|
// Get full document snapshot
|
|
556
|
-
const snapshot = doc
|
|
660
|
+
const snapshot = doc.$.toJSON();
|
|
557
661
|
|
|
558
662
|
// Get snapshot of a specific list
|
|
559
|
-
const todos = doc.
|
|
663
|
+
const todos = doc.todos.toJSON(); // returns plain array of todos
|
|
560
664
|
|
|
561
665
|
// Works with nested structures
|
|
562
|
-
const metadata = doc.
|
|
666
|
+
const metadata = doc.metadata.toJSON(); // returns plain object
|
|
563
667
|
|
|
564
668
|
// Serialize as JSON
|
|
565
|
-
const serializedMetadata = JSON.stringify(doc.
|
|
669
|
+
const serializedMetadata = JSON.stringify(doc.metadata); // returns string
|
|
566
670
|
```
|
|
567
671
|
|
|
568
672
|
**Note:** `JSON.stringify()` is recommended for serialization as it handles all data types correctly. `.toJSON()` is available on all `TypedRef` objects and proxied placeholders for convenience when you need a direct object snapshot.
|
|
@@ -584,7 +688,7 @@ interface TodoDoc {
|
|
|
584
688
|
const todoSchema = Shape.doc({
|
|
585
689
|
title: Shape.text(),
|
|
586
690
|
todos: Shape.list(
|
|
587
|
-
Shape.plain.
|
|
691
|
+
Shape.plain.struct({
|
|
588
692
|
id: Shape.plain.string(),
|
|
589
693
|
text: Shape.plain.string(),
|
|
590
694
|
done: Shape.plain.boolean(),
|
|
@@ -593,10 +697,10 @@ const todoSchema = Shape.doc({
|
|
|
593
697
|
});
|
|
594
698
|
|
|
595
699
|
// TypeScript will ensure the schema produces the correct type
|
|
596
|
-
const doc =
|
|
700
|
+
const doc = createTypedDoc(todoSchema);
|
|
597
701
|
|
|
598
|
-
//
|
|
599
|
-
|
|
702
|
+
// Mutations are type-safe
|
|
703
|
+
change(doc, (draft) => {
|
|
600
704
|
draft.title.insert(0, "Hello"); // ✅ Valid - TypeScript knows this is LoroText
|
|
601
705
|
draft.todos.push({
|
|
602
706
|
// ✅ Valid - TypeScript knows the expected shape
|
|
@@ -609,6 +713,9 @@ const result: TodoDoc = doc.change((draft) => {
|
|
|
609
713
|
// draft.todos.push({ invalid: true }); // ❌ TypeScript error
|
|
610
714
|
});
|
|
611
715
|
|
|
716
|
+
// The result is properly typed as TodoDoc
|
|
717
|
+
const result: TodoDoc = doc.toJSON();
|
|
718
|
+
|
|
612
719
|
// You can also use type assertion to ensure schema compatibility
|
|
613
720
|
type SchemaType = InferPlainType<typeof todoSchema>;
|
|
614
721
|
const _typeCheck: TodoDoc = {} as SchemaType; // ✅ Will error if types don't match
|
|
@@ -622,13 +729,14 @@ const _typeCheck: TodoDoc = {} as SchemaType; // ✅ Will error if types don't m
|
|
|
622
729
|
|
|
623
730
|
```typescript
|
|
624
731
|
import { LoroDoc } from "loro-crdt";
|
|
732
|
+
import { createTypedDoc, getLoroDoc } from "@loro-extended/change";
|
|
625
733
|
|
|
626
734
|
// Wrap existing LoroDoc
|
|
627
735
|
const existingDoc = new LoroDoc();
|
|
628
|
-
const typedDoc =
|
|
736
|
+
const typedDoc = createTypedDoc(schema, existingDoc);
|
|
629
737
|
|
|
630
738
|
// Access underlying LoroDoc
|
|
631
|
-
const loroDoc = typedDoc
|
|
739
|
+
const loroDoc = getLoroDoc(typedDoc);
|
|
632
740
|
|
|
633
741
|
// Use with existing Loro APIs
|
|
634
742
|
loroDoc.subscribe((event) => {
|
|
@@ -644,8 +752,8 @@ The `TypedPresence` class provides type-safe access to ephemeral presence data w
|
|
|
644
752
|
import { TypedPresence, Shape } from "@loro-extended/change";
|
|
645
753
|
|
|
646
754
|
// Define a presence schema with placeholders
|
|
647
|
-
const PresenceSchema = Shape.plain.
|
|
648
|
-
cursor: Shape.plain.
|
|
755
|
+
const PresenceSchema = Shape.plain.struct({
|
|
756
|
+
cursor: Shape.plain.struct({
|
|
649
757
|
x: Shape.plain.number(),
|
|
650
758
|
y: Shape.plain.number(),
|
|
651
759
|
}),
|
|
@@ -696,7 +804,7 @@ This is typically provided by `UntypedDocHandle.presence` in `@loro-extended/rep
|
|
|
696
804
|
|
|
697
805
|
## Performance Considerations
|
|
698
806
|
|
|
699
|
-
- All changes within a `change()`
|
|
807
|
+
- All changes within a `change()` call are batched into a single transaction
|
|
700
808
|
- Empty state overlay is computed on-demand, not stored
|
|
701
809
|
- Container creation is lazy - containers are only created when accessed
|
|
702
810
|
- Type validation occurs at development time, not runtime
|