@loro-extended/change 0.9.1 → 1.0.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 +201 -93
- package/dist/index.d.ts +361 -169
- package/dist/index.js +516 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +180 -175
- package/src/conversion.test.ts +19 -19
- package/src/conversion.ts +7 -7
- package/src/derive-placeholder.test.ts +14 -14
- package/src/derive-placeholder.ts +3 -3
- package/src/discriminated-union-assignability.test.ts +7 -7
- package/src/discriminated-union-tojson.test.ts +13 -24
- package/src/discriminated-union.test.ts +9 -8
- package/src/equality.test.ts +10 -2
- package/src/functional-helpers.test.ts +149 -0
- package/src/functional-helpers.ts +61 -0
- package/src/grand-unified-api.test.ts +423 -0
- package/src/index.ts +8 -6
- package/src/json-patch.test.ts +64 -56
- package/src/overlay-recursion.test.ts +23 -22
- package/src/overlay.ts +9 -9
- package/src/readonly.test.ts +27 -26
- package/src/shape.ts +103 -21
- package/src/typed-doc.ts +227 -58
- package/src/typed-refs/base.ts +23 -1
- package/src/typed-refs/counter.test.ts +44 -13
- package/src/typed-refs/counter.ts +40 -3
- package/src/typed-refs/doc.ts +12 -6
- package/src/typed-refs/json-compatibility.test.ts +37 -32
- package/src/typed-refs/list-base.ts +26 -22
- package/src/typed-refs/list.test.ts +4 -3
- package/src/typed-refs/movable-list.test.ts +3 -2
- package/src/typed-refs/movable-list.ts +4 -1
- package/src/typed-refs/proxy-handlers.ts +14 -1
- package/src/typed-refs/record.test.ts +107 -42
- package/src/typed-refs/record.ts +37 -19
- package/src/typed-refs/{map.ts → struct.ts} +31 -16
- package/src/typed-refs/text.ts +42 -1
- package/src/typed-refs/utils.ts +28 -6
- package/src/types.test.ts +34 -39
- package/src/types.ts +5 -40
- package/src/utils/type-guards.ts +11 -6
- package/src/validation.ts +10 -10
|
@@ -5,7 +5,8 @@ import type { RecordRef } from "./record.js"
|
|
|
5
5
|
export const recordProxyHandler: ProxyHandler<RecordRef<any>> = {
|
|
6
6
|
get: (target, prop) => {
|
|
7
7
|
if (typeof prop === "string" && !(prop in target)) {
|
|
8
|
-
|
|
8
|
+
// Use getRef for reading - returns undefined for non-existent keys
|
|
9
|
+
return target.getRef(prop)
|
|
9
10
|
}
|
|
10
11
|
return Reflect.get(target, prop)
|
|
11
12
|
},
|
|
@@ -23,6 +24,18 @@ export const recordProxyHandler: ProxyHandler<RecordRef<any>> = {
|
|
|
23
24
|
}
|
|
24
25
|
return Reflect.deleteProperty(target, prop)
|
|
25
26
|
},
|
|
27
|
+
// Support `in` operator for checking key existence
|
|
28
|
+
has: (target, prop) => {
|
|
29
|
+
if (typeof prop === "string") {
|
|
30
|
+
// Check if it's a method/property on the class first
|
|
31
|
+
if (prop in target) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
// Otherwise check the underlying container
|
|
35
|
+
return target.has(prop)
|
|
36
|
+
}
|
|
37
|
+
return Reflect.has(target, prop)
|
|
38
|
+
},
|
|
26
39
|
ownKeys: target => {
|
|
27
40
|
return target.keys()
|
|
28
41
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import {
|
|
2
|
+
import { change } from "../functional-helpers.js"
|
|
3
|
+
import { createTypedDoc, Shape } from "../index.js"
|
|
3
4
|
|
|
4
5
|
describe("Record Types", () => {
|
|
5
6
|
describe("Shape.record (Container)", () => {
|
|
@@ -8,9 +9,9 @@ describe("Record Types", () => {
|
|
|
8
9
|
scores: Shape.record(Shape.counter()),
|
|
9
10
|
})
|
|
10
11
|
|
|
11
|
-
const doc =
|
|
12
|
+
const doc = createTypedDoc(schema)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
change(doc, draft => {
|
|
14
15
|
draft.scores.getOrCreateRef("alice").increment(10)
|
|
15
16
|
draft.scores.getOrCreateRef("bob").increment(5)
|
|
16
17
|
})
|
|
@@ -20,7 +21,7 @@ describe("Record Types", () => {
|
|
|
20
21
|
bob: 5,
|
|
21
22
|
})
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
change(doc, draft => {
|
|
24
25
|
draft.scores.getOrCreateRef("alice").increment(5)
|
|
25
26
|
draft.scores.delete("bob")
|
|
26
27
|
})
|
|
@@ -35,9 +36,9 @@ describe("Record Types", () => {
|
|
|
35
36
|
notes: Shape.record(Shape.text()),
|
|
36
37
|
})
|
|
37
38
|
|
|
38
|
-
const doc =
|
|
39
|
+
const doc = createTypedDoc(schema)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
change(doc, draft => {
|
|
41
42
|
draft.notes.getOrCreateRef("todo").insert(0, "Buy milk")
|
|
42
43
|
draft.notes.getOrCreateRef("reminders").insert(0, "Call mom")
|
|
43
44
|
})
|
|
@@ -53,9 +54,9 @@ describe("Record Types", () => {
|
|
|
53
54
|
groups: Shape.record(Shape.list(Shape.plain.string())),
|
|
54
55
|
})
|
|
55
56
|
|
|
56
|
-
const doc =
|
|
57
|
+
const doc = createTypedDoc(schema)
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
change(doc, draft => {
|
|
59
60
|
const groupA = draft.groups.getOrCreateRef("groupA")
|
|
60
61
|
groupA.push("alice")
|
|
61
62
|
groupA.push("bob")
|
|
@@ -74,14 +75,14 @@ describe("Record Types", () => {
|
|
|
74
75
|
describe("Shape.plain.record (Value)", () => {
|
|
75
76
|
it("should handle record of plain strings", () => {
|
|
76
77
|
const schema = Shape.doc({
|
|
77
|
-
wrapper: Shape.
|
|
78
|
+
wrapper: Shape.struct({
|
|
78
79
|
config: Shape.plain.record(Shape.plain.string()),
|
|
79
80
|
}),
|
|
80
81
|
})
|
|
81
82
|
|
|
82
|
-
const doc =
|
|
83
|
+
const doc = createTypedDoc(schema)
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
change(doc, draft => {
|
|
85
86
|
draft.wrapper.config.theme = "dark"
|
|
86
87
|
draft.wrapper.config.lang = "en"
|
|
87
88
|
})
|
|
@@ -91,7 +92,7 @@ describe("Record Types", () => {
|
|
|
91
92
|
lang: "en",
|
|
92
93
|
})
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
change(doc, draft => {
|
|
95
96
|
delete draft.wrapper.config.theme
|
|
96
97
|
draft.wrapper.config.lang = "fr"
|
|
97
98
|
})
|
|
@@ -103,14 +104,14 @@ describe("Record Types", () => {
|
|
|
103
104
|
|
|
104
105
|
it("should handle record of plain numbers", () => {
|
|
105
106
|
const schema = Shape.doc({
|
|
106
|
-
wrapper: Shape.
|
|
107
|
+
wrapper: Shape.struct({
|
|
107
108
|
stats: Shape.plain.record(Shape.plain.number()),
|
|
108
109
|
}),
|
|
109
110
|
})
|
|
110
111
|
|
|
111
|
-
const doc =
|
|
112
|
+
const doc = createTypedDoc(schema)
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
change(doc, draft => {
|
|
114
115
|
draft.wrapper.stats.visits = 100
|
|
115
116
|
draft.wrapper.stats.clicks = 50
|
|
116
117
|
})
|
|
@@ -123,16 +124,16 @@ describe("Record Types", () => {
|
|
|
123
124
|
|
|
124
125
|
it("should handle nested records", () => {
|
|
125
126
|
const schema = Shape.doc({
|
|
126
|
-
wrapper: Shape.
|
|
127
|
+
wrapper: Shape.struct({
|
|
127
128
|
settings: Shape.plain.record(
|
|
128
129
|
Shape.plain.record(Shape.plain.boolean()),
|
|
129
130
|
),
|
|
130
131
|
}),
|
|
131
132
|
})
|
|
132
133
|
|
|
133
|
-
const doc =
|
|
134
|
+
const doc = createTypedDoc(schema)
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
change(doc, draft => {
|
|
136
137
|
draft.wrapper.settings.ui = {
|
|
137
138
|
darkMode: true,
|
|
138
139
|
sidebar: false,
|
|
@@ -160,16 +161,16 @@ describe("Record Types", () => {
|
|
|
160
161
|
it("should handle record of maps", () => {
|
|
161
162
|
const schema = Shape.doc({
|
|
162
163
|
users: Shape.record(
|
|
163
|
-
Shape.
|
|
164
|
+
Shape.struct({
|
|
164
165
|
name: Shape.plain.string(),
|
|
165
166
|
age: Shape.plain.number(),
|
|
166
167
|
}),
|
|
167
168
|
),
|
|
168
169
|
})
|
|
169
170
|
|
|
170
|
-
const doc =
|
|
171
|
+
const doc = createTypedDoc(schema)
|
|
171
172
|
|
|
172
|
-
|
|
173
|
+
change(doc, draft => {
|
|
173
174
|
const alice = draft.users.getOrCreateRef("u1")
|
|
174
175
|
alice.name = "Alice"
|
|
175
176
|
alice.age = 30
|
|
@@ -188,7 +189,7 @@ describe("Record Types", () => {
|
|
|
188
189
|
it("should allow setting a plain object for a record with map values", () => {
|
|
189
190
|
const schema = Shape.doc({
|
|
190
191
|
participants: Shape.record(
|
|
191
|
-
Shape.
|
|
192
|
+
Shape.struct({
|
|
192
193
|
id: Shape.plain.string(),
|
|
193
194
|
role: Shape.plain.string(),
|
|
194
195
|
name: Shape.plain.string(),
|
|
@@ -197,9 +198,9 @@ describe("Record Types", () => {
|
|
|
197
198
|
),
|
|
198
199
|
})
|
|
199
200
|
|
|
200
|
-
const doc =
|
|
201
|
+
const doc = createTypedDoc(schema)
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
change(doc, draft => {
|
|
203
204
|
draft.participants["student-1"] = {
|
|
204
205
|
id: "student-1",
|
|
205
206
|
role: "student",
|
|
@@ -219,17 +220,17 @@ describe("Record Types", () => {
|
|
|
219
220
|
it("should allow setting a plain object for a record with nested map values", () => {
|
|
220
221
|
const schema = Shape.doc({
|
|
221
222
|
data: Shape.record(
|
|
222
|
-
Shape.
|
|
223
|
-
info: Shape.
|
|
223
|
+
Shape.struct({
|
|
224
|
+
info: Shape.struct({
|
|
224
225
|
name: Shape.plain.string(),
|
|
225
226
|
}),
|
|
226
227
|
}),
|
|
227
228
|
),
|
|
228
229
|
})
|
|
229
230
|
|
|
230
|
-
const doc =
|
|
231
|
+
const doc = createTypedDoc(schema)
|
|
231
232
|
|
|
232
|
-
|
|
233
|
+
change(doc, draft => {
|
|
233
234
|
draft.data["item-1"] = {
|
|
234
235
|
info: {
|
|
235
236
|
name: "Item 1",
|
|
@@ -249,15 +250,15 @@ describe("Record Types", () => {
|
|
|
249
250
|
histories: Shape.record(Shape.list(Shape.plain.string())),
|
|
250
251
|
})
|
|
251
252
|
|
|
252
|
-
const doc =
|
|
253
|
+
const doc = createTypedDoc(schema)
|
|
253
254
|
|
|
254
|
-
|
|
255
|
+
change(doc, draft => {
|
|
255
256
|
draft.histories.user1 = ["a", "b"]
|
|
256
257
|
})
|
|
257
258
|
|
|
258
259
|
expect(doc.toJSON().histories.user1).toEqual(["a", "b"])
|
|
259
260
|
|
|
260
|
-
|
|
261
|
+
change(doc, draft => {
|
|
261
262
|
// biome-ignore lint/complexity/useLiteralKeys: tests indexed assignment
|
|
262
263
|
draft.histories["user1"] = ["c"]
|
|
263
264
|
})
|
|
@@ -265,6 +266,70 @@ describe("Record Types", () => {
|
|
|
265
266
|
// biome-ignore lint/complexity/useLiteralKeys: tests indexed assignment
|
|
266
267
|
expect(doc.toJSON().histories["user1"]).toEqual(["c"])
|
|
267
268
|
})
|
|
269
|
+
|
|
270
|
+
it("should allow setting a plain string for a record of text", () => {
|
|
271
|
+
const schema = Shape.doc({
|
|
272
|
+
notes: Shape.record(Shape.text()),
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const doc = createTypedDoc(schema)
|
|
276
|
+
|
|
277
|
+
change(doc, draft => {
|
|
278
|
+
draft.notes.set("note-1", "Hello World")
|
|
279
|
+
draft.notes["note-2"] = "Another note"
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
expect(doc.toJSON().notes).toEqual({
|
|
283
|
+
"note-1": "Hello World",
|
|
284
|
+
"note-2": "Another note",
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it("should allow setting a plain number for a record of counter", () => {
|
|
289
|
+
const schema = Shape.doc({
|
|
290
|
+
scores: Shape.record(Shape.counter()),
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const doc = createTypedDoc(schema)
|
|
294
|
+
|
|
295
|
+
change(doc, draft => {
|
|
296
|
+
draft.scores.set("alice", 100)
|
|
297
|
+
draft.scores.bob = 50
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
expect(doc.toJSON().scores).toEqual({
|
|
301
|
+
alice: 100,
|
|
302
|
+
bob: 50,
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it("should allow setting a plain object with text fields for a record of maps", () => {
|
|
307
|
+
const schema = Shape.doc({
|
|
308
|
+
users: Shape.record(
|
|
309
|
+
Shape.struct({
|
|
310
|
+
userId: Shape.plain.string(),
|
|
311
|
+
displayName: Shape.text(),
|
|
312
|
+
email: Shape.plain.string(),
|
|
313
|
+
}),
|
|
314
|
+
),
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const doc = createTypedDoc(schema)
|
|
318
|
+
|
|
319
|
+
change(doc, draft => {
|
|
320
|
+
draft.users.set("user-123", {
|
|
321
|
+
userId: "user-123",
|
|
322
|
+
displayName: "Test User",
|
|
323
|
+
email: "test@example.com",
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
expect(doc.toJSON().users["user-123"]).toEqual({
|
|
328
|
+
userId: "user-123",
|
|
329
|
+
displayName: "Test User",
|
|
330
|
+
email: "test@example.com",
|
|
331
|
+
})
|
|
332
|
+
})
|
|
268
333
|
})
|
|
269
334
|
|
|
270
335
|
describe("Readonly access to non-existent keys", () => {
|
|
@@ -273,26 +338,26 @@ describe("Record Types", () => {
|
|
|
273
338
|
// preferences: Record<string, { showTip: boolean }>
|
|
274
339
|
const schema = Shape.doc({
|
|
275
340
|
preferences: Shape.record(
|
|
276
|
-
Shape.
|
|
341
|
+
Shape.struct({
|
|
277
342
|
showTip: Shape.plain.boolean(),
|
|
278
343
|
}),
|
|
279
344
|
),
|
|
280
345
|
})
|
|
281
346
|
|
|
282
|
-
const doc =
|
|
347
|
+
const doc = createTypedDoc(schema)
|
|
283
348
|
|
|
284
349
|
// First, set a value for a specific peer
|
|
285
|
-
|
|
350
|
+
change(doc, d => {
|
|
286
351
|
d.preferences.peer1 = { showTip: true }
|
|
287
352
|
})
|
|
288
353
|
|
|
289
354
|
// This should work - accessing an existing key
|
|
290
|
-
expect(doc.
|
|
355
|
+
expect(doc.preferences.peer1?.showTip).toBe(true)
|
|
291
356
|
|
|
292
357
|
// Accessing a non-existent key should NOT throw "placeholder required"
|
|
293
358
|
// It should return undefined so optional chaining works correctly
|
|
294
359
|
expect(() => {
|
|
295
|
-
const result = doc.
|
|
360
|
+
const result = doc.preferences.nonexistent?.showTip
|
|
296
361
|
return result
|
|
297
362
|
}).not.toThrow()
|
|
298
363
|
})
|
|
@@ -300,16 +365,16 @@ describe("Record Types", () => {
|
|
|
300
365
|
it("should return undefined for non-existent record keys in readonly mode", () => {
|
|
301
366
|
const schema = Shape.doc({
|
|
302
367
|
preferences: Shape.record(
|
|
303
|
-
Shape.
|
|
368
|
+
Shape.struct({
|
|
304
369
|
showTip: Shape.plain.boolean(),
|
|
305
370
|
}),
|
|
306
371
|
),
|
|
307
372
|
})
|
|
308
373
|
|
|
309
|
-
const doc =
|
|
374
|
+
const doc = createTypedDoc(schema)
|
|
310
375
|
|
|
311
376
|
// Access a key that doesn't exist - should return undefined
|
|
312
|
-
const prefs = doc.
|
|
377
|
+
const prefs = doc.preferences.nonexistent
|
|
313
378
|
expect(prefs).toBeUndefined()
|
|
314
379
|
})
|
|
315
380
|
|
|
@@ -317,19 +382,19 @@ describe("Record Types", () => {
|
|
|
317
382
|
// Exact reproduction of a user's schema and access pattern
|
|
318
383
|
const schema = Shape.doc({
|
|
319
384
|
preferences: Shape.record(
|
|
320
|
-
Shape.
|
|
385
|
+
Shape.struct({
|
|
321
386
|
showTip: Shape.plain.boolean(),
|
|
322
387
|
}),
|
|
323
388
|
),
|
|
324
389
|
})
|
|
325
390
|
|
|
326
|
-
const doc =
|
|
391
|
+
const doc = createTypedDoc(schema)
|
|
327
392
|
const myPeerId = "some-peer-id"
|
|
328
393
|
|
|
329
394
|
// This is the exact code pattern from the user's app:
|
|
330
395
|
// doc.preferences[myPeerId]?.showTip !== false
|
|
331
396
|
expect(() => {
|
|
332
|
-
const showTip = doc.
|
|
397
|
+
const showTip = doc.preferences[myPeerId]?.showTip
|
|
333
398
|
const result = showTip !== false
|
|
334
399
|
return result
|
|
335
400
|
}).not.toThrow()
|
package/src/typed-refs/record.ts
CHANGED
|
@@ -59,20 +59,34 @@ export class RecordRef<
|
|
|
59
59
|
getContainer: () =>
|
|
60
60
|
this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
|
|
61
61
|
readonly: this.readonly,
|
|
62
|
+
autoCommit: this._params.autoCommit,
|
|
63
|
+
getDoc: this._params.getDoc,
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Gets an existing ref for a key, or returns undefined if the key doesn't exist.
|
|
69
|
+
* Used for reading operations where we want optional chaining to work.
|
|
70
|
+
*/
|
|
71
|
+
getRef(key: string): any {
|
|
72
|
+
// For container shapes, check if the key exists first
|
|
67
73
|
// This allows optional chaining (?.) to work correctly for non-existent keys
|
|
68
|
-
|
|
69
|
-
if (this.readonly && isContainerShape(this.shape.shape)) {
|
|
74
|
+
if (isContainerShape(this.shape.shape)) {
|
|
70
75
|
const existing = this.container.get(key)
|
|
71
76
|
if (existing === undefined) {
|
|
72
77
|
return undefined
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
return this.getOrCreateRef(key)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets or creates a ref for a key.
|
|
86
|
+
* Always creates the container if it doesn't exist.
|
|
87
|
+
* This is the method used for write operations.
|
|
88
|
+
*/
|
|
89
|
+
getOrCreateRef(key: string): any {
|
|
76
90
|
let ref = this.refCache.get(key)
|
|
77
91
|
if (!ref) {
|
|
78
92
|
const shape = this.shape.shape
|
|
@@ -116,7 +130,7 @@ export class RecordRef<
|
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
get(key: string): InferMutableType<NestedShape> {
|
|
119
|
-
return this.
|
|
133
|
+
return this.getRef(key)
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
set(key: string, value: any): void {
|
|
@@ -124,17 +138,15 @@ export class RecordRef<
|
|
|
124
138
|
if (isValueShape(this.shape.shape)) {
|
|
125
139
|
this.container.set(key, value)
|
|
126
140
|
this.refCache.set(key, value)
|
|
141
|
+
this.commitIfAuto()
|
|
127
142
|
} else {
|
|
128
|
-
// For
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
}
|
|
143
|
+
// For container shapes, try to assign the plain value
|
|
144
|
+
// Use getOrCreateRef to ensure the container is created
|
|
145
|
+
const ref = this.getOrCreateRef(key)
|
|
146
|
+
if (assignPlainValueToTypedRef(ref, value)) {
|
|
147
|
+
this.commitIfAuto()
|
|
148
|
+
return
|
|
136
149
|
}
|
|
137
|
-
|
|
138
150
|
throw new Error(
|
|
139
151
|
"Cannot set container directly, modify the typed ref instead",
|
|
140
152
|
)
|
|
@@ -143,13 +155,16 @@ export class RecordRef<
|
|
|
143
155
|
|
|
144
156
|
setContainer<C extends Container>(key: string, container: C): C {
|
|
145
157
|
this.assertMutable()
|
|
146
|
-
|
|
158
|
+
const result = this.container.setContainer(key, container)
|
|
159
|
+
this.commitIfAuto()
|
|
160
|
+
return result
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
delete(key: string): void {
|
|
150
164
|
this.assertMutable()
|
|
151
165
|
this.container.delete(key)
|
|
152
166
|
this.refCache.delete(key)
|
|
167
|
+
this.commitIfAuto()
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
has(key: string): boolean {
|
|
@@ -168,12 +183,12 @@ export class RecordRef<
|
|
|
168
183
|
return this.container.size
|
|
169
184
|
}
|
|
170
185
|
|
|
171
|
-
toJSON(): Record<string,
|
|
186
|
+
toJSON(): Record<string, Infer<NestedShape>> {
|
|
172
187
|
// Fast path: readonly mode
|
|
173
188
|
if (this.readonly) {
|
|
174
189
|
const nativeJson = this.container.toJSON() as Record<string, any>
|
|
175
190
|
// For records, we need to overlay placeholders for each entry's nested shape
|
|
176
|
-
const result: Record<string,
|
|
191
|
+
const result: Record<string, Infer<NestedShape>> = {}
|
|
177
192
|
for (const key of Object.keys(nativeJson)) {
|
|
178
193
|
// For records, the placeholder is always {}, so we need to derive
|
|
179
194
|
// the placeholder for the nested shape on the fly
|
|
@@ -183,11 +198,14 @@ export class RecordRef<
|
|
|
183
198
|
this.shape.shape,
|
|
184
199
|
nativeJson[key],
|
|
185
200
|
nestedPlaceholderValue as Value,
|
|
186
|
-
)
|
|
201
|
+
) as Infer<NestedShape>
|
|
187
202
|
}
|
|
188
203
|
return result
|
|
189
204
|
}
|
|
190
205
|
|
|
191
|
-
return serializeRefToJSON(this, this.keys())
|
|
206
|
+
return serializeRefToJSON(this, this.keys()) as Record<
|
|
207
|
+
string,
|
|
208
|
+
Infer<NestedShape>
|
|
209
|
+
>
|
|
192
210
|
}
|
|
193
211
|
}
|
|
@@ -3,9 +3,10 @@ import { mergeValue } from "../overlay.js"
|
|
|
3
3
|
import type {
|
|
4
4
|
ContainerOrValueShape,
|
|
5
5
|
ContainerShape,
|
|
6
|
-
|
|
6
|
+
StructContainerShape,
|
|
7
7
|
ValueShape,
|
|
8
8
|
} from "../shape.js"
|
|
9
|
+
import type { Infer } from "../types.js"
|
|
9
10
|
import { isContainerShape, isValueShape } from "../utils/type-guards.js"
|
|
10
11
|
import { TypedRef, type TypedRefParams } from "./base.js"
|
|
11
12
|
import {
|
|
@@ -17,19 +18,22 @@ import {
|
|
|
17
18
|
unwrapReadonlyPrimitive,
|
|
18
19
|
} from "./utils.js"
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Typed ref for struct containers (objects with fixed keys).
|
|
23
|
+
* Uses LoroMap as the underlying container.
|
|
24
|
+
*/
|
|
25
|
+
export class StructRef<
|
|
22
26
|
NestedShapes extends Record<string, ContainerOrValueShape>,
|
|
23
27
|
> extends TypedRef<any> {
|
|
24
28
|
private propertyCache = new Map<string, TypedRef<ContainerShape> | Value>()
|
|
25
29
|
|
|
26
|
-
constructor(params: TypedRefParams<
|
|
30
|
+
constructor(params: TypedRefParams<StructContainerShape<NestedShapes>>) {
|
|
27
31
|
super(params)
|
|
28
32
|
this.createLazyProperties()
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
protected get shape():
|
|
32
|
-
return super.shape as
|
|
35
|
+
protected get shape(): StructContainerShape<NestedShapes> {
|
|
36
|
+
return super.shape as StructContainerShape<NestedShapes>
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
protected get container(): LoroMap {
|
|
@@ -54,6 +58,8 @@ export class MapRef<
|
|
|
54
58
|
getContainer: () =>
|
|
55
59
|
this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
|
|
56
60
|
readonly: this.readonly,
|
|
61
|
+
autoCommit: this._params.autoCommit,
|
|
62
|
+
getDoc: this._params.getDoc,
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -115,12 +121,10 @@ export class MapRef<
|
|
|
115
121
|
this.container.set(key, value)
|
|
116
122
|
this.propertyCache.set(key, value)
|
|
117
123
|
} else {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
+
// For container shapes, try to assign the plain value
|
|
125
|
+
const ref = this.getOrCreateRef(key, shape)
|
|
126
|
+
if (assignPlainValueToTypedRef(ref as TypedRef<any>, value)) {
|
|
127
|
+
return
|
|
124
128
|
}
|
|
125
129
|
throw new Error(
|
|
126
130
|
"Cannot set container directly, modify the typed ref instead",
|
|
@@ -132,15 +136,22 @@ export class MapRef<
|
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
toJSON():
|
|
139
|
+
toJSON(): Infer<StructContainerShape<NestedShapes>> {
|
|
136
140
|
// Fast path: readonly mode
|
|
137
141
|
if (this.readonly) {
|
|
138
142
|
const nativeJson = this.container.toJSON() as Value
|
|
139
143
|
// Overlay placeholders for missing properties
|
|
140
|
-
return mergeValue(
|
|
144
|
+
return mergeValue(
|
|
145
|
+
this.shape,
|
|
146
|
+
nativeJson,
|
|
147
|
+
this.placeholder as Value,
|
|
148
|
+
) as Infer<StructContainerShape<NestedShapes>>
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
return serializeRefToJSON(
|
|
151
|
+
return serializeRefToJSON(
|
|
152
|
+
this as any,
|
|
153
|
+
Object.keys(this.shape.shapes),
|
|
154
|
+
) as Infer<StructContainerShape<NestedShapes>>
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
// TODO(duane): return correct type here
|
|
@@ -151,16 +162,20 @@ export class MapRef<
|
|
|
151
162
|
set(key: string, value: Value): void {
|
|
152
163
|
this.assertMutable()
|
|
153
164
|
this.container.set(key, value)
|
|
165
|
+
this.commitIfAuto()
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
setContainer<C extends Container>(key: string, container: C): C {
|
|
157
169
|
this.assertMutable()
|
|
158
|
-
|
|
170
|
+
const result = this.container.setContainer(key, container)
|
|
171
|
+
this.commitIfAuto()
|
|
172
|
+
return result
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
delete(key: string): void {
|
|
162
176
|
this.assertMutable()
|
|
163
177
|
this.container.delete(key)
|
|
178
|
+
this.commitIfAuto()
|
|
164
179
|
}
|
|
165
180
|
|
|
166
181
|
has(key: string): boolean {
|