@loro-extended/change 0.8.1 → 0.9.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.
Files changed (41) hide show
  1. package/README.md +78 -0
  2. package/dist/index.d.ts +199 -43
  3. package/dist/index.js +642 -429
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/change.test.ts +277 -1
  7. package/src/conversion.test.ts +72 -72
  8. package/src/conversion.ts +5 -5
  9. package/src/discriminated-union-assignability.test.ts +45 -0
  10. package/src/discriminated-union-tojson.test.ts +128 -0
  11. package/src/index.ts +7 -0
  12. package/src/overlay-recursion.test.ts +325 -0
  13. package/src/overlay.ts +45 -8
  14. package/src/placeholder-proxy.test.ts +52 -0
  15. package/src/placeholder-proxy.ts +37 -0
  16. package/src/presence-interface.ts +52 -0
  17. package/src/shape.ts +44 -50
  18. package/src/typed-doc.ts +4 -4
  19. package/src/typed-presence.ts +96 -0
  20. package/src/{draft-nodes → typed-refs}/base.ts +14 -4
  21. package/src/{draft-nodes → typed-refs}/counter.test.ts +1 -1
  22. package/src/{draft-nodes → typed-refs}/counter.ts +9 -3
  23. package/src/{draft-nodes → typed-refs}/doc.ts +32 -25
  24. package/src/typed-refs/json-compatibility.test.ts +255 -0
  25. package/src/{draft-nodes → typed-refs}/list-base.ts +115 -42
  26. package/src/{draft-nodes → typed-refs}/list.test.ts +1 -1
  27. package/src/{draft-nodes → typed-refs}/list.ts +4 -4
  28. package/src/{draft-nodes → typed-refs}/map.ts +50 -66
  29. package/src/{draft-nodes → typed-refs}/movable-list.test.ts +1 -1
  30. package/src/{draft-nodes → typed-refs}/movable-list.ts +6 -6
  31. package/src/{draft-nodes → typed-refs}/proxy-handlers.ts +25 -26
  32. package/src/{draft-nodes → typed-refs}/record.test.ts +78 -9
  33. package/src/typed-refs/record.ts +193 -0
  34. package/src/{draft-nodes → typed-refs}/text.ts +13 -3
  35. package/src/{draft-nodes → typed-refs}/tree.ts +6 -3
  36. package/src/typed-refs/utils.ts +177 -0
  37. package/src/types.test.ts +97 -2
  38. package/src/types.ts +62 -5
  39. package/src/draft-nodes/counter.md +0 -31
  40. package/src/draft-nodes/record.ts +0 -177
  41. package/src/draft-nodes/utils.ts +0 -96
@@ -0,0 +1,52 @@
1
+ import type { Value } from "loro-crdt"
2
+
3
+ /**
4
+ * A record of string keys to Loro values, used for presence data.
5
+ */
6
+ export type ObjectValue = Record<string, Value>
7
+
8
+ /**
9
+ * Interface for presence management that can be implemented by different backends.
10
+ * This abstraction allows TypedPresence to work with any presence provider.
11
+ */
12
+ export interface PresenceInterface {
13
+ /**
14
+ * Set multiple presence values at once.
15
+ */
16
+ set: (values: ObjectValue) => void
17
+
18
+ /**
19
+ * Get a single presence value by key.
20
+ */
21
+ get: (key: string) => Value
22
+
23
+ /**
24
+ * The current peer's presence state.
25
+ */
26
+ readonly self: ObjectValue
27
+
28
+ /**
29
+ * Other peers' presence states, keyed by peer ID.
30
+ * Does NOT include self. Use this for iterating over remote peers.
31
+ */
32
+ readonly peers: Map<string, ObjectValue>
33
+
34
+ /**
35
+ * All peers' presence states, keyed by peer ID (includes self).
36
+ * @deprecated Use `peers` and `self` separately. This property is synthesized
37
+ * from `peers` and `self` for backward compatibility.
38
+ */
39
+ readonly all: Record<string, ObjectValue>
40
+
41
+ /**
42
+ * Set a single raw value by key (escape hatch for arbitrary keys).
43
+ */
44
+ setRaw: (key: string, value: Value) => void
45
+
46
+ /**
47
+ * Subscribe to presence changes.
48
+ * @param cb Callback that receives the aggregated presence values
49
+ * @returns Unsubscribe function
50
+ */
51
+ subscribe: (cb: (values: ObjectValue) => void) => () => void
52
+ }
package/src/shape.ts CHANGED
@@ -9,17 +9,17 @@ import type {
9
9
  LoroTree,
10
10
  } from "loro-crdt"
11
11
 
12
- import type { CounterDraftNode } from "./draft-nodes/counter.js"
13
- import type { ListDraftNode } from "./draft-nodes/list.js"
14
- import type { MapDraftNode } from "./draft-nodes/map.js"
15
- import type { MovableListDraftNode } from "./draft-nodes/movable-list.js"
16
- import type { RecordDraftNode } from "./draft-nodes/record.js"
17
- import type { TextDraftNode } from "./draft-nodes/text.js"
18
-
19
- export interface Shape<Plain, Draft, Placeholder = Plain> {
12
+ import type { CounterRef } from "./typed-refs/counter.js"
13
+ import type { ListRef } from "./typed-refs/list.js"
14
+ import type { MapRef } from "./typed-refs/map.js"
15
+ import type { MovableListRef } from "./typed-refs/movable-list.js"
16
+ import type { RecordRef } from "./typed-refs/record.js"
17
+ import type { TextRef } from "./typed-refs/text.js"
18
+
19
+ export interface Shape<Plain, Mutable, Placeholder = Plain> {
20
20
  readonly _type: string
21
21
  readonly _plain: Plain
22
- readonly _draft: Draft
22
+ readonly _mutable: Mutable
23
23
  readonly _placeholder: Placeholder
24
24
  }
25
25
 
@@ -35,7 +35,7 @@ export interface DocShape<
35
35
  >,
36
36
  > extends Shape<
37
37
  { [K in keyof NestedShapes]: NestedShapes[K]["_plain"] },
38
- { [K in keyof NestedShapes]: NestedShapes[K]["_draft"] },
38
+ { [K in keyof NestedShapes]: NestedShapes[K]["_mutable"] },
39
39
  { [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"] }
40
40
  > {
41
41
  readonly _type: "doc"
@@ -43,12 +43,11 @@ export interface DocShape<
43
43
  readonly shapes: NestedShapes
44
44
  }
45
45
 
46
- export interface TextContainerShape
47
- extends Shape<string, TextDraftNode, string> {
46
+ export interface TextContainerShape extends Shape<string, TextRef, string> {
48
47
  readonly _type: "text"
49
48
  }
50
49
  export interface CounterContainerShape
51
- extends Shape<number, CounterDraftNode, number> {
50
+ extends Shape<number, CounterRef, number> {
52
51
  readonly _type: "counter"
53
52
  }
54
53
  export interface TreeContainerShape<NestedShape = ContainerOrValueShape>
@@ -64,7 +63,7 @@ export interface TreeContainerShape<NestedShape = ContainerOrValueShape>
64
63
  // This prevents users from expecting per-entry merging behavior.
65
64
  export interface ListContainerShape<
66
65
  NestedShape extends ContainerOrValueShape = ContainerOrValueShape,
67
- > extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape>, never[]> {
66
+ > extends Shape<NestedShape["_plain"][], ListRef<NestedShape>, never[]> {
68
67
  readonly _type: "list"
69
68
  // A list contains many elements, all of the same 'shape'
70
69
  readonly shape: NestedShape
@@ -72,11 +71,7 @@ export interface ListContainerShape<
72
71
 
73
72
  export interface MovableListContainerShape<
74
73
  NestedShape extends ContainerOrValueShape = ContainerOrValueShape,
75
- > extends Shape<
76
- NestedShape["_plain"][],
77
- MovableListDraftNode<NestedShape>,
78
- never[]
79
- > {
74
+ > extends Shape<NestedShape["_plain"][], MovableListRef<NestedShape>, never[]> {
80
75
  readonly _type: "movableList"
81
76
  // A list contains many elements, all of the same 'shape'
82
77
  readonly shape: NestedShape
@@ -89,8 +84,8 @@ export interface MapContainerShape<
89
84
  >,
90
85
  > extends Shape<
91
86
  { [K in keyof NestedShapes]: NestedShapes[K]["_plain"] },
92
- MapDraftNode<NestedShapes> & {
93
- [K in keyof NestedShapes]: NestedShapes[K]["_draft"]
87
+ MapRef<NestedShapes> & {
88
+ [K in keyof NestedShapes]: NestedShapes[K]["_mutable"]
94
89
  },
95
90
  { [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"] }
96
91
  > {
@@ -103,7 +98,7 @@ export interface RecordContainerShape<
103
98
  NestedShape extends ContainerOrValueShape = ContainerOrValueShape,
104
99
  > extends Shape<
105
100
  Record<string, NestedShape["_plain"]>,
106
- RecordDraftNode<NestedShape>,
101
+ RecordRef<NestedShape>,
107
102
  Record<string, never>
108
103
  > {
109
104
  readonly _type: "record"
@@ -155,7 +150,7 @@ export interface ObjectValueShape<
155
150
  T extends Record<string, ValueShape> = Record<string, ValueShape>,
156
151
  > extends Shape<
157
152
  { [K in keyof T]: T[K]["_plain"] },
158
- { [K in keyof T]: T[K]["_draft"] },
153
+ { [K in keyof T]: T[K]["_mutable"] },
159
154
  { [K in keyof T]: T[K]["_placeholder"] }
160
155
  > {
161
156
  readonly _type: "value"
@@ -168,7 +163,7 @@ export interface ObjectValueShape<
168
163
  export interface RecordValueShape<T extends ValueShape = ValueShape>
169
164
  extends Shape<
170
165
  Record<string, T["_plain"]>,
171
- Record<string, T["_draft"]>,
166
+ Record<string, T["_mutable"]>,
172
167
  Record<string, never>
173
168
  > {
174
169
  readonly _type: "value"
@@ -177,7 +172,7 @@ export interface RecordValueShape<T extends ValueShape = ValueShape>
177
172
  }
178
173
 
179
174
  export interface ArrayValueShape<T extends ValueShape = ValueShape>
180
- extends Shape<T["_plain"][], T["_draft"][], never[]> {
175
+ extends Shape<T["_plain"][], T["_mutable"][], never[]> {
181
176
  readonly _type: "value"
182
177
  readonly valueType: "array"
183
178
  readonly shape: T
@@ -186,7 +181,7 @@ export interface ArrayValueShape<T extends ValueShape = ValueShape>
186
181
  export interface UnionValueShape<T extends ValueShape[] = ValueShape[]>
187
182
  extends Shape<
188
183
  T[number]["_plain"],
189
- T[number]["_draft"],
184
+ T[number]["_mutable"],
190
185
  T[number]["_placeholder"]
191
186
  > {
192
187
  readonly _type: "value"
@@ -210,11 +205,10 @@ export interface UnionValueShape<T extends ValueShape[] = ValueShape[]>
210
205
  export interface DiscriminatedUnionValueShape<
211
206
  K extends string = string,
212
207
  T extends Record<string, ObjectValueShape> = Record<string, ObjectValueShape>,
213
- > extends Shape<
214
- T[keyof T]["_plain"],
215
- T[keyof T]["_draft"],
216
- T[keyof T]["_placeholder"]
217
- > {
208
+ Plain = T[keyof T]["_plain"],
209
+ Mutable = T[keyof T]["_mutable"],
210
+ Placeholder = T[keyof T]["_placeholder"],
211
+ > extends Shape<Plain, Mutable, Placeholder> {
218
212
  readonly _type: "value"
219
213
  readonly valueType: "discriminatedUnion"
220
214
  readonly discriminantKey: K
@@ -249,7 +243,7 @@ export const Shape = {
249
243
  _type: "doc" as const,
250
244
  shapes: shape,
251
245
  _plain: {} as any,
252
- _draft: {} as any,
246
+ _mutable: {} as any,
253
247
  _placeholder: {} as any,
254
248
  }),
255
249
 
@@ -259,7 +253,7 @@ export const Shape = {
259
253
  const base: CounterContainerShape = {
260
254
  _type: "counter" as const,
261
255
  _plain: 0,
262
- _draft: {} as CounterDraftNode,
256
+ _mutable: {} as CounterRef,
263
257
  _placeholder: 0,
264
258
  }
265
259
  return Object.assign(base, {
@@ -273,7 +267,7 @@ export const Shape = {
273
267
  _type: "list" as const,
274
268
  shape,
275
269
  _plain: [] as any,
276
- _draft: {} as any,
270
+ _mutable: {} as any,
277
271
  _placeholder: [] as never[],
278
272
  }),
279
273
 
@@ -283,7 +277,7 @@ export const Shape = {
283
277
  _type: "map" as const,
284
278
  shapes: shape,
285
279
  _plain: {} as any,
286
- _draft: {} as any,
280
+ _mutable: {} as any,
287
281
  _placeholder: {} as any,
288
282
  }),
289
283
 
@@ -293,7 +287,7 @@ export const Shape = {
293
287
  _type: "record" as const,
294
288
  shape,
295
289
  _plain: {} as any,
296
- _draft: {} as any,
290
+ _mutable: {} as any,
297
291
  _placeholder: {} as Record<string, never>,
298
292
  }),
299
293
 
@@ -303,7 +297,7 @@ export const Shape = {
303
297
  _type: "movableList" as const,
304
298
  shape,
305
299
  _plain: [] as any,
306
- _draft: {} as any,
300
+ _mutable: {} as any,
307
301
  _placeholder: [] as never[],
308
302
  }),
309
303
 
@@ -311,7 +305,7 @@ export const Shape = {
311
305
  const base: TextContainerShape = {
312
306
  _type: "text" as const,
313
307
  _plain: "",
314
- _draft: {} as TextDraftNode,
308
+ _mutable: {} as TextRef,
315
309
  _placeholder: "",
316
310
  }
317
311
  return Object.assign(base, {
@@ -325,7 +319,7 @@ export const Shape = {
325
319
  _type: "tree" as const,
326
320
  shape,
327
321
  _plain: {} as any,
328
- _draft: {} as any,
322
+ _mutable: {} as any,
329
323
  _placeholder: [] as never[],
330
324
  }),
331
325
 
@@ -341,7 +335,7 @@ export const Shape = {
341
335
  _type: "value" as const,
342
336
  valueType: "string" as const,
343
337
  _plain: (options[0] ?? "") as T,
344
- _draft: (options[0] ?? "") as T,
338
+ _mutable: (options[0] ?? "") as T,
345
339
  _placeholder: (options[0] ?? "") as T,
346
340
  options: options.length > 0 ? options : undefined,
347
341
  }
@@ -357,7 +351,7 @@ export const Shape = {
357
351
  _type: "value" as const,
358
352
  valueType: "number" as const,
359
353
  _plain: 0,
360
- _draft: 0,
354
+ _mutable: 0,
361
355
  _placeholder: 0,
362
356
  }
363
357
  return Object.assign(base, {
@@ -372,7 +366,7 @@ export const Shape = {
372
366
  _type: "value" as const,
373
367
  valueType: "boolean" as const,
374
368
  _plain: false,
375
- _draft: false,
369
+ _mutable: false,
376
370
  _placeholder: false,
377
371
  }
378
372
  return Object.assign(base, {
@@ -386,7 +380,7 @@ export const Shape = {
386
380
  _type: "value" as const,
387
381
  valueType: "null" as const,
388
382
  _plain: null,
389
- _draft: null,
383
+ _mutable: null,
390
384
  _placeholder: null,
391
385
  }),
392
386
 
@@ -394,7 +388,7 @@ export const Shape = {
394
388
  _type: "value" as const,
395
389
  valueType: "undefined" as const,
396
390
  _plain: undefined,
397
- _draft: undefined,
391
+ _mutable: undefined,
398
392
  _placeholder: undefined,
399
393
  }),
400
394
 
@@ -402,7 +396,7 @@ export const Shape = {
402
396
  _type: "value" as const,
403
397
  valueType: "uint8array" as const,
404
398
  _plain: new Uint8Array(),
405
- _draft: new Uint8Array(),
399
+ _mutable: new Uint8Array(),
406
400
  _placeholder: new Uint8Array(),
407
401
  }),
408
402
 
@@ -413,7 +407,7 @@ export const Shape = {
413
407
  valueType: "object" as const,
414
408
  shape,
415
409
  _plain: {} as any,
416
- _draft: {} as any,
410
+ _mutable: {} as any,
417
411
  _placeholder: {} as any,
418
412
  }),
419
413
 
@@ -422,7 +416,7 @@ export const Shape = {
422
416
  valueType: "record" as const,
423
417
  shape,
424
418
  _plain: {} as any,
425
- _draft: {} as any,
419
+ _mutable: {} as any,
426
420
  _placeholder: {} as Record<string, never>,
427
421
  }),
428
422
 
@@ -431,7 +425,7 @@ export const Shape = {
431
425
  valueType: "array" as const,
432
426
  shape,
433
427
  _plain: [] as any,
434
- _draft: [] as any,
428
+ _mutable: [] as any,
435
429
  _placeholder: [] as never[],
436
430
  }),
437
431
 
@@ -445,7 +439,7 @@ export const Shape = {
445
439
  valueType: "union" as const,
446
440
  shapes,
447
441
  _plain: {} as any,
448
- _draft: {} as any,
442
+ _mutable: {} as any,
449
443
  _placeholder: {} as any,
450
444
  }
451
445
  return Object.assign(base, {
@@ -494,7 +488,7 @@ export const Shape = {
494
488
  discriminantKey,
495
489
  variants,
496
490
  _plain: {} as any,
497
- _draft: {} as any,
491
+ _mutable: {} as any,
498
492
  _placeholder: {} as any,
499
493
  }
500
494
  return Object.assign(base, {
package/src/typed-doc.ts CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { LoroDoc } from "loro-crdt"
4
4
  import { derivePlaceholder } from "./derive-placeholder.js"
5
- import { DraftDoc } from "./draft-nodes/doc.js"
6
5
  import {
7
6
  type JsonPatch,
8
7
  JsonPatchApplicator,
@@ -11,6 +10,7 @@ import {
11
10
  } from "./json-patch.js"
12
11
  import { overlayPlaceholder } from "./overlay.js"
13
12
  import type { DocShape } from "./shape.js"
13
+ import { DocRef } from "./typed-refs/doc.js"
14
14
  import type {
15
15
  DeepReadonly,
16
16
  Draft,
@@ -46,7 +46,7 @@ export class TypedDoc<Shape extends DocShape> {
46
46
  * This is efficient (O(1) per access) and always up-to-date.
47
47
  */
48
48
  get value(): DeepReadonly<Infer<Shape>> {
49
- return new DraftDoc({
49
+ return new DocRef({
50
50
  shape: this.shape,
51
51
  placeholder: this.placeholder as any,
52
52
  doc: this.doc,
@@ -68,8 +68,8 @@ export class TypedDoc<Shape extends DocShape> {
68
68
  }
69
69
 
70
70
  change(fn: (draft: Draft<Shape>) => void): Infer<Shape> {
71
- // Reuse existing DocumentDraft system with placeholder integration
72
- const draft = new DraftDoc({
71
+ // Reuse existing DocRef system with placeholder integration
72
+ const draft = new DocRef({
73
73
  shape: this.shape,
74
74
  placeholder: this.placeholder as any,
75
75
  doc: this.doc,
@@ -0,0 +1,96 @@
1
+ import { deriveShapePlaceholder } from "./derive-placeholder.js"
2
+ import { mergeValue } from "./overlay.js"
3
+ import type { ObjectValue, PresenceInterface } from "./presence-interface.js"
4
+ import type { ContainerShape, ValueShape } from "./shape.js"
5
+ import type { Infer } from "./types.js"
6
+
7
+ /**
8
+ * A strongly-typed wrapper around a PresenceInterface.
9
+ * Provides type-safe access to presence data with automatic placeholder merging.
10
+ *
11
+ * @typeParam S - The shape of the presence data
12
+ */
13
+ export class TypedPresence<S extends ContainerShape | ValueShape> {
14
+ private placeholder: Infer<S>
15
+
16
+ constructor(
17
+ public shape: S,
18
+ private presence: PresenceInterface,
19
+ ) {
20
+ this.placeholder = deriveShapePlaceholder(shape) as Infer<S>
21
+ }
22
+
23
+ /**
24
+ * Get the current peer's presence state with placeholder values merged in.
25
+ */
26
+ get self(): Infer<S> {
27
+ return mergeValue(
28
+ this.shape,
29
+ this.presence.self,
30
+ this.placeholder,
31
+ ) as Infer<S>
32
+ }
33
+
34
+ /**
35
+ * Get other peers' presence states with placeholder values merged in.
36
+ * Does NOT include self. Use this for iterating over remote peers.
37
+ */
38
+ get peers(): Map<string, Infer<S>> {
39
+ const result = new Map<string, Infer<S>>()
40
+ for (const [peerId, value] of this.presence.peers) {
41
+ result.set(
42
+ peerId,
43
+ mergeValue(this.shape, value, this.placeholder) as Infer<S>,
44
+ )
45
+ }
46
+ return result
47
+ }
48
+
49
+ /**
50
+ * Get all peers' presence states with placeholder values merged in.
51
+ * @deprecated Use `peers` and `self` separately. This property is synthesized
52
+ * from `peers` and `self` for backward compatibility.
53
+ */
54
+ get all(): Record<string, Infer<S>> {
55
+ const result: Record<string, Infer<S>> = {}
56
+ const all = this.presence.all
57
+ for (const peerId of Object.keys(all)) {
58
+ result[peerId] = mergeValue(
59
+ this.shape,
60
+ all[peerId],
61
+ this.placeholder,
62
+ ) as Infer<S>
63
+ }
64
+ return result
65
+ }
66
+
67
+ /**
68
+ * Set presence values for the current peer.
69
+ */
70
+ set(value: Partial<Infer<S>>) {
71
+ this.presence.set(value as ObjectValue)
72
+ }
73
+
74
+ /**
75
+ * Subscribe to presence changes.
76
+ * The callback is called immediately with the current state, then on each change.
77
+ *
78
+ * @param cb Callback that receives the typed presence state
79
+ * @returns Unsubscribe function
80
+ */
81
+ subscribe(
82
+ cb: (state: {
83
+ self: Infer<S>
84
+ peers: Map<string, Infer<S>>
85
+ /** @deprecated Use `peers` and `self` separately */
86
+ all: Record<string, Infer<S>>
87
+ }) => void,
88
+ ): () => void {
89
+ // Initial call
90
+ cb({ self: this.self, peers: this.peers, all: this.all })
91
+
92
+ return this.presence.subscribe(() => {
93
+ cb({ self: this.self, peers: this.peers, all: this.all })
94
+ })
95
+ }
96
+ }
@@ -1,18 +1,18 @@
1
1
  import type { ContainerShape, DocShape, ShapeToContainer } from "../shape.js"
2
2
  import type { Infer } from "../types.js"
3
3
 
4
- export type DraftNodeParams<Shape extends DocShape | ContainerShape> = {
4
+ export type TypedRefParams<Shape extends DocShape | ContainerShape> = {
5
5
  shape: Shape
6
6
  placeholder?: Infer<Shape>
7
7
  getContainer: () => ShapeToContainer<Shape>
8
8
  readonly?: boolean
9
9
  }
10
10
 
11
- // Base class for all draft nodes
12
- export abstract class DraftNode<Shape extends DocShape | ContainerShape> {
11
+ // Base class for all typed refs
12
+ export abstract class TypedRef<Shape extends DocShape | ContainerShape> {
13
13
  protected _cachedContainer?: ShapeToContainer<Shape>
14
14
 
15
- constructor(protected _params: DraftNodeParams<Shape>) {}
15
+ constructor(protected _params: TypedRefParams<Shape>) {}
16
16
 
17
17
  abstract absorbPlainValues(): void
18
18
 
@@ -28,6 +28,16 @@ export abstract class DraftNode<Shape extends DocShape | ContainerShape> {
28
28
  return !!this._params.readonly
29
29
  }
30
30
 
31
+ /**
32
+ * Throws an error if this ref is in readonly mode.
33
+ * Call this at the start of any mutating method.
34
+ */
35
+ protected assertMutable(): void {
36
+ if (this.readonly) {
37
+ throw new Error("Cannot modify readonly ref")
38
+ }
39
+ }
40
+
31
41
  protected get container(): ShapeToContainer<Shape> {
32
42
  if (!this._cachedContainer) {
33
43
  const container = this._params.getContainer()
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"
2
2
  import { Shape } from "../shape.js"
3
3
  import { createTypedDoc } from "../typed-doc.js"
4
4
 
5
- describe("Counter Draft Node", () => {
5
+ describe("Counter Ref", () => {
6
6
  it("should return placeholder value without materializing the container", () => {
7
7
  const schema = Shape.doc({
8
8
  counter: Shape.counter().placeholder(10),
@@ -1,21 +1,27 @@
1
1
  import type { CounterContainerShape } from "../shape.js"
2
- import { DraftNode } from "./base.js"
2
+ import { TypedRef } from "./base.js"
3
3
 
4
- // Counter draft node
5
- export class CounterDraftNode extends DraftNode<CounterContainerShape> {
4
+ // Counter typed ref
5
+ export class CounterRef extends TypedRef<CounterContainerShape> {
6
6
  absorbPlainValues() {
7
7
  // no plain values contained within
8
8
  }
9
9
 
10
10
  increment(value: number): void {
11
+ this.assertMutable()
11
12
  this.container.increment(value)
12
13
  }
13
14
 
14
15
  decrement(value: number): void {
16
+ this.assertMutable()
15
17
  this.container.decrement(value)
16
18
  }
17
19
 
18
20
  get value(): number {
19
21
  return this.container.value
20
22
  }
23
+
24
+ toJSON(): number {
25
+ return this.value
26
+ }
21
27
  }