@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.
- package/README.md +78 -0
- package/dist/index.d.ts +199 -43
- package/dist/index.js +642 -429
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +277 -1
- package/src/conversion.test.ts +72 -72
- package/src/conversion.ts +5 -5
- package/src/discriminated-union-assignability.test.ts +45 -0
- package/src/discriminated-union-tojson.test.ts +128 -0
- package/src/index.ts +7 -0
- package/src/overlay-recursion.test.ts +325 -0
- package/src/overlay.ts +45 -8
- package/src/placeholder-proxy.test.ts +52 -0
- package/src/placeholder-proxy.ts +37 -0
- package/src/presence-interface.ts +52 -0
- package/src/shape.ts +44 -50
- package/src/typed-doc.ts +4 -4
- package/src/typed-presence.ts +96 -0
- package/src/{draft-nodes → typed-refs}/base.ts +14 -4
- package/src/{draft-nodes → typed-refs}/counter.test.ts +1 -1
- package/src/{draft-nodes → typed-refs}/counter.ts +9 -3
- package/src/{draft-nodes → typed-refs}/doc.ts +32 -25
- package/src/typed-refs/json-compatibility.test.ts +255 -0
- package/src/{draft-nodes → typed-refs}/list-base.ts +115 -42
- package/src/{draft-nodes → typed-refs}/list.test.ts +1 -1
- package/src/{draft-nodes → typed-refs}/list.ts +4 -4
- package/src/{draft-nodes → typed-refs}/map.ts +50 -66
- package/src/{draft-nodes → typed-refs}/movable-list.test.ts +1 -1
- package/src/{draft-nodes → typed-refs}/movable-list.ts +6 -6
- package/src/{draft-nodes → typed-refs}/proxy-handlers.ts +25 -26
- package/src/{draft-nodes → typed-refs}/record.test.ts +78 -9
- package/src/typed-refs/record.ts +193 -0
- package/src/{draft-nodes → typed-refs}/text.ts +13 -3
- package/src/{draft-nodes → typed-refs}/tree.ts +6 -3
- package/src/typed-refs/utils.ts +177 -0
- package/src/types.test.ts +97 -2
- package/src/types.ts +62 -5
- package/src/draft-nodes/counter.md +0 -31
- package/src/draft-nodes/record.ts +0 -177
- 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 {
|
|
13
|
-
import type {
|
|
14
|
-
import type {
|
|
15
|
-
import type {
|
|
16
|
-
import type {
|
|
17
|
-
import type {
|
|
18
|
-
|
|
19
|
-
export interface Shape<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
|
|
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]["
|
|
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,
|
|
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"][],
|
|
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
|
-
|
|
93
|
-
[K in keyof NestedShapes]: NestedShapes[K]["
|
|
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
|
-
|
|
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]["
|
|
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["
|
|
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["
|
|
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]["
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
72
|
-
const draft = new
|
|
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
|
|
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
|
|
12
|
-
export abstract class
|
|
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:
|
|
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
|
|
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 {
|
|
2
|
+
import { TypedRef } from "./base.js"
|
|
3
3
|
|
|
4
|
-
// Counter
|
|
5
|
-
export class
|
|
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
|
}
|