@loro-extended/change 5.2.0 → 5.3.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/index.d.ts +21 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/functional-helpers.test.ts +125 -0
- package/src/shape.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-extended/change",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
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",
|
|
@@ -361,6 +361,131 @@ describe("functional helpers", () => {
|
|
|
361
361
|
})
|
|
362
362
|
})
|
|
363
363
|
|
|
364
|
+
describe("loro(ref).subscribe() for imported (remote) changes", () => {
|
|
365
|
+
it("should fire TextRef subscription when changes are imported", () => {
|
|
366
|
+
// Create two documents - simulating two clients
|
|
367
|
+
const doc1 = createTypedDoc(fullSchema)
|
|
368
|
+
const doc2 = createTypedDoc(fullSchema)
|
|
369
|
+
|
|
370
|
+
// Set up subscription on doc2's title ref
|
|
371
|
+
const callback = vi.fn()
|
|
372
|
+
const unsubscribe = loro(doc2.title).subscribe(callback)
|
|
373
|
+
|
|
374
|
+
// Make changes on doc1
|
|
375
|
+
doc1.title.insert(0, "Hello from doc1")
|
|
376
|
+
loro(doc1).doc.commit()
|
|
377
|
+
|
|
378
|
+
// Export from doc1 and import into doc2 (simulating sync)
|
|
379
|
+
const snapshot = loro(doc1).doc.export({ mode: "snapshot" })
|
|
380
|
+
loro(doc2).doc.import(snapshot)
|
|
381
|
+
|
|
382
|
+
// The subscription should have fired
|
|
383
|
+
expect(callback).toHaveBeenCalled()
|
|
384
|
+
|
|
385
|
+
// And the value should be updated
|
|
386
|
+
expect(doc2.title.toString()).toBe("Hello from doc1")
|
|
387
|
+
|
|
388
|
+
unsubscribe()
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it("should fire CounterRef subscription when changes are imported", () => {
|
|
392
|
+
const doc1 = createTypedDoc(fullSchema)
|
|
393
|
+
const doc2 = createTypedDoc(fullSchema)
|
|
394
|
+
|
|
395
|
+
const callback = vi.fn()
|
|
396
|
+
const unsubscribe = loro(doc2.count).subscribe(callback)
|
|
397
|
+
|
|
398
|
+
doc1.count.increment(42)
|
|
399
|
+
loro(doc1).doc.commit()
|
|
400
|
+
|
|
401
|
+
const snapshot = loro(doc1).doc.export({ mode: "snapshot" })
|
|
402
|
+
loro(doc2).doc.import(snapshot)
|
|
403
|
+
|
|
404
|
+
expect(callback).toHaveBeenCalled()
|
|
405
|
+
expect(doc2.count.value).toBe(42)
|
|
406
|
+
|
|
407
|
+
unsubscribe()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it("should fire ListRef subscription when changes are imported", () => {
|
|
411
|
+
const doc1 = createTypedDoc(fullSchema)
|
|
412
|
+
const doc2 = createTypedDoc(fullSchema)
|
|
413
|
+
|
|
414
|
+
const callback = vi.fn()
|
|
415
|
+
const unsubscribe = loro(doc2.items).subscribe(callback)
|
|
416
|
+
|
|
417
|
+
doc1.items.push("item1")
|
|
418
|
+
doc1.items.push("item2")
|
|
419
|
+
loro(doc1).doc.commit()
|
|
420
|
+
|
|
421
|
+
const snapshot = loro(doc1).doc.export({ mode: "snapshot" })
|
|
422
|
+
loro(doc2).doc.import(snapshot)
|
|
423
|
+
|
|
424
|
+
expect(callback).toHaveBeenCalled()
|
|
425
|
+
expect(doc2.items.toJSON()).toEqual(["item1", "item2"])
|
|
426
|
+
|
|
427
|
+
unsubscribe()
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it("should fire doc-level subscription when changes are imported", () => {
|
|
431
|
+
const doc1 = createTypedDoc(fullSchema)
|
|
432
|
+
const doc2 = createTypedDoc(fullSchema)
|
|
433
|
+
|
|
434
|
+
const callback = vi.fn()
|
|
435
|
+
const unsubscribe = loro(doc2).doc.subscribe(callback)
|
|
436
|
+
|
|
437
|
+
doc1.title.insert(0, "Hello")
|
|
438
|
+
loro(doc1).doc.commit()
|
|
439
|
+
|
|
440
|
+
const snapshot = loro(doc1).doc.export({ mode: "snapshot" })
|
|
441
|
+
loro(doc2).doc.import(snapshot)
|
|
442
|
+
|
|
443
|
+
expect(callback).toHaveBeenCalled()
|
|
444
|
+
|
|
445
|
+
unsubscribe()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it("should NOT fire subscription for containers that were not changed", () => {
|
|
449
|
+
const doc1 = createTypedDoc(fullSchema)
|
|
450
|
+
const doc2 = createTypedDoc(fullSchema)
|
|
451
|
+
|
|
452
|
+
// Subscribe to count, but only change title
|
|
453
|
+
const countCallback = vi.fn()
|
|
454
|
+
const unsubscribe = loro(doc2.count).subscribe(countCallback)
|
|
455
|
+
|
|
456
|
+
doc1.title.insert(0, "Hello")
|
|
457
|
+
loro(doc1).doc.commit()
|
|
458
|
+
|
|
459
|
+
const snapshot = loro(doc1).doc.export({ mode: "snapshot" })
|
|
460
|
+
loro(doc2).doc.import(snapshot)
|
|
461
|
+
|
|
462
|
+
// Count subscription should NOT have fired since count wasn't changed
|
|
463
|
+
expect(countCallback).not.toHaveBeenCalled()
|
|
464
|
+
|
|
465
|
+
unsubscribe()
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it("should provide updated value in subscription callback", () => {
|
|
469
|
+
const doc1 = createTypedDoc(fullSchema)
|
|
470
|
+
const doc2 = createTypedDoc(fullSchema)
|
|
471
|
+
|
|
472
|
+
let capturedValue: string | undefined
|
|
473
|
+
const unsubscribe = loro(doc2.title).subscribe(() => {
|
|
474
|
+
capturedValue = doc2.title.toString()
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
doc1.title.insert(0, "Remote text")
|
|
478
|
+
loro(doc1).doc.commit()
|
|
479
|
+
|
|
480
|
+
const snapshot = loro(doc1).doc.export({ mode: "snapshot" })
|
|
481
|
+
loro(doc2).doc.import(snapshot)
|
|
482
|
+
|
|
483
|
+
expect(capturedValue).toBe("Remote text")
|
|
484
|
+
|
|
485
|
+
unsubscribe()
|
|
486
|
+
})
|
|
487
|
+
})
|
|
488
|
+
|
|
364
489
|
describe("getLoroDoc() on refs", () => {
|
|
365
490
|
it("should return LoroDoc from TextRef", () => {
|
|
366
491
|
const doc = createTypedDoc(fullSchema)
|
package/src/shape.ts
CHANGED
|
@@ -222,6 +222,27 @@ export interface AnyContainerShape extends Shape<unknown, unknown, undefined> {
|
|
|
222
222
|
readonly _type: "any"
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Union of all container shape types.
|
|
227
|
+
*
|
|
228
|
+
* Each container shape has a `_mutable` type parameter that maps to the
|
|
229
|
+
* corresponding TypedRef class (e.g., TextContainerShape → TextRef).
|
|
230
|
+
* This enables deriving ref types from shapes:
|
|
231
|
+
*
|
|
232
|
+
* ```typescript
|
|
233
|
+
* // Get the ref type for any container shape
|
|
234
|
+
* type RefType = ContainerShape["_mutable"]
|
|
235
|
+
*
|
|
236
|
+
* // Exclude AnyContainerShape to get only typed refs
|
|
237
|
+
* type AnyTypedRef = Exclude<ContainerShape, AnyContainerShape>["_mutable"]
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* This creates intentional parallel hierarchies:
|
|
241
|
+
* - ContainerShape → defines what data looks like (schema)
|
|
242
|
+
* - TypedRef (via _mutable) → defines how you interact with data
|
|
243
|
+
* - loro() overloads → CRDT escape hatch (IDE DX)
|
|
244
|
+
* - change() overloads → mutation boundaries (IDE DX)
|
|
245
|
+
*/
|
|
225
246
|
export type ContainerShape =
|
|
226
247
|
| AnyContainerShape
|
|
227
248
|
| CounterContainerShape
|