@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.
Files changed (56) hide show
  1. package/README.md +173 -149
  2. package/dist/index.d.ts +962 -335
  3. package/dist/index.js +1040 -598
  4. package/dist/index.js.map +1 -1
  5. package/package.json +4 -4
  6. package/src/change.test.ts +51 -52
  7. package/src/functional-helpers.test.ts +316 -4
  8. package/src/functional-helpers.ts +96 -6
  9. package/src/grand-unified-api.test.ts +35 -29
  10. package/src/index.ts +25 -1
  11. package/src/json-patch.test.ts +46 -27
  12. package/src/loro.test.ts +449 -0
  13. package/src/loro.ts +273 -0
  14. package/src/overlay-recursion.test.ts +1 -1
  15. package/src/path-evaluator.ts +1 -1
  16. package/src/path-selector.test.ts +94 -1
  17. package/src/shape.ts +47 -15
  18. package/src/typed-doc.ts +99 -98
  19. package/src/typed-refs/base.ts +126 -35
  20. package/src/typed-refs/counter-ref-internals.ts +62 -0
  21. package/src/typed-refs/{counter.test.ts → counter-ref.test.ts} +5 -4
  22. package/src/typed-refs/counter-ref.ts +45 -0
  23. package/src/typed-refs/{doc.ts → doc-ref-internals.ts} +33 -38
  24. package/src/typed-refs/doc-ref.ts +47 -0
  25. package/src/typed-refs/encapsulation.test.ts +226 -0
  26. package/src/typed-refs/list-ref-base-internals.ts +280 -0
  27. package/src/typed-refs/{list-base.ts → list-ref-base.ts} +255 -160
  28. package/src/typed-refs/list-ref-internals.ts +21 -0
  29. package/src/typed-refs/{list.ts → list-ref.ts} +10 -11
  30. package/src/typed-refs/movable-list-ref-internals.ts +38 -0
  31. package/src/typed-refs/movable-list-ref.ts +31 -0
  32. package/src/typed-refs/proxy-handlers.ts +13 -4
  33. package/src/typed-refs/{record.ts → record-ref-internals.ts} +78 -79
  34. package/src/typed-refs/{record.test.ts → record-ref.test.ts} +21 -16
  35. package/src/typed-refs/record-ref.ts +80 -0
  36. package/src/typed-refs/struct-ref-internals.ts +195 -0
  37. package/src/typed-refs/{struct-value-updates.test.ts → struct-ref.test.ts} +5 -3
  38. package/src/typed-refs/struct-ref.ts +257 -0
  39. package/src/typed-refs/text-ref-internals.ts +100 -0
  40. package/src/typed-refs/text-ref.ts +72 -0
  41. package/src/typed-refs/tree-node-ref-internals.ts +111 -0
  42. package/src/typed-refs/{tree-node.ts → tree-node-ref.ts} +58 -94
  43. package/src/typed-refs/tree-ref-internals.ts +110 -0
  44. package/src/typed-refs/tree-ref.ts +194 -0
  45. package/src/typed-refs/utils.ts +21 -23
  46. package/src/typed-refs/counter.ts +0 -62
  47. package/src/typed-refs/movable-list.ts +0 -32
  48. package/src/typed-refs/struct.ts +0 -201
  49. package/src/typed-refs/text.ts +0 -91
  50. package/src/typed-refs/tree.ts +0 -268
  51. /package/src/typed-refs/{list-value-updates.test.ts → list-ref-value-updates.test.ts} +0 -0
  52. /package/src/typed-refs/{list.test.ts → list-ref.test.ts} +0 -0
  53. /package/src/typed-refs/{movable-list.test.ts → movable-list-ref.test.ts} +0 -0
  54. /package/src/typed-refs/{record-value-updates.test.ts → record-ref-value-updates.test.ts} +0 -0
  55. /package/src/typed-refs/{tree-node-value-updates.test.ts → tree-node-ref.test.ts} +0 -0
  56. /package/src/typed-refs/{tree.test.ts → tree-node.test.ts} +0 -0
@@ -0,0 +1,449 @@
1
+ /**
2
+ * Tests for the loro() escape hatch function and doc.change() method.
3
+ */
4
+
5
+ import { LoroCounter, LoroDoc, LoroList, LoroMap, LoroText } from "loro-crdt"
6
+ import { describe, expect, it } from "vitest"
7
+ import { change, createTypedDoc, loro, Shape } from "./index.js"
8
+
9
+ describe("loro() function", () => {
10
+ describe("with TypedDoc", () => {
11
+ const schema = Shape.doc({
12
+ title: Shape.text(),
13
+ count: Shape.counter(),
14
+ items: Shape.list(Shape.plain.string()),
15
+ settings: Shape.struct({
16
+ darkMode: Shape.plain.boolean().placeholder(false),
17
+ }),
18
+ })
19
+
20
+ it("should access the underlying LoroDoc", () => {
21
+ const doc = createTypedDoc(schema)
22
+
23
+ const loroDoc = loro(doc).doc
24
+ expect(loroDoc).toBeInstanceOf(LoroDoc)
25
+ })
26
+
27
+ it("should access the container (same as doc for TypedDoc)", () => {
28
+ const doc = createTypedDoc(schema)
29
+
30
+ const container = loro(doc).container
31
+ expect(container).toBeInstanceOf(LoroDoc)
32
+ expect(container).toBe(loro(doc).doc)
33
+ })
34
+
35
+ it("should subscribe to doc-level changes", () => {
36
+ const doc = createTypedDoc(schema)
37
+ const events: unknown[] = []
38
+
39
+ const subscription = loro(doc).subscribe(event => {
40
+ events.push(event)
41
+ })
42
+
43
+ doc.title.insert(0, "Hello")
44
+
45
+ expect(events.length).toBeGreaterThan(0)
46
+ subscription()
47
+ })
48
+
49
+ it("should access docShape", () => {
50
+ const doc = createTypedDoc(schema)
51
+
52
+ const docShape = loro(doc).docShape
53
+ expect(docShape).toBe(schema)
54
+ })
55
+
56
+ it("should access rawValue", () => {
57
+ const doc = createTypedDoc(schema)
58
+ doc.title.insert(0, "Hello")
59
+
60
+ const rawValue = loro(doc).rawValue
61
+ expect(rawValue).toHaveProperty("title", "Hello")
62
+ })
63
+
64
+ it("should apply JSON patches", () => {
65
+ const doc = createTypedDoc(schema)
66
+
67
+ // Use add operation for counter since it's a container
68
+ loro(doc).applyPatch([
69
+ { op: "add", path: ["items", 0], value: "test-item" },
70
+ ])
71
+
72
+ expect(doc.items.toJSON()).toContain("test-item")
73
+ })
74
+ })
75
+
76
+ describe("with TextRef", () => {
77
+ const schema = Shape.doc({
78
+ title: Shape.text(),
79
+ })
80
+
81
+ it("should access the underlying LoroDoc", () => {
82
+ const doc = createTypedDoc(schema)
83
+
84
+ const loroDoc = loro(doc.title).doc
85
+ expect(loroDoc).toBeInstanceOf(LoroDoc)
86
+ })
87
+
88
+ it("should access the underlying LoroText container", () => {
89
+ const doc = createTypedDoc(schema)
90
+
91
+ const container = loro(doc.title).container
92
+ expect(container).toBeInstanceOf(LoroText)
93
+ })
94
+
95
+ it("should subscribe to text changes", () => {
96
+ const doc = createTypedDoc(schema)
97
+ const events: unknown[] = []
98
+
99
+ const subscription = loro(doc.title).subscribe(event => {
100
+ events.push(event)
101
+ })
102
+
103
+ doc.title.insert(0, "Hello")
104
+
105
+ expect(events.length).toBeGreaterThan(0)
106
+ subscription()
107
+ })
108
+ })
109
+
110
+ describe("with CounterRef", () => {
111
+ const schema = Shape.doc({
112
+ count: Shape.counter(),
113
+ })
114
+
115
+ it("should access the underlying LoroDoc", () => {
116
+ const doc = createTypedDoc(schema)
117
+
118
+ const loroDoc = loro(doc.count).doc
119
+ expect(loroDoc).toBeInstanceOf(LoroDoc)
120
+ })
121
+
122
+ it("should access the underlying LoroCounter container", () => {
123
+ const doc = createTypedDoc(schema)
124
+
125
+ const container = loro(doc.count).container
126
+ expect(container).toBeInstanceOf(LoroCounter)
127
+ })
128
+
129
+ it("should subscribe to counter changes", () => {
130
+ const doc = createTypedDoc(schema)
131
+ const events: unknown[] = []
132
+
133
+ const subscription = loro(doc.count).subscribe(event => {
134
+ events.push(event)
135
+ })
136
+
137
+ doc.count.increment(5)
138
+
139
+ expect(events.length).toBeGreaterThan(0)
140
+ subscription()
141
+ })
142
+ })
143
+
144
+ describe("with ListRef", () => {
145
+ const schema = Shape.doc({
146
+ items: Shape.list(Shape.plain.string()),
147
+ })
148
+
149
+ it("should access the underlying LoroDoc", () => {
150
+ const doc = createTypedDoc(schema)
151
+
152
+ const loroDoc = loro(doc.items).doc
153
+ expect(loroDoc).toBeInstanceOf(LoroDoc)
154
+ })
155
+
156
+ it("should access the underlying LoroList container", () => {
157
+ const doc = createTypedDoc(schema)
158
+
159
+ const container = loro(doc.items).container
160
+ expect(container).toBeInstanceOf(LoroList)
161
+ })
162
+
163
+ it("should subscribe to list changes", () => {
164
+ const doc = createTypedDoc(schema)
165
+ const events: unknown[] = []
166
+
167
+ const subscription = loro(doc.items).subscribe(event => {
168
+ events.push(event)
169
+ })
170
+
171
+ doc.items.push("item1")
172
+
173
+ expect(events.length).toBeGreaterThan(0)
174
+ subscription()
175
+ })
176
+ })
177
+
178
+ describe("with StructRef", () => {
179
+ const schema = Shape.doc({
180
+ settings: Shape.struct({
181
+ darkMode: Shape.plain.boolean().placeholder(false),
182
+ fontSize: Shape.plain.number().placeholder(14),
183
+ }),
184
+ })
185
+
186
+ it("should access the underlying LoroDoc", () => {
187
+ const doc = createTypedDoc(schema)
188
+
189
+ const loroDoc = loro(doc.settings).doc
190
+ expect(loroDoc).toBeInstanceOf(LoroDoc)
191
+ })
192
+
193
+ it("should access the underlying LoroMap container", () => {
194
+ const doc = createTypedDoc(schema)
195
+
196
+ const container = loro(doc.settings).container
197
+ expect(container).toBeInstanceOf(LoroMap)
198
+ })
199
+
200
+ it("should subscribe to struct changes", () => {
201
+ const doc = createTypedDoc(schema)
202
+ const events: unknown[] = []
203
+
204
+ const subscription = loro(doc.settings).subscribe(event => {
205
+ events.push(event)
206
+ })
207
+
208
+ // Use change() to ensure the subscription fires
209
+ change(doc, draft => {
210
+ draft.settings.darkMode = true
211
+ })
212
+
213
+ expect(events.length).toBeGreaterThan(0)
214
+ subscription()
215
+ })
216
+ })
217
+
218
+ describe("with RecordRef", () => {
219
+ const schema = Shape.doc({
220
+ users: Shape.record(
221
+ Shape.struct({
222
+ name: Shape.plain.string().placeholder(""),
223
+ }),
224
+ ),
225
+ })
226
+
227
+ it("should access the underlying LoroDoc", () => {
228
+ const doc = createTypedDoc(schema)
229
+
230
+ const loroDoc = loro(doc.users).doc
231
+ expect(loroDoc).toBeInstanceOf(LoroDoc)
232
+ })
233
+
234
+ it("should access the underlying LoroMap container", () => {
235
+ const doc = createTypedDoc(schema)
236
+
237
+ const container = loro(doc.users).container
238
+ expect(container).toBeInstanceOf(LoroMap)
239
+ })
240
+
241
+ it("should subscribe to record changes", () => {
242
+ const doc = createTypedDoc(schema)
243
+ const events: unknown[] = []
244
+
245
+ const subscription = loro(doc.users).subscribe(event => {
246
+ events.push(event)
247
+ })
248
+
249
+ change(doc, draft => {
250
+ draft.users.set("alice", { name: "Alice" })
251
+ })
252
+
253
+ expect(events.length).toBeGreaterThan(0)
254
+ subscription()
255
+ })
256
+ })
257
+
258
+ describe("backward compatibility with $", () => {
259
+ const schema = Shape.doc({
260
+ title: Shape.text(),
261
+ count: Shape.counter(),
262
+ })
263
+
264
+ it("$ and loro() should access the same LoroDoc", () => {
265
+ const doc = createTypedDoc(schema)
266
+
267
+ expect(loro(doc).doc).toBe(loro(doc).doc)
268
+ })
269
+
270
+ it("$ and loro() should access the same container for refs", () => {
271
+ const doc = createTypedDoc(schema)
272
+
273
+ expect(loro(doc.title).container).toBe(loro(doc.title).container)
274
+ expect(loro(doc.count).container).toBe(loro(doc.count).container)
275
+ })
276
+ })
277
+
278
+ describe("container operations via loro()", () => {
279
+ describe("ListRef container operations", () => {
280
+ const schema = Shape.doc({
281
+ items: Shape.list(
282
+ Shape.struct({
283
+ name: Shape.plain.string().placeholder(""),
284
+ }),
285
+ ),
286
+ })
287
+
288
+ it("should pushContainer via loro()", () => {
289
+ const doc = createTypedDoc(schema)
290
+ const { LoroMap } = require("loro-crdt")
291
+
292
+ const newMap = new LoroMap()
293
+ newMap.set("name", "pushed-via-loro")
294
+
295
+ // Use loro() to push a container
296
+ const loroList = loro(doc.items) as any
297
+ loroList.pushContainer(newMap)
298
+
299
+ expect(doc.items.length).toBe(1)
300
+ expect(doc.items.toJSON()[0].name).toBe("pushed-via-loro")
301
+ })
302
+
303
+ it("should insertContainer via loro()", () => {
304
+ const doc = createTypedDoc(schema)
305
+ const { LoroMap } = require("loro-crdt")
306
+
307
+ // First add an item normally
308
+ change(doc, draft => {
309
+ draft.items.push({ name: "first" })
310
+ })
311
+
312
+ const newMap = new LoroMap()
313
+ newMap.set("name", "inserted-via-loro")
314
+
315
+ // Use loro() to insert a container at index 0
316
+ const loroList = loro(doc.items) as any
317
+ loroList.insertContainer(0, newMap)
318
+
319
+ expect(doc.items.length).toBe(2)
320
+ expect(doc.items.toJSON()[0].name).toBe("inserted-via-loro")
321
+ expect(doc.items.toJSON()[1].name).toBe("first")
322
+ })
323
+ })
324
+
325
+ describe("StructRef container operations", () => {
326
+ const schema = Shape.doc({
327
+ settings: Shape.struct({
328
+ nested: Shape.struct({
329
+ value: Shape.plain.number().placeholder(0),
330
+ }),
331
+ }),
332
+ })
333
+
334
+ it("should setContainer via loro()", () => {
335
+ const doc = createTypedDoc(schema)
336
+ const { LoroMap } = require("loro-crdt")
337
+
338
+ const newMap = new LoroMap()
339
+ newMap.set("value", 42)
340
+
341
+ // Use loro() to set a container
342
+ const loroStruct = loro(doc.settings) as any
343
+ loroStruct.setContainer("nested", newMap)
344
+
345
+ expect(doc.settings.nested.value).toBe(42)
346
+ })
347
+
348
+ describe("doc.change() method", () => {
349
+ const schema = Shape.doc({
350
+ title: Shape.text(),
351
+ count: Shape.counter(),
352
+ items: Shape.list(Shape.plain.string()),
353
+ })
354
+
355
+ it("should batch mutations via doc.change()", () => {
356
+ const doc = createTypedDoc(schema)
357
+
358
+ doc.change(draft => {
359
+ draft.title.insert(0, "Hello")
360
+ draft.count.increment(5)
361
+ draft.items.push("item1")
362
+ })
363
+
364
+ expect(doc.title.toString()).toBe("Hello")
365
+ expect(doc.count.value).toBe(5)
366
+ expect(doc.items.toJSON()).toEqual(["item1"])
367
+ })
368
+
369
+ it("should return the doc for chaining", () => {
370
+ const doc = createTypedDoc(schema)
371
+
372
+ const result = doc.change(draft => {
373
+ draft.count.increment(1)
374
+ })
375
+
376
+ expect(result).toBe(doc)
377
+ })
378
+
379
+ it("should support chained change() calls", () => {
380
+ const doc = createTypedDoc(schema)
381
+
382
+ doc
383
+ .change(draft => {
384
+ draft.count.increment(1)
385
+ })
386
+ .change(draft => {
387
+ draft.count.increment(2)
388
+ })
389
+ .change(draft => {
390
+ draft.count.increment(3)
391
+ })
392
+
393
+ expect(doc.count.value).toBe(6)
394
+ })
395
+
396
+ it("doc.change() and doc.change() should be equivalent", () => {
397
+ const doc1 = createTypedDoc(schema)
398
+ const doc2 = createTypedDoc(schema)
399
+
400
+ doc1.change(draft => {
401
+ draft.count.increment(5)
402
+ draft.title.insert(0, "Test")
403
+ })
404
+
405
+ doc2.change(draft => {
406
+ draft.count.increment(5)
407
+ draft.title.insert(0, "Test")
408
+ })
409
+
410
+ expect(doc1.toJSON()).toEqual(doc2.toJSON())
411
+ })
412
+
413
+ it("change() helper should use doc.change() internally", () => {
414
+ const doc = createTypedDoc(schema)
415
+
416
+ change(doc, draft => {
417
+ draft.count.increment(10)
418
+ })
419
+
420
+ expect(doc.count.value).toBe(10)
421
+ })
422
+ })
423
+ })
424
+
425
+ describe("RecordRef container operations", () => {
426
+ const schema = Shape.doc({
427
+ users: Shape.record(
428
+ Shape.struct({
429
+ name: Shape.plain.string().placeholder(""),
430
+ }),
431
+ ),
432
+ })
433
+
434
+ it("should setContainer via loro()", () => {
435
+ const doc = createTypedDoc(schema)
436
+ const { LoroMap } = require("loro-crdt")
437
+
438
+ const newMap = new LoroMap()
439
+ newMap.set("name", "Alice via loro")
440
+
441
+ // Use loro() to set a container
442
+ const loroRecord = loro(doc.users) as any
443
+ loroRecord.setContainer("alice", newMap)
444
+
445
+ expect(doc.users.get("alice")?.name).toBe("Alice via loro")
446
+ })
447
+ })
448
+ })
449
+ })
package/src/loro.ts ADDED
@@ -0,0 +1,273 @@
1
+ /**
2
+ * The `loro()` function - single escape hatch for CRDT internals.
3
+ *
4
+ * Design Principle:
5
+ * > If it takes a plain JavaScript value, keep it on the ref.
6
+ * > If it takes a Loro container or exposes CRDT internals, move to `loro()`.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { loro } from "@loro-extended/change"
11
+ *
12
+ * // Access underlying LoroDoc
13
+ * loro(ref).doc
14
+ *
15
+ * // Access underlying Loro container (correctly typed)
16
+ * loro(ref).container // LoroList, LoroMap, LoroText, etc.
17
+ *
18
+ * // Subscribe to changes
19
+ * loro(ref).subscribe(callback)
20
+ *
21
+ * // Container operations
22
+ * loro(list).pushContainer(loroMap)
23
+ * loro(struct).setContainer('key', loroMap)
24
+ * ```
25
+ */
26
+
27
+ import type {
28
+ Container,
29
+ LoroCounter,
30
+ LoroDoc,
31
+ LoroList,
32
+ LoroMap,
33
+ LoroMovableList,
34
+ LoroText,
35
+ LoroTree,
36
+ Subscription,
37
+ } from "loro-crdt"
38
+ import type { JsonPatch } from "./json-patch.js"
39
+ import type {
40
+ ContainerOrValueShape,
41
+ ContainerShape,
42
+ DocShape,
43
+ StructContainerShape,
44
+ } from "./shape.js"
45
+ import type { TypedDoc } from "./typed-doc.js"
46
+ import type { TypedRef } from "./typed-refs/base.js"
47
+ import type { CounterRef } from "./typed-refs/counter-ref.js"
48
+ import type { ListRef } from "./typed-refs/list-ref.js"
49
+ import type { MovableListRef } from "./typed-refs/movable-list-ref.js"
50
+ import type { RecordRef } from "./typed-refs/record-ref.js"
51
+ import type { StructRef } from "./typed-refs/struct-ref.js"
52
+ import type { TextRef } from "./typed-refs/text-ref.js"
53
+ import type { TreeRef } from "./typed-refs/tree-ref.js"
54
+
55
+ // ============================================================================
56
+ // Symbol for loro() access
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Well-known Symbol for loro() access.
61
+ * This is exported so advanced users can access it directly if needed.
62
+ */
63
+ export const LORO_SYMBOL = Symbol.for("loro-extended:loro")
64
+
65
+ // ============================================================================
66
+ // Interface definitions for loro() return types
67
+ // ============================================================================
68
+
69
+ /**
70
+ * Base interface for all loro() return types.
71
+ * Provides access to the underlying LoroDoc, container, and subscription.
72
+ */
73
+ export interface LoroRefBase {
74
+ /** The underlying LoroDoc */
75
+ readonly doc: LoroDoc
76
+
77
+ /** The underlying Loro container */
78
+ readonly container: unknown
79
+
80
+ /**
81
+ * Subscribe to container-level changes.
82
+ * @param callback - Function called when the container changes
83
+ * @returns Subscription that can be used to unsubscribe
84
+ */
85
+ subscribe(callback: (event: unknown) => void): Subscription
86
+ }
87
+
88
+ /**
89
+ * loro() return type for ListRef and MovableListRef.
90
+ * Provides container operations that take Loro containers.
91
+ */
92
+ export interface LoroListRef extends LoroRefBase {
93
+ /** The underlying LoroList or LoroMovableList */
94
+ readonly container: LoroList | LoroMovableList
95
+
96
+ /**
97
+ * Push a Loro container to the end of the list.
98
+ * Use this when you need to add a pre-existing container.
99
+ */
100
+ pushContainer(container: Container): Container
101
+
102
+ /**
103
+ * Insert a Loro container at the specified index.
104
+ * Use this when you need to insert a pre-existing container.
105
+ */
106
+ insertContainer(index: number, container: Container): Container
107
+ }
108
+
109
+ /**
110
+ * loro() return type for StructRef and RecordRef.
111
+ * Provides container operations that take Loro containers.
112
+ */
113
+ export interface LoroMapRef extends LoroRefBase {
114
+ /** The underlying LoroMap */
115
+ readonly container: LoroMap
116
+
117
+ /**
118
+ * Set a Loro container at the specified key.
119
+ * Use this when you need to set a pre-existing container.
120
+ */
121
+ setContainer(key: string, container: Container): Container
122
+ }
123
+
124
+ /**
125
+ * loro() return type for TextRef.
126
+ */
127
+ export interface LoroTextRef extends LoroRefBase {
128
+ /** The underlying LoroText */
129
+ readonly container: LoroText
130
+ }
131
+
132
+ /**
133
+ * loro() return type for CounterRef.
134
+ */
135
+ export interface LoroCounterRef extends LoroRefBase {
136
+ /** The underlying LoroCounter */
137
+ readonly container: LoroCounter
138
+ }
139
+
140
+ /**
141
+ * loro() return type for TreeRef.
142
+ */
143
+ export interface LoroTreeRef extends LoroRefBase {
144
+ /** The underlying LoroTree */
145
+ readonly container: LoroTree
146
+ }
147
+
148
+ /**
149
+ * loro() return type for TypedDoc.
150
+ * Provides access to doc-level operations.
151
+ */
152
+ export interface LoroTypedDocRef extends LoroRefBase {
153
+ /** The underlying LoroDoc (same as doc for TypedDoc) */
154
+ readonly container: LoroDoc
155
+
156
+ /**
157
+ * Apply JSON Patch operations to the document.
158
+ * @param patch - Array of JSON Patch operations (RFC 6902)
159
+ * @param pathPrefix - Optional path prefix for scoped operations
160
+ */
161
+ applyPatch(patch: JsonPatch, pathPrefix?: (string | number)[]): void
162
+
163
+ /** Access the document schema shape */
164
+ readonly docShape: DocShape
165
+
166
+ /** Get raw CRDT value without placeholder overlay */
167
+ readonly rawValue: unknown
168
+ }
169
+
170
+ // ============================================================================
171
+ // loro() function overloads
172
+ // ============================================================================
173
+
174
+ /**
175
+ * Access CRDT internals for a ListRef.
176
+ */
177
+ export function loro<NestedShape extends ContainerShape>(
178
+ ref: ListRef<NestedShape>,
179
+ ): LoroListRef
180
+
181
+ /**
182
+ * Access CRDT internals for a MovableListRef.
183
+ */
184
+ export function loro<NestedShape extends ContainerShape>(
185
+ ref: MovableListRef<NestedShape>,
186
+ ): LoroListRef
187
+
188
+ /**
189
+ * Access CRDT internals for a StructRef.
190
+ */
191
+ export function loro<
192
+ NestedShapes extends Record<string, ContainerOrValueShape>,
193
+ >(ref: StructRef<NestedShapes>): LoroMapRef
194
+
195
+ /**
196
+ * Access CRDT internals for a RecordRef.
197
+ */
198
+ export function loro<NestedShape extends ContainerShape>(
199
+ ref: RecordRef<NestedShape>,
200
+ ): LoroMapRef
201
+
202
+ /**
203
+ * Access CRDT internals for a TextRef.
204
+ */
205
+ export function loro(ref: TextRef): LoroTextRef
206
+
207
+ /**
208
+ * Access CRDT internals for a CounterRef.
209
+ */
210
+ export function loro(ref: CounterRef): LoroCounterRef
211
+
212
+ /**
213
+ * Access CRDT internals for a TreeRef.
214
+ */
215
+ export function loro<DataShape extends StructContainerShape>(
216
+ ref: TreeRef<DataShape>,
217
+ ): LoroTreeRef
218
+
219
+ /**
220
+ * Access CRDT internals for a TypedDoc.
221
+ */
222
+ export function loro<Shape extends DocShape>(
223
+ doc: TypedDoc<Shape>,
224
+ ): LoroTypedDocRef
225
+
226
+ /**
227
+ * Access CRDT internals for any TypedRef.
228
+ */
229
+ export function loro<Shape extends ContainerShape>(
230
+ ref: TypedRef<Shape>,
231
+ ): LoroRefBase
232
+
233
+ /**
234
+ * The `loro()` function - single escape hatch for CRDT internals.
235
+ *
236
+ * Use this to access:
237
+ * - The underlying LoroDoc
238
+ * - The underlying Loro container (correctly typed)
239
+ * - Container-level subscriptions
240
+ * - Container operations that take Loro containers (pushContainer, setContainer, etc.)
241
+ *
242
+ * @param refOrDoc - A TypedRef or TypedDoc
243
+ * @returns An object with CRDT internals and operations
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * import { loro } from "@loro-extended/change"
248
+ *
249
+ * // Access underlying LoroDoc
250
+ * loro(doc.settings).doc
251
+ *
252
+ * // Access underlying Loro container
253
+ * loro(doc.items).container // LoroList
254
+ *
255
+ * // Subscribe to changes
256
+ * loro(doc.settings).subscribe(event => { ... })
257
+ *
258
+ * // Container operations
259
+ * loro(doc.items).pushContainer(loroMap)
260
+ * ```
261
+ */
262
+ export function loro(
263
+ refOrDoc: TypedRef<any> | TypedDoc<any> | TreeRef<any> | StructRef<any>,
264
+ ): LoroRefBase {
265
+ // Access the loro namespace via the well-known symbol
266
+ const loroNamespace = (refOrDoc as any)[LORO_SYMBOL]
267
+ if (!loroNamespace) {
268
+ throw new Error(
269
+ "Invalid argument: expected TypedRef, TreeRef, or TypedDoc with loro() support",
270
+ )
271
+ }
272
+ return loroNamespace
273
+ }