@loro-extended/change 4.0.0 → 5.0.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/README.md +173 -149
- package/dist/index.d.ts +962 -335
- package/dist/index.js +1040 -598
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/change.test.ts +51 -52
- package/src/functional-helpers.test.ts +316 -4
- package/src/functional-helpers.ts +96 -6
- package/src/grand-unified-api.test.ts +35 -29
- package/src/index.ts +25 -1
- package/src/json-patch.test.ts +46 -27
- package/src/loro.test.ts +449 -0
- package/src/loro.ts +273 -0
- package/src/overlay-recursion.test.ts +1 -1
- package/src/path-evaluator.ts +1 -1
- package/src/path-selector.test.ts +94 -1
- package/src/shape.ts +47 -15
- package/src/typed-doc.ts +99 -98
- package/src/typed-refs/base.ts +126 -35
- package/src/typed-refs/counter-ref-internals.ts +62 -0
- package/src/typed-refs/{counter.test.ts → counter-ref.test.ts} +5 -4
- package/src/typed-refs/counter-ref.ts +45 -0
- package/src/typed-refs/{doc.ts → doc-ref-internals.ts} +33 -38
- package/src/typed-refs/doc-ref.ts +47 -0
- package/src/typed-refs/encapsulation.test.ts +226 -0
- package/src/typed-refs/list-ref-base-internals.ts +280 -0
- package/src/typed-refs/{list-base.ts → list-ref-base.ts} +255 -160
- package/src/typed-refs/list-ref-internals.ts +21 -0
- package/src/typed-refs/{list.ts → list-ref.ts} +10 -11
- package/src/typed-refs/movable-list-ref-internals.ts +38 -0
- package/src/typed-refs/movable-list-ref.ts +31 -0
- package/src/typed-refs/proxy-handlers.ts +13 -4
- package/src/typed-refs/{record.ts → record-ref-internals.ts} +78 -79
- package/src/typed-refs/{record.test.ts → record-ref.test.ts} +21 -16
- package/src/typed-refs/record-ref.ts +80 -0
- package/src/typed-refs/struct-ref-internals.ts +195 -0
- package/src/typed-refs/{struct-value-updates.test.ts → struct-ref.test.ts} +5 -3
- package/src/typed-refs/struct-ref.ts +257 -0
- package/src/typed-refs/text-ref-internals.ts +100 -0
- package/src/typed-refs/text-ref.ts +72 -0
- package/src/typed-refs/tree-node-ref-internals.ts +111 -0
- package/src/typed-refs/{tree-node.ts → tree-node-ref.ts} +58 -94
- package/src/typed-refs/tree-ref-internals.ts +110 -0
- package/src/typed-refs/tree-ref.ts +194 -0
- package/src/typed-refs/utils.ts +21 -23
- package/src/typed-refs/counter.ts +0 -62
- package/src/typed-refs/movable-list.ts +0 -32
- package/src/typed-refs/struct.ts +0 -201
- package/src/typed-refs/text.ts +0 -91
- package/src/typed-refs/tree.ts +0 -268
- /package/src/typed-refs/{list-value-updates.test.ts → list-ref-value-updates.test.ts} +0 -0
- /package/src/typed-refs/{list.test.ts → list-ref.test.ts} +0 -0
- /package/src/typed-refs/{movable-list.test.ts → movable-list-ref.test.ts} +0 -0
- /package/src/typed-refs/{record-value-updates.test.ts → record-ref-value-updates.test.ts} +0 -0
- /package/src/typed-refs/{tree-node-value-updates.test.ts → tree-node-ref.test.ts} +0 -0
- /package/src/typed-refs/{tree.test.ts → tree-node.test.ts} +0 -0
package/README.md
CHANGED
|
@@ -12,10 +12,10 @@ Working with Loro directly involves somewhat verbose container operations and co
|
|
|
12
12
|
|
|
13
13
|
- **Schema-First Design**: Define your document structure with type-safe schemas
|
|
14
14
|
- **Natural Syntax**: Write `doc.title.insert(0, "Hello")` instead of verbose CRDT operations
|
|
15
|
-
- **
|
|
15
|
+
- **Placeholders**: 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 (`doc
|
|
17
|
+
- **Transactional Changes**: All mutations within a `change()` block are atomic
|
|
18
|
+
- **Loro Compatible**: Works seamlessly with existing Loro code (`loro(doc).doc` is a familiar `LoroDoc`)
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
@@ -59,7 +59,7 @@ if ("alice" in doc.users) {
|
|
|
59
59
|
|
|
60
60
|
// Batched mutations - commit together (optional, for performance)
|
|
61
61
|
// Using functional helper (recommended)
|
|
62
|
-
change(
|
|
62
|
+
doc.change((draft) => {
|
|
63
63
|
draft.title.insert(0, "Change: ");
|
|
64
64
|
draft.count.increment(10);
|
|
65
65
|
draft.users.set("bob", { name: "Bob" });
|
|
@@ -85,7 +85,7 @@ import { Shape } from "@loro-extended/change";
|
|
|
85
85
|
const blogSchema = Shape.doc({
|
|
86
86
|
// CRDT containers for collaborative editing
|
|
87
87
|
title: Shape.text(), // Collaborative text
|
|
88
|
-
viewCount: Shape.counter(), //
|
|
88
|
+
viewCount: Shape.counter(), // Collaborative increment/decrement counter
|
|
89
89
|
|
|
90
90
|
// Lists for ordered data
|
|
91
91
|
tags: Shape.list(Shape.plain.string()), // List of strings
|
|
@@ -110,9 +110,20 @@ const blogSchema = Shape.doc({
|
|
|
110
110
|
|
|
111
111
|
**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.
|
|
112
112
|
|
|
113
|
-
### Empty State Overlay
|
|
113
|
+
### Placeholders (Empty State Overlay)
|
|
114
|
+
|
|
115
|
+
Placeholders provide default values that are merged when CRDT containers are empty, ensuring the entire document remains type-safe even before any data has been written.
|
|
116
|
+
|
|
117
|
+
#### Why placeholders matter in distributed systems:
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
In traditional client-server architectures, you typically have a single source of truth that initializes default values. But in CRDTs, multiple peers can start working independently without coordination. This creates a challenge: who initializes the defaults?
|
|
120
|
+
|
|
121
|
+
Placeholders solve this elegantly:
|
|
122
|
+
|
|
123
|
+
- No initialization race conditions - Every peer sees the same defaults without needing to coordinate who writes them first
|
|
124
|
+
- Zero-cost defaults - Placeholders aren't stored in the CRDT; they're computed on read. This means no wasted storage or sync bandwidth for default values
|
|
125
|
+
- Conflict-free - Since placeholders aren't written to the CRDT, there's no possibility of conflicts between peers trying to initialize the same field
|
|
126
|
+
- Lazy materialization - Defaults only become "real" CRDT data when a peer explicitly modifies them
|
|
116
127
|
|
|
117
128
|
```typescript
|
|
118
129
|
// Use .placeholder() to set default values
|
|
@@ -141,7 +152,7 @@ console.log(doc.toJSON());
|
|
|
141
152
|
// { title: "Untitled Document", viewCount: 0, ... }
|
|
142
153
|
|
|
143
154
|
// After changes, CRDT values take priority over empty state
|
|
144
|
-
change(
|
|
155
|
+
doc.change((draft) => {
|
|
145
156
|
draft.title.insert(0, "My Blog Post");
|
|
146
157
|
draft.viewCount.increment(10);
|
|
147
158
|
});
|
|
@@ -153,7 +164,7 @@ console.log(doc.toJSON());
|
|
|
153
164
|
|
|
154
165
|
### Direct Mutations vs Batched Mutations
|
|
155
166
|
|
|
156
|
-
|
|
167
|
+
You can access and write schema properties directly on a TypedDoc. Mutations commit immediately by default:
|
|
157
168
|
|
|
158
169
|
```typescript
|
|
159
170
|
// Direct mutations - each commits immediately
|
|
@@ -165,9 +176,7 @@ doc.tags.push("typescript");
|
|
|
165
176
|
For batched operations (better performance, atomic undo), use `change()`:
|
|
166
177
|
|
|
167
178
|
```typescript
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
change(doc, (draft) => {
|
|
179
|
+
doc.change((draft) => {
|
|
171
180
|
// Text operations
|
|
172
181
|
draft.title.insert(0, "📝");
|
|
173
182
|
draft.title.delete(5, 3);
|
|
@@ -181,9 +190,9 @@ change(doc, (draft) => {
|
|
|
181
190
|
draft.tags.insert(0, "loro");
|
|
182
191
|
draft.tags.delete(1, 1);
|
|
183
192
|
|
|
184
|
-
// Struct operations (
|
|
185
|
-
draft.metadata.
|
|
186
|
-
draft.metadata.
|
|
193
|
+
// Struct operations (property assignment)
|
|
194
|
+
draft.metadata.author = "John Doe";
|
|
195
|
+
delete draft.metadata.featured;
|
|
187
196
|
|
|
188
197
|
// Movable list operations
|
|
189
198
|
draft.sections.push({
|
|
@@ -195,7 +204,6 @@ change(doc, (draft) => {
|
|
|
195
204
|
});
|
|
196
205
|
|
|
197
206
|
// All changes are committed atomically as one transaction
|
|
198
|
-
// change() returns the doc for chaining
|
|
199
207
|
console.log(doc.toJSON()); // Updated document state
|
|
200
208
|
```
|
|
201
209
|
|
|
@@ -209,8 +217,6 @@ console.log(doc.toJSON()); // Updated document state
|
|
|
209
217
|
| Performance-critical bulk updates | Batched: `change(doc, d => { ... })` |
|
|
210
218
|
| Simple reads + writes | Direct: `doc.users.set(...)` |
|
|
211
219
|
|
|
212
|
-
> **Note:** The `$.change()` method is available as an escape hatch, but the functional `change()` helper is recommended for cleaner code.
|
|
213
|
-
|
|
214
220
|
## Advanced Usage
|
|
215
221
|
|
|
216
222
|
### Discriminated Unions
|
|
@@ -218,12 +224,12 @@ console.log(doc.toJSON()); // Updated document state
|
|
|
218
224
|
For type-safe tagged unions (like different message types or presence states), use `Shape.plain.discriminatedUnion()`:
|
|
219
225
|
|
|
220
226
|
```typescript
|
|
221
|
-
import { Shape
|
|
227
|
+
import { Shape } from "@loro-extended/change";
|
|
222
228
|
|
|
223
229
|
// Define variant shapes - each must have the discriminant key
|
|
224
230
|
const ClientPresenceShape = Shape.plain.struct({
|
|
225
231
|
type: Shape.plain.string("client"), // Literal type for discrimination
|
|
226
|
-
name: Shape.plain.string(),
|
|
232
|
+
name: Shape.plain.string().placeholder("Anonymous"),
|
|
227
233
|
input: Shape.plain.struct({
|
|
228
234
|
force: Shape.plain.number(),
|
|
229
235
|
angle: Shape.plain.number(),
|
|
@@ -247,26 +253,8 @@ const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
|
247
253
|
server: ServerPresenceShape,
|
|
248
254
|
});
|
|
249
255
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
type: "client" as const,
|
|
253
|
-
name: "",
|
|
254
|
-
input: { force: 0, angle: 0 },
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const EmptyServerPresence = {
|
|
258
|
-
type: "server" as const,
|
|
259
|
-
cars: {},
|
|
260
|
-
tick: 0,
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// Use with mergeValue for presence data
|
|
264
|
-
const crdtValue = { type: "client", name: "Alice" };
|
|
265
|
-
const result = mergeValue(GamePresenceSchema, crdtValue, EmptyClientPresence);
|
|
266
|
-
// Result: { type: "client", name: "Alice", input: { force: 0, angle: 0 } }
|
|
267
|
-
|
|
268
|
-
// Type-safe filtering
|
|
269
|
-
function handlePresence(presence: typeof result) {
|
|
256
|
+
// Type-safe handling based on discriminant
|
|
257
|
+
function handlePresence(presence: Infer<typeof GamePresenceSchema>) {
|
|
270
258
|
if (presence.type === "server") {
|
|
271
259
|
// TypeScript knows this is ServerPresence
|
|
272
260
|
console.log(presence.cars, presence.tick);
|
|
@@ -279,9 +267,9 @@ function handlePresence(presence: typeof result) {
|
|
|
279
267
|
|
|
280
268
|
**Key features:**
|
|
281
269
|
|
|
282
|
-
- The discriminant
|
|
283
|
-
-
|
|
284
|
-
- Works seamlessly with `@loro-extended/
|
|
270
|
+
- The discriminant (e.g., `"type"`) determines which variant shape to use
|
|
271
|
+
- Use `.placeholder()` on fields to provide defaults (placeholders are applied automatically)
|
|
272
|
+
- Works seamlessly with `@loro-extended/repo`'s presence system
|
|
285
273
|
- Full TypeScript support for discriminated union types
|
|
286
274
|
|
|
287
275
|
### Untyped Integration with External Libraries
|
|
@@ -305,18 +293,7 @@ const CursorPresenceSchema = Shape.plain.struct({
|
|
|
305
293
|
|
|
306
294
|
// With @loro-extended/repo:
|
|
307
295
|
|
|
308
|
-
//
|
|
309
|
-
const handle = repo.get(docId, Shape.any(), { presence: CursorPresenceSchema });
|
|
310
|
-
handle.loroDoc; // Raw LoroDoc - use Loro API directly
|
|
311
|
-
handle.loroDoc.getMap("anything").set("key", "value");
|
|
312
|
-
handle.presence.setSelf({
|
|
313
|
-
// Typed presence
|
|
314
|
-
anchor: cursor.encode(), // Uint8Array directly
|
|
315
|
-
focus: null,
|
|
316
|
-
user: { name: "Alice", color: "#ff0000" },
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Option 2: Shape.any() in a container - one container is untyped
|
|
296
|
+
// Shape.any() in a container - one container is untyped
|
|
320
297
|
const ProseMirrorDocShape = Shape.doc({
|
|
321
298
|
doc: Shape.any(), // loro-prosemirror manages this
|
|
322
299
|
metadata: Shape.struct({
|
|
@@ -324,7 +301,9 @@ const ProseMirrorDocShape = Shape.doc({
|
|
|
324
301
|
title: Shape.text(),
|
|
325
302
|
}),
|
|
326
303
|
});
|
|
327
|
-
const handle2 = repo.get(docId, ProseMirrorDocShape, {
|
|
304
|
+
const handle2 = repo.get(docId, ProseMirrorDocShape, {
|
|
305
|
+
presence: CursorPresenceSchema,
|
|
306
|
+
});
|
|
328
307
|
handle2.doc.toJSON(); // { doc: unknown, metadata: { title: string } }
|
|
329
308
|
```
|
|
330
309
|
|
|
@@ -337,13 +316,13 @@ handle2.doc.toJSON(); // { doc: unknown, metadata: { title: string } }
|
|
|
337
316
|
|
|
338
317
|
**When to use:**
|
|
339
318
|
|
|
340
|
-
| Scenario
|
|
341
|
-
|
|
|
342
|
-
| External library manages entire document
|
|
343
|
-
| External library manages one container
|
|
344
|
-
| Flexible metadata in presence
|
|
345
|
-
| Binary cursor/selection data
|
|
346
|
-
| Full type safety
|
|
319
|
+
| Scenario | Shape to Use |
|
|
320
|
+
| ---------------------------------------- | ------------------------------------------------------------ |
|
|
321
|
+
| External library manages entire document | `repo.get(docId, Shape.any(), { presence: presenceSchema })` |
|
|
322
|
+
| External library manages one container | `Shape.doc({ doc: Shape.any(), ... })` |
|
|
323
|
+
| Flexible metadata in presence | `Shape.plain.any()` for dynamic values |
|
|
324
|
+
| Binary cursor/selection data | `Shape.plain.bytes().nullable()` for `Uint8Array` \| `null` |
|
|
325
|
+
| Full type safety | Use specific shapes like `Shape.struct()`, `Shape.text()` |
|
|
347
326
|
|
|
348
327
|
### Nested Structures
|
|
349
328
|
|
|
@@ -356,29 +335,16 @@ const complexSchema = Shape.doc({
|
|
|
356
335
|
metadata: Shape.struct({
|
|
357
336
|
views: Shape.counter(),
|
|
358
337
|
author: Shape.struct({
|
|
359
|
-
name: Shape.plain.string(),
|
|
338
|
+
name: Shape.plain.string().placeholder("Anonymous),
|
|
360
339
|
email: Shape.plain.string(),
|
|
361
340
|
}),
|
|
362
341
|
}),
|
|
363
342
|
}),
|
|
364
343
|
});
|
|
365
344
|
|
|
366
|
-
const emptyState = {
|
|
367
|
-
article: {
|
|
368
|
-
title: "",
|
|
369
|
-
metadata: {
|
|
370
|
-
views: 0,
|
|
371
|
-
author: {
|
|
372
|
-
name: "Anonymous",
|
|
373
|
-
email: "",
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
};
|
|
378
|
-
|
|
379
345
|
const doc = createTypedDoc(complexSchema);
|
|
380
346
|
|
|
381
|
-
change(
|
|
347
|
+
doc.change((draft) => {
|
|
382
348
|
draft.article.title.insert(0, "Deep Nesting Example");
|
|
383
349
|
draft.article.metadata.views.increment(5);
|
|
384
350
|
draft.article.metadata.author.name = "Alice"; // plain string update is captured and applied after closure
|
|
@@ -427,7 +393,7 @@ const collaborativeSchema = Shape.doc({
|
|
|
427
393
|
),
|
|
428
394
|
});
|
|
429
395
|
|
|
430
|
-
change(
|
|
396
|
+
doc.change((draft) => {
|
|
431
397
|
// Push creates and configures nested containers automatically
|
|
432
398
|
draft.articles.push({
|
|
433
399
|
title: "Collaborative Article",
|
|
@@ -440,15 +406,14 @@ change(doc, (draft) => {
|
|
|
440
406
|
});
|
|
441
407
|
|
|
442
408
|
// Later, edit the collaborative parts
|
|
443
|
-
|
|
444
|
-
draft.articles.
|
|
445
|
-
draft.articles.get(0)?.tags.push("real-time");
|
|
409
|
+
draft.articles.[0]?.title.insert(0, "✨ ");
|
|
410
|
+
draft.articles.[0]?.tags.push("real-time");
|
|
446
411
|
});
|
|
447
412
|
```
|
|
448
413
|
|
|
449
414
|
## Path Selector DSL
|
|
450
415
|
|
|
451
|
-
The `@loro-extended/change` package exports a type-safe path selector DSL for building (a subset of) JSONPath expressions with full TypeScript type inference. This is primarily used by `
|
|
416
|
+
The `@loro-extended/change` package exports a type-safe path selector DSL for building (a subset of) JSONPath expressions with full TypeScript type inference. This is primarily used by `handle.subscribe()` in `@loro-extended/repo` for efficient, type-safe subscriptions:
|
|
452
417
|
|
|
453
418
|
```typescript
|
|
454
419
|
// In @loro-extended/repo, use with Handle.subscribe():
|
|
@@ -486,67 +451,124 @@ const doc = createTypedDoc(schema);
|
|
|
486
451
|
const docFromExisting = createTypedDoc(schema, existingLoroDoc);
|
|
487
452
|
```
|
|
488
453
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
Constructor-style API. Use `createTypedDoc()` instead for cleaner code.
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
// Deprecated - use createTypedDoc() instead
|
|
495
|
-
const doc = new TypedDoc(schema);
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Functional Helpers (Recommended)
|
|
454
|
+
### The `loro()` Escape Hatch
|
|
499
455
|
|
|
500
|
-
|
|
456
|
+
The `loro()` function provides access to CRDT internals and container-specific operations. It follows a simple design principle:
|
|
501
457
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
Batches multiple mutations into a single transaction. Returns the doc for chaining.
|
|
505
|
-
|
|
506
|
-
```typescript
|
|
507
|
-
import { change } from "@loro-extended/change";
|
|
508
|
-
|
|
509
|
-
change(doc, (draft) => {
|
|
510
|
-
draft.title.insert(0, "Hello");
|
|
511
|
-
draft.count.increment(5);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Chainable - change returns the doc
|
|
515
|
-
change(doc, (d) => d.count.increment(1)).count.increment(2);
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
#### `doc.toJSON()`
|
|
519
|
-
|
|
520
|
-
Returns the full plain JavaScript object representation of the document.
|
|
458
|
+
> **If it takes a plain JavaScript value, keep it on the ref.** > **If it takes a Loro container or exposes CRDT internals, use `loro()`.**
|
|
521
459
|
|
|
522
460
|
```typescript
|
|
523
|
-
|
|
524
|
-
|
|
461
|
+
import { loro } from "@loro-extended/change";
|
|
462
|
+
|
|
463
|
+
// Access underlying Loro primitives
|
|
464
|
+
loro(ref).doc; // LoroDoc
|
|
465
|
+
loro(ref).container; // LoroList, LoroMap, etc. (correctly typed)
|
|
466
|
+
loro(ref).subscribe(cb); // Subscribe to changes
|
|
467
|
+
|
|
468
|
+
// Container operations (take Loro containers, not plain values)
|
|
469
|
+
loro(list).pushContainer(loroMap);
|
|
470
|
+
loro(list).insertContainer(0, loroMap);
|
|
471
|
+
loro(struct).setContainer("key", loroMap);
|
|
472
|
+
loro(record).setContainer("key", loroMap);
|
|
473
|
+
|
|
474
|
+
// TypedDoc operations
|
|
475
|
+
loro(doc).doc; // Raw LoroDoc access
|
|
476
|
+
loro(doc).applyPatch(patch); // JSON Patch operations
|
|
477
|
+
loro(doc).docShape; // Schema access
|
|
478
|
+
loro(doc).rawValue; // Unmerged CRDT value
|
|
525
479
|
```
|
|
526
480
|
|
|
527
|
-
####
|
|
528
|
-
|
|
529
|
-
|
|
481
|
+
#### API Surface by Ref Type
|
|
482
|
+
|
|
483
|
+
**ListRef / MovableListRef**
|
|
484
|
+
|
|
485
|
+
| Direct Access | Only via `loro()` |
|
|
486
|
+
| ---------------------- | ----------------------------------- |
|
|
487
|
+
| `push(item)` | `pushContainer(container)` |
|
|
488
|
+
| `insert(index, item)` | `insertContainer(index, container)` |
|
|
489
|
+
| `delete(index, len)` | `subscribe(callback)` |
|
|
490
|
+
| `find(predicate)` | `doc` |
|
|
491
|
+
| `filter(predicate)` | `container` |
|
|
492
|
+
| `map(callback)` | |
|
|
493
|
+
| `forEach(callback)` | |
|
|
494
|
+
| `some(predicate)` | |
|
|
495
|
+
| `every(predicate)` | |
|
|
496
|
+
| `slice(start, end)` | |
|
|
497
|
+
| `findIndex(predicate)` | |
|
|
498
|
+
| `length`, `[index]` | |
|
|
499
|
+
| `toJSON()` | |
|
|
500
|
+
|
|
501
|
+
**StructRef**
|
|
502
|
+
|
|
503
|
+
| Direct Access | Only via `loro()` |
|
|
504
|
+
| ---------------------------- | ------------------------------ |
|
|
505
|
+
| `obj.property` (get) | `setContainer(key, container)` |
|
|
506
|
+
| `obj.property = value` (set) | `subscribe(callback)` |
|
|
507
|
+
| `Object.keys(obj)` | `doc` |
|
|
508
|
+
| `'key' in obj` | `container` |
|
|
509
|
+
| `delete obj.key` | |
|
|
510
|
+
| `toJSON()` | |
|
|
511
|
+
|
|
512
|
+
**RecordRef** (Map-like interface)
|
|
513
|
+
|
|
514
|
+
| Direct Access | Only via `loro()` |
|
|
515
|
+
| -------------------- | ------------------------------ |
|
|
516
|
+
| `get(key)` | `setContainer(key, container)` |
|
|
517
|
+
| `set(key, value)` | `subscribe(callback)` |
|
|
518
|
+
| `delete(key)` | `doc` |
|
|
519
|
+
| `has(key)` | `container` |
|
|
520
|
+
| `keys()`, `values()` | |
|
|
521
|
+
| `size` | |
|
|
522
|
+
| `toJSON()` | |
|
|
523
|
+
|
|
524
|
+
**TextRef**
|
|
525
|
+
|
|
526
|
+
| Direct Access | Only via `loro()` |
|
|
527
|
+
| -------------------------------- | --------------------- |
|
|
528
|
+
| `insert(index, content)` | `subscribe(callback)` |
|
|
529
|
+
| `delete(index, len)` | `doc` |
|
|
530
|
+
| `update(text)` | `container` |
|
|
531
|
+
| `mark(range, key, value)` | |
|
|
532
|
+
| `unmark(range, key)` | |
|
|
533
|
+
| `toDelta()`, `applyDelta(delta)` | |
|
|
534
|
+
| `toString()`, `valueOf()` | |
|
|
535
|
+
| `length`, `toJSON()` | |
|
|
536
|
+
|
|
537
|
+
**CounterRef**
|
|
538
|
+
|
|
539
|
+
| Direct Access | Only via `loro()` |
|
|
540
|
+
| -------------------- | --------------------- |
|
|
541
|
+
| `increment(value)` | `subscribe(callback)` |
|
|
542
|
+
| `decrement(value)` | `doc` |
|
|
543
|
+
| `value`, `valueOf()` | `container` |
|
|
544
|
+
| `toJSON()` | |
|
|
545
|
+
|
|
546
|
+
**TypedDoc**
|
|
547
|
+
|
|
548
|
+
| Direct Access | Only via `loro()` |
|
|
549
|
+
| ------------------------------ | --------------------- |
|
|
550
|
+
| `doc.property` (schema access) | `doc` (raw LoroDoc) |
|
|
551
|
+
| `toJSON()` | `subscribe(callback)` |
|
|
552
|
+
| `change(fn)` | `applyPatch(patch)` |
|
|
553
|
+
| | `docShape` |
|
|
554
|
+
| | `rawValue` |
|
|
555
|
+
|
|
556
|
+
### Subscribing to Ref Changes
|
|
557
|
+
|
|
558
|
+
The `loro()` function enables the "pass around a ref" pattern where components can receive a ref and subscribe to its changes without needing the full document:
|
|
530
559
|
|
|
531
560
|
```typescript
|
|
532
|
-
import {
|
|
533
|
-
|
|
534
|
-
const loroDoc = getLoroDoc(doc);
|
|
535
|
-
loroDoc.subscribe((event) => console.log("Changed:", event));
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
### $ Namespace (Escape Hatch)
|
|
539
|
-
|
|
540
|
-
The `$` namespace provides access to meta-operations. While functional helpers are recommended, the `$` namespace is available for advanced use cases:
|
|
541
|
-
|
|
542
|
-
#### `doc.$.change(mutator)`
|
|
561
|
+
import { loro } from "@loro-extended/change";
|
|
543
562
|
|
|
544
|
-
|
|
563
|
+
function TextEditor({ textRef }: { textRef: TextRef }) {
|
|
564
|
+
useEffect(() => {
|
|
565
|
+
return loro(textRef).subscribe((event) => {
|
|
566
|
+
// Handle text changes
|
|
567
|
+
});
|
|
568
|
+
}, [textRef]);
|
|
545
569
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// Make changes to draft - all commit together
|
|
549
|
-
});
|
|
570
|
+
return <div>...</div>;
|
|
571
|
+
}
|
|
550
572
|
```
|
|
551
573
|
|
|
552
574
|
### Schema Builders
|
|
@@ -636,7 +658,7 @@ email: Shape.plain
|
|
|
636
658
|
|
|
637
659
|
### TypedDoc API
|
|
638
660
|
|
|
639
|
-
With the proxy-based API, schema properties are accessed directly on the doc object, and
|
|
661
|
+
With the proxy-based API, schema properties are accessed directly on the doc object, and CRDT internals are accessed via the `loro()` function.
|
|
640
662
|
|
|
641
663
|
#### Direct Schema Access
|
|
642
664
|
|
|
@@ -645,7 +667,7 @@ Access schema properties directly on the doc. Mutations commit immediately (auto
|
|
|
645
667
|
```typescript
|
|
646
668
|
// Read values
|
|
647
669
|
const title = doc.title.toString();
|
|
648
|
-
const count = doc.count
|
|
670
|
+
const count = doc.count;
|
|
649
671
|
|
|
650
672
|
// Mutate directly - commits immediately
|
|
651
673
|
doc.title.insert(0, "Hello");
|
|
@@ -657,30 +679,32 @@ doc.users.has("alice"); // true
|
|
|
657
679
|
"alice" in doc.users; // true
|
|
658
680
|
```
|
|
659
681
|
|
|
660
|
-
For batched mutations, use
|
|
682
|
+
For batched mutations, use `change(doc, fn)`.
|
|
661
683
|
|
|
662
|
-
#### `doc
|
|
684
|
+
#### `doc.toJSON()`
|
|
663
685
|
|
|
664
|
-
|
|
686
|
+
Returns the full plain JavaScript object representation.
|
|
665
687
|
|
|
666
688
|
```typescript
|
|
667
|
-
const snapshot = doc
|
|
689
|
+
const snapshot = doc.toJSON();
|
|
668
690
|
```
|
|
669
691
|
|
|
670
|
-
#### `doc
|
|
692
|
+
#### `loro(doc).rawValue`
|
|
671
693
|
|
|
672
|
-
Returns raw CRDT state without empty state overlay.
|
|
694
|
+
Returns raw CRDT state without placeholders (empty state overlay).
|
|
673
695
|
|
|
674
696
|
```typescript
|
|
675
|
-
|
|
697
|
+
import { loro } from "@loro-extended/change";
|
|
698
|
+
const crdtState = loro(doc).rawValue;
|
|
676
699
|
```
|
|
677
700
|
|
|
678
|
-
#### `doc
|
|
701
|
+
#### `loro(doc).doc`
|
|
679
702
|
|
|
680
|
-
|
|
703
|
+
Access the underlying LoroDoc.
|
|
681
704
|
|
|
682
705
|
```typescript
|
|
683
|
-
|
|
706
|
+
import { loro } from "@loro-extended/change";
|
|
707
|
+
const loroDoc = loro(doc).doc;
|
|
684
708
|
```
|
|
685
709
|
|
|
686
710
|
## CRDT Container Operations
|
|
@@ -743,7 +767,7 @@ draft.todos.forEach((todo, index) => {
|
|
|
743
767
|
});
|
|
744
768
|
```
|
|
745
769
|
|
|
746
|
-
|
|
770
|
+
Methods like `find()` and `filter()` return **mutable draft objects** that you can modify directly:
|
|
747
771
|
|
|
748
772
|
```typescript
|
|
749
773
|
change(doc, (draft) => {
|
|
@@ -909,7 +933,7 @@ You can easily get a plain JavaScript object snapshot of any part of the documen
|
|
|
909
933
|
|
|
910
934
|
```typescript
|
|
911
935
|
// Get full document snapshot
|
|
912
|
-
const snapshot = doc
|
|
936
|
+
const snapshot = doc.toJSON();
|
|
913
937
|
|
|
914
938
|
// Get snapshot of a specific list
|
|
915
939
|
const todos = doc.todos.toJSON(); // returns plain array of todos
|
|
@@ -1049,7 +1073,7 @@ This package is part of the loro-extended ecosystem. Contributions welcome!
|
|
|
1049
1073
|
|
|
1050
1074
|
- **Build**: `pnpm build`
|
|
1051
1075
|
- **Test**: `pnpm test`
|
|
1052
|
-
- **Lint**:
|
|
1076
|
+
- **Lint**: `pnpm check`
|
|
1053
1077
|
|
|
1054
1078
|
## License
|
|
1055
1079
|
|