@loro-extended/change 0.7.0 → 0.8.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 +26 -31
- package/dist/index.d.ts +86 -54
- package/dist/index.js +723 -400
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +97 -403
- package/src/derive-placeholder.test.ts +245 -0
- package/src/derive-placeholder.ts +132 -0
- package/src/discriminated-union.test.ts +5 -9
- package/src/draft-nodes/base.ts +9 -4
- package/src/draft-nodes/counter.md +31 -0
- package/src/draft-nodes/counter.test.ts +53 -0
- package/src/draft-nodes/doc.ts +27 -6
- package/src/draft-nodes/list-base.ts +24 -3
- package/src/draft-nodes/list.test.ts +43 -0
- package/src/draft-nodes/list.ts +3 -0
- package/src/draft-nodes/map.ts +57 -16
- package/src/draft-nodes/movable-list.test.ts +27 -0
- package/src/draft-nodes/movable-list.ts +5 -0
- package/src/draft-nodes/proxy-handlers.ts +87 -0
- package/src/{record.test.ts → draft-nodes/record.test.ts} +98 -18
- package/src/draft-nodes/record.ts +42 -80
- package/src/draft-nodes/utils.ts +46 -5
- package/src/equality.test.ts +19 -0
- package/src/index.ts +12 -6
- package/src/json-patch.test.ts +33 -167
- package/src/overlay.ts +45 -44
- package/src/readonly.test.ts +92 -0
- package/src/shape.ts +132 -77
- package/src/{change.ts → typed-doc.ts} +50 -28
- package/src/types.test.ts +26 -7
- package/src/types.ts +7 -7
- package/src/validation.ts +12 -12
package/dist/index.js
CHANGED
|
@@ -1,4 +1,446 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/derive-placeholder.ts
|
|
2
|
+
function derivePlaceholder(schema) {
|
|
3
|
+
const result = {};
|
|
4
|
+
for (const [key, shape] of Object.entries(schema.shapes)) {
|
|
5
|
+
result[key] = deriveShapePlaceholder(shape);
|
|
6
|
+
}
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
function deriveShapePlaceholder(shape) {
|
|
10
|
+
switch (shape._type) {
|
|
11
|
+
// Leaf containers - use _placeholder directly
|
|
12
|
+
case "text":
|
|
13
|
+
return shape._placeholder;
|
|
14
|
+
case "counter":
|
|
15
|
+
return shape._placeholder;
|
|
16
|
+
// Dynamic containers - always empty (no per-entry merging)
|
|
17
|
+
case "list":
|
|
18
|
+
case "movableList":
|
|
19
|
+
case "tree":
|
|
20
|
+
return [];
|
|
21
|
+
case "record":
|
|
22
|
+
return {};
|
|
23
|
+
// Structured container - recurse into nested shapes
|
|
24
|
+
case "map": {
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [key, nestedShape] of Object.entries(shape.shapes)) {
|
|
27
|
+
result[key] = deriveShapePlaceholder(nestedShape);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
case "value":
|
|
32
|
+
return deriveValueShapePlaceholder(shape);
|
|
33
|
+
default:
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function deriveValueShapePlaceholder(shape) {
|
|
38
|
+
switch (shape.valueType) {
|
|
39
|
+
// Leaf values - use _placeholder directly
|
|
40
|
+
case "string":
|
|
41
|
+
return shape._placeholder;
|
|
42
|
+
case "number":
|
|
43
|
+
return shape._placeholder;
|
|
44
|
+
case "boolean":
|
|
45
|
+
return shape._placeholder;
|
|
46
|
+
case "null":
|
|
47
|
+
return null;
|
|
48
|
+
case "undefined":
|
|
49
|
+
return void 0;
|
|
50
|
+
case "uint8array":
|
|
51
|
+
return shape._placeholder;
|
|
52
|
+
// Structured value - recurse into nested shapes (like map)
|
|
53
|
+
case "object": {
|
|
54
|
+
const result = {};
|
|
55
|
+
for (const [key, nestedShape] of Object.entries(shape.shape)) {
|
|
56
|
+
result[key] = deriveValueShapePlaceholder(nestedShape);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
// Dynamic values - always empty
|
|
61
|
+
case "array":
|
|
62
|
+
return [];
|
|
63
|
+
case "record":
|
|
64
|
+
return {};
|
|
65
|
+
// Unions - use _placeholder if explicitly set, otherwise derive from first variant
|
|
66
|
+
case "union": {
|
|
67
|
+
const placeholder = shape._placeholder;
|
|
68
|
+
if (placeholder !== void 0) {
|
|
69
|
+
if (placeholder === null || typeof placeholder !== "object") {
|
|
70
|
+
return placeholder;
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(placeholder).length > 0) {
|
|
73
|
+
return placeholder;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return deriveValueShapePlaceholder(shape.shapes[0]);
|
|
77
|
+
}
|
|
78
|
+
case "discriminatedUnion": {
|
|
79
|
+
const placeholder = shape._placeholder;
|
|
80
|
+
if (placeholder !== void 0) {
|
|
81
|
+
if (placeholder === null || typeof placeholder !== "object") {
|
|
82
|
+
return placeholder;
|
|
83
|
+
}
|
|
84
|
+
if (Object.keys(placeholder).length > 0) {
|
|
85
|
+
return placeholder;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const firstKey = Object.keys(shape.variants)[0];
|
|
89
|
+
return deriveValueShapePlaceholder(shape.variants[firstKey]);
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/utils/type-guards.ts
|
|
97
|
+
import { isContainer, isContainerId } from "loro-crdt";
|
|
98
|
+
function isContainerShape(schema) {
|
|
99
|
+
return schema._type && schema._type !== "value";
|
|
100
|
+
}
|
|
101
|
+
function isValueShape(schema) {
|
|
102
|
+
return schema._type === "value" && [
|
|
103
|
+
"string",
|
|
104
|
+
"number",
|
|
105
|
+
"boolean",
|
|
106
|
+
"null",
|
|
107
|
+
"undefined",
|
|
108
|
+
"uint8array",
|
|
109
|
+
"object",
|
|
110
|
+
"record",
|
|
111
|
+
"array",
|
|
112
|
+
"union",
|
|
113
|
+
"discriminatedUnion"
|
|
114
|
+
].includes(schema.valueType);
|
|
115
|
+
}
|
|
116
|
+
function isObjectValue(value) {
|
|
117
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Uint8Array);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/overlay.ts
|
|
121
|
+
function overlayPlaceholder(shape, crdtValue, placeholderValue) {
|
|
122
|
+
if (typeof crdtValue !== "object") {
|
|
123
|
+
throw new Error("crdt object is required");
|
|
124
|
+
}
|
|
125
|
+
if (typeof placeholderValue !== "object") {
|
|
126
|
+
throw new Error("placeholder object is required");
|
|
127
|
+
}
|
|
128
|
+
const result = { ...placeholderValue };
|
|
129
|
+
for (const [key, propShape] of Object.entries(shape.shapes)) {
|
|
130
|
+
const propCrdtValue = crdtValue[key];
|
|
131
|
+
const propPlaceholderValue = placeholderValue[key];
|
|
132
|
+
result[key] = mergeValue(
|
|
133
|
+
propShape,
|
|
134
|
+
propCrdtValue,
|
|
135
|
+
propPlaceholderValue
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
function mergeValue(shape, crdtValue, placeholderValue) {
|
|
141
|
+
if (crdtValue === void 0 && placeholderValue === void 0) {
|
|
142
|
+
throw new Error("either crdt or placeholder value must be defined");
|
|
143
|
+
}
|
|
144
|
+
switch (shape._type) {
|
|
145
|
+
case "text":
|
|
146
|
+
return crdtValue ?? placeholderValue ?? "";
|
|
147
|
+
case "counter":
|
|
148
|
+
return crdtValue ?? placeholderValue ?? 0;
|
|
149
|
+
case "list":
|
|
150
|
+
case "movableList":
|
|
151
|
+
return crdtValue ?? placeholderValue ?? [];
|
|
152
|
+
case "map": {
|
|
153
|
+
if (!isObjectValue(crdtValue) && crdtValue !== void 0) {
|
|
154
|
+
throw new Error("map crdt must be object");
|
|
155
|
+
}
|
|
156
|
+
const crdtMapValue = crdtValue ?? {};
|
|
157
|
+
if (!isObjectValue(placeholderValue) && placeholderValue !== void 0) {
|
|
158
|
+
throw new Error("map placeholder must be object");
|
|
159
|
+
}
|
|
160
|
+
const placeholderMapValue = placeholderValue ?? {};
|
|
161
|
+
const result = { ...placeholderMapValue };
|
|
162
|
+
for (const [key, nestedShape] of Object.entries(shape.shapes)) {
|
|
163
|
+
const nestedCrdtValue = crdtMapValue[key];
|
|
164
|
+
const nestedPlaceholderValue = placeholderMapValue[key];
|
|
165
|
+
result[key] = mergeValue(
|
|
166
|
+
nestedShape,
|
|
167
|
+
nestedCrdtValue,
|
|
168
|
+
nestedPlaceholderValue
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
case "tree":
|
|
174
|
+
return crdtValue ?? placeholderValue ?? [];
|
|
175
|
+
default:
|
|
176
|
+
if (shape._type === "value" && shape.valueType === "object") {
|
|
177
|
+
const crdtObj = crdtValue ?? {};
|
|
178
|
+
const placeholderObj = placeholderValue ?? {};
|
|
179
|
+
const result = { ...placeholderObj };
|
|
180
|
+
if (typeof crdtObj !== "object" || crdtObj === null) {
|
|
181
|
+
return crdtValue ?? placeholderValue;
|
|
182
|
+
}
|
|
183
|
+
for (const [key, propShape] of Object.entries(shape.shape)) {
|
|
184
|
+
const propCrdt = crdtObj[key];
|
|
185
|
+
const propPlaceholder = placeholderObj[key];
|
|
186
|
+
result[key] = mergeValue(propShape, propCrdt, propPlaceholder);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
if (shape._type === "value" && shape.valueType === "discriminatedUnion") {
|
|
191
|
+
return mergeDiscriminatedUnion(
|
|
192
|
+
shape,
|
|
193
|
+
crdtValue,
|
|
194
|
+
placeholderValue
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return crdtValue ?? placeholderValue;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function mergeDiscriminatedUnion(shape, crdtValue, placeholderValue) {
|
|
201
|
+
const crdtObj = crdtValue ?? {};
|
|
202
|
+
const placeholderObj = placeholderValue ?? {};
|
|
203
|
+
const discriminantValue = crdtObj[shape.discriminantKey] ?? placeholderObj[shape.discriminantKey];
|
|
204
|
+
if (typeof discriminantValue !== "string") {
|
|
205
|
+
return placeholderValue;
|
|
206
|
+
}
|
|
207
|
+
const variantShape = shape.variants[discriminantValue];
|
|
208
|
+
if (!variantShape) {
|
|
209
|
+
return crdtValue ?? placeholderValue;
|
|
210
|
+
}
|
|
211
|
+
const placeholderDiscriminant = placeholderObj[shape.discriminantKey];
|
|
212
|
+
const effectivePlaceholderValue = placeholderDiscriminant === discriminantValue ? placeholderValue : void 0;
|
|
213
|
+
return mergeValue(variantShape, crdtValue, effectivePlaceholderValue);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/shape.ts
|
|
217
|
+
var Shape = {
|
|
218
|
+
doc: (shape) => ({
|
|
219
|
+
_type: "doc",
|
|
220
|
+
shapes: shape,
|
|
221
|
+
_plain: {},
|
|
222
|
+
_draft: {},
|
|
223
|
+
_placeholder: {}
|
|
224
|
+
}),
|
|
225
|
+
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
226
|
+
// various CRDT algorithms
|
|
227
|
+
counter: () => {
|
|
228
|
+
const base = {
|
|
229
|
+
_type: "counter",
|
|
230
|
+
_plain: 0,
|
|
231
|
+
_draft: {},
|
|
232
|
+
_placeholder: 0
|
|
233
|
+
};
|
|
234
|
+
return Object.assign(base, {
|
|
235
|
+
placeholder(value) {
|
|
236
|
+
return { ...base, _placeholder: value };
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
list: (shape) => ({
|
|
241
|
+
_type: "list",
|
|
242
|
+
shape,
|
|
243
|
+
_plain: [],
|
|
244
|
+
_draft: {},
|
|
245
|
+
_placeholder: []
|
|
246
|
+
}),
|
|
247
|
+
map: (shape) => ({
|
|
248
|
+
_type: "map",
|
|
249
|
+
shapes: shape,
|
|
250
|
+
_plain: {},
|
|
251
|
+
_draft: {},
|
|
252
|
+
_placeholder: {}
|
|
253
|
+
}),
|
|
254
|
+
record: (shape) => ({
|
|
255
|
+
_type: "record",
|
|
256
|
+
shape,
|
|
257
|
+
_plain: {},
|
|
258
|
+
_draft: {},
|
|
259
|
+
_placeholder: {}
|
|
260
|
+
}),
|
|
261
|
+
movableList: (shape) => ({
|
|
262
|
+
_type: "movableList",
|
|
263
|
+
shape,
|
|
264
|
+
_plain: [],
|
|
265
|
+
_draft: {},
|
|
266
|
+
_placeholder: []
|
|
267
|
+
}),
|
|
268
|
+
text: () => {
|
|
269
|
+
const base = {
|
|
270
|
+
_type: "text",
|
|
271
|
+
_plain: "",
|
|
272
|
+
_draft: {},
|
|
273
|
+
_placeholder: ""
|
|
274
|
+
};
|
|
275
|
+
return Object.assign(base, {
|
|
276
|
+
placeholder(value) {
|
|
277
|
+
return { ...base, _placeholder: value };
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
tree: (shape) => ({
|
|
282
|
+
_type: "tree",
|
|
283
|
+
shape,
|
|
284
|
+
_plain: {},
|
|
285
|
+
_draft: {},
|
|
286
|
+
_placeholder: []
|
|
287
|
+
}),
|
|
288
|
+
// Values are represented as plain JS objects, with the limitation that they MUST be
|
|
289
|
+
// representable as a Loro "Value"--basically JSON. The behavior of a Value is basically
|
|
290
|
+
// "Last Write Wins", meaning there is no subtle convergent behavior here, just taking
|
|
291
|
+
// the most recent value based on the current available information.
|
|
292
|
+
plain: {
|
|
293
|
+
string: (...options) => {
|
|
294
|
+
const base = {
|
|
295
|
+
_type: "value",
|
|
296
|
+
valueType: "string",
|
|
297
|
+
_plain: options[0] ?? "",
|
|
298
|
+
_draft: options[0] ?? "",
|
|
299
|
+
_placeholder: options[0] ?? "",
|
|
300
|
+
options: options.length > 0 ? options : void 0
|
|
301
|
+
};
|
|
302
|
+
return Object.assign(base, {
|
|
303
|
+
placeholder(value) {
|
|
304
|
+
return { ...base, _placeholder: value };
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
number: () => {
|
|
309
|
+
const base = {
|
|
310
|
+
_type: "value",
|
|
311
|
+
valueType: "number",
|
|
312
|
+
_plain: 0,
|
|
313
|
+
_draft: 0,
|
|
314
|
+
_placeholder: 0
|
|
315
|
+
};
|
|
316
|
+
return Object.assign(base, {
|
|
317
|
+
placeholder(value) {
|
|
318
|
+
return { ...base, _placeholder: value };
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
boolean: () => {
|
|
323
|
+
const base = {
|
|
324
|
+
_type: "value",
|
|
325
|
+
valueType: "boolean",
|
|
326
|
+
_plain: false,
|
|
327
|
+
_draft: false,
|
|
328
|
+
_placeholder: false
|
|
329
|
+
};
|
|
330
|
+
return Object.assign(base, {
|
|
331
|
+
placeholder(value) {
|
|
332
|
+
return { ...base, _placeholder: value };
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
null: () => ({
|
|
337
|
+
_type: "value",
|
|
338
|
+
valueType: "null",
|
|
339
|
+
_plain: null,
|
|
340
|
+
_draft: null,
|
|
341
|
+
_placeholder: null
|
|
342
|
+
}),
|
|
343
|
+
undefined: () => ({
|
|
344
|
+
_type: "value",
|
|
345
|
+
valueType: "undefined",
|
|
346
|
+
_plain: void 0,
|
|
347
|
+
_draft: void 0,
|
|
348
|
+
_placeholder: void 0
|
|
349
|
+
}),
|
|
350
|
+
uint8Array: () => ({
|
|
351
|
+
_type: "value",
|
|
352
|
+
valueType: "uint8array",
|
|
353
|
+
_plain: new Uint8Array(),
|
|
354
|
+
_draft: new Uint8Array(),
|
|
355
|
+
_placeholder: new Uint8Array()
|
|
356
|
+
}),
|
|
357
|
+
object: (shape) => ({
|
|
358
|
+
_type: "value",
|
|
359
|
+
valueType: "object",
|
|
360
|
+
shape,
|
|
361
|
+
_plain: {},
|
|
362
|
+
_draft: {},
|
|
363
|
+
_placeholder: {}
|
|
364
|
+
}),
|
|
365
|
+
record: (shape) => ({
|
|
366
|
+
_type: "value",
|
|
367
|
+
valueType: "record",
|
|
368
|
+
shape,
|
|
369
|
+
_plain: {},
|
|
370
|
+
_draft: {},
|
|
371
|
+
_placeholder: {}
|
|
372
|
+
}),
|
|
373
|
+
array: (shape) => ({
|
|
374
|
+
_type: "value",
|
|
375
|
+
valueType: "array",
|
|
376
|
+
shape,
|
|
377
|
+
_plain: [],
|
|
378
|
+
_draft: [],
|
|
379
|
+
_placeholder: []
|
|
380
|
+
}),
|
|
381
|
+
// Special value type that helps make things like `string | null` representable
|
|
382
|
+
// TODO(duane): should this be a more general type for containers too?
|
|
383
|
+
union: (shapes) => {
|
|
384
|
+
const base = {
|
|
385
|
+
_type: "value",
|
|
386
|
+
valueType: "union",
|
|
387
|
+
shapes,
|
|
388
|
+
_plain: {},
|
|
389
|
+
_draft: {},
|
|
390
|
+
_placeholder: {}
|
|
391
|
+
};
|
|
392
|
+
return Object.assign(base, {
|
|
393
|
+
placeholder(value) {
|
|
394
|
+
return { ...base, _placeholder: value };
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
/**
|
|
399
|
+
* Creates a discriminated union shape for type-safe tagged unions.
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```typescript
|
|
403
|
+
* const ClientPresenceShape = Shape.plain.object({
|
|
404
|
+
* type: Shape.plain.string("client"),
|
|
405
|
+
* name: Shape.plain.string(),
|
|
406
|
+
* input: Shape.plain.object({ force: Shape.plain.number(), angle: Shape.plain.number() }),
|
|
407
|
+
* })
|
|
408
|
+
*
|
|
409
|
+
* const ServerPresenceShape = Shape.plain.object({
|
|
410
|
+
* type: Shape.plain.string("server"),
|
|
411
|
+
* cars: Shape.plain.record(Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() })),
|
|
412
|
+
* tick: Shape.plain.number(),
|
|
413
|
+
* })
|
|
414
|
+
*
|
|
415
|
+
* const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
416
|
+
* client: ClientPresenceShape,
|
|
417
|
+
* server: ServerPresenceShape,
|
|
418
|
+
* })
|
|
419
|
+
* ```
|
|
420
|
+
*
|
|
421
|
+
* @param discriminantKey - The key used to discriminate between variants (e.g., "type")
|
|
422
|
+
* @param variants - A record mapping discriminant values to their object shapes
|
|
423
|
+
*/
|
|
424
|
+
discriminatedUnion: (discriminantKey, variants) => {
|
|
425
|
+
const base = {
|
|
426
|
+
_type: "value",
|
|
427
|
+
valueType: "discriminatedUnion",
|
|
428
|
+
discriminantKey,
|
|
429
|
+
variants,
|
|
430
|
+
_plain: {},
|
|
431
|
+
_draft: {},
|
|
432
|
+
_placeholder: {}
|
|
433
|
+
};
|
|
434
|
+
return Object.assign(base, {
|
|
435
|
+
placeholder(value) {
|
|
436
|
+
return { ...base, _placeholder: value };
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// src/typed-doc.ts
|
|
2
444
|
import { LoroDoc } from "loro-crdt";
|
|
3
445
|
|
|
4
446
|
// src/draft-nodes/base.ts
|
|
@@ -10,8 +452,11 @@ var DraftNode = class {
|
|
|
10
452
|
get shape() {
|
|
11
453
|
return this._params.shape;
|
|
12
454
|
}
|
|
13
|
-
get
|
|
14
|
-
return this._params.
|
|
455
|
+
get placeholder() {
|
|
456
|
+
return this._params.placeholder;
|
|
457
|
+
}
|
|
458
|
+
get readonly() {
|
|
459
|
+
return !!this._params.readonly;
|
|
15
460
|
}
|
|
16
461
|
get container() {
|
|
17
462
|
if (!this._cachedContainer) {
|
|
@@ -46,32 +491,6 @@ import {
|
|
|
46
491
|
LoroMovableList,
|
|
47
492
|
LoroText
|
|
48
493
|
} from "loro-crdt";
|
|
49
|
-
|
|
50
|
-
// src/utils/type-guards.ts
|
|
51
|
-
import { isContainer, isContainerId } from "loro-crdt";
|
|
52
|
-
function isContainerShape(schema) {
|
|
53
|
-
return schema._type && schema._type !== "value";
|
|
54
|
-
}
|
|
55
|
-
function isValueShape(schema) {
|
|
56
|
-
return schema._type === "value" && [
|
|
57
|
-
"string",
|
|
58
|
-
"number",
|
|
59
|
-
"boolean",
|
|
60
|
-
"null",
|
|
61
|
-
"undefined",
|
|
62
|
-
"uint8array",
|
|
63
|
-
"object",
|
|
64
|
-
"record",
|
|
65
|
-
"array",
|
|
66
|
-
"union",
|
|
67
|
-
"discriminatedUnion"
|
|
68
|
-
].includes(schema.valueType);
|
|
69
|
-
}
|
|
70
|
-
function isObjectValue(value) {
|
|
71
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Uint8Array);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// src/conversion.ts
|
|
75
494
|
function convertTextInput(value) {
|
|
76
495
|
const text = new LoroText();
|
|
77
496
|
text.insert(0, value);
|
|
@@ -242,15 +661,16 @@ var ListDraftNodeBase = class extends DraftNode {
|
|
|
242
661
|
getDraftNodeParams(index, shape) {
|
|
243
662
|
return {
|
|
244
663
|
shape,
|
|
245
|
-
|
|
246
|
-
// List items don't have
|
|
664
|
+
placeholder: void 0,
|
|
665
|
+
// List items don't have placeholder
|
|
247
666
|
getContainer: () => {
|
|
248
667
|
const containerItem = this.container.get(index);
|
|
249
668
|
if (!containerItem || !isContainer(containerItem)) {
|
|
250
669
|
throw new Error(`No container found at index ${index}`);
|
|
251
670
|
}
|
|
252
671
|
return containerItem;
|
|
253
|
-
}
|
|
672
|
+
},
|
|
673
|
+
readonly: this.readonly
|
|
254
674
|
};
|
|
255
675
|
}
|
|
256
676
|
// Get item for predicate functions - always returns plain Item for filtering logic
|
|
@@ -294,13 +714,24 @@ var ListDraftNodeBase = class extends DraftNode {
|
|
|
294
714
|
} else {
|
|
295
715
|
cachedItem = containerItem;
|
|
296
716
|
}
|
|
297
|
-
this.
|
|
717
|
+
if (!this.readonly) {
|
|
718
|
+
this.itemCache.set(index, cachedItem);
|
|
719
|
+
}
|
|
298
720
|
return cachedItem;
|
|
299
721
|
} else {
|
|
300
722
|
cachedItem = createContainerDraftNode(
|
|
301
723
|
this.getDraftNodeParams(index, this.shape.shape)
|
|
302
724
|
);
|
|
303
725
|
this.itemCache.set(index, cachedItem);
|
|
726
|
+
if (this.readonly) {
|
|
727
|
+
const shape = this.shape.shape;
|
|
728
|
+
if (shape._type === "counter") {
|
|
729
|
+
return cachedItem.value;
|
|
730
|
+
}
|
|
731
|
+
if (shape._type === "text") {
|
|
732
|
+
return cachedItem.toString();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
304
735
|
return cachedItem;
|
|
305
736
|
}
|
|
306
737
|
}
|
|
@@ -367,20 +798,25 @@ var ListDraftNodeBase = class extends DraftNode {
|
|
|
367
798
|
return true;
|
|
368
799
|
}
|
|
369
800
|
insert(index, item) {
|
|
801
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
370
802
|
this.updateCacheForInsert(index);
|
|
371
803
|
this.insertWithConversion(index, item);
|
|
372
804
|
}
|
|
373
805
|
delete(index, len) {
|
|
806
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
374
807
|
this.updateCacheForDelete(index, len);
|
|
375
808
|
this.container.delete(index, len);
|
|
376
809
|
}
|
|
377
810
|
push(item) {
|
|
811
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
378
812
|
this.pushWithConversion(item);
|
|
379
813
|
}
|
|
380
814
|
pushContainer(container) {
|
|
815
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
381
816
|
return this.container.pushContainer(container);
|
|
382
817
|
}
|
|
383
818
|
insertContainer(index, container) {
|
|
819
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
384
820
|
return this.container.insertContainer(index, container);
|
|
385
821
|
}
|
|
386
822
|
get(index) {
|
|
@@ -469,12 +905,13 @@ var MapDraftNode = class extends DraftNode {
|
|
|
469
905
|
}
|
|
470
906
|
}
|
|
471
907
|
getDraftNodeParams(key, shape) {
|
|
472
|
-
const
|
|
908
|
+
const placeholder = this.placeholder?.[key];
|
|
473
909
|
const LoroContainer = containerConstructor[shape._type];
|
|
474
910
|
return {
|
|
475
911
|
shape,
|
|
476
|
-
|
|
477
|
-
getContainer: () => this.container.getOrCreateContainer(key, new LoroContainer())
|
|
912
|
+
placeholder,
|
|
913
|
+
getContainer: () => this.container.getOrCreateContainer(key, new LoroContainer()),
|
|
914
|
+
readonly: this.readonly
|
|
478
915
|
};
|
|
479
916
|
}
|
|
480
917
|
getOrCreateNode(key, shape) {
|
|
@@ -482,20 +919,35 @@ var MapDraftNode = class extends DraftNode {
|
|
|
482
919
|
if (!node) {
|
|
483
920
|
if (isContainerShape(shape)) {
|
|
484
921
|
node = createContainerDraftNode(this.getDraftNodeParams(key, shape));
|
|
922
|
+
this.propertyCache.set(key, node);
|
|
485
923
|
} else {
|
|
486
924
|
const containerValue = this.container.get(key);
|
|
487
925
|
if (containerValue !== void 0) {
|
|
488
926
|
node = containerValue;
|
|
489
927
|
} else {
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
throw new Error("
|
|
928
|
+
const placeholder = this.placeholder?.[key];
|
|
929
|
+
if (placeholder === void 0) {
|
|
930
|
+
throw new Error("placeholder required");
|
|
493
931
|
}
|
|
494
|
-
node =
|
|
932
|
+
node = placeholder;
|
|
933
|
+
}
|
|
934
|
+
if (!this.readonly) {
|
|
935
|
+
this.propertyCache.set(key, node);
|
|
495
936
|
}
|
|
496
937
|
}
|
|
497
938
|
if (node === void 0) throw new Error("no container made");
|
|
498
|
-
|
|
939
|
+
}
|
|
940
|
+
if (this.readonly && isContainerShape(shape)) {
|
|
941
|
+
const existing = this.container.get(key);
|
|
942
|
+
if (existing === void 0) {
|
|
943
|
+
return this.placeholder?.[key];
|
|
944
|
+
}
|
|
945
|
+
if (shape._type === "counter") {
|
|
946
|
+
return node.value;
|
|
947
|
+
}
|
|
948
|
+
if (shape._type === "text") {
|
|
949
|
+
return node.toString();
|
|
950
|
+
}
|
|
499
951
|
}
|
|
500
952
|
return node;
|
|
501
953
|
}
|
|
@@ -504,10 +956,23 @@ var MapDraftNode = class extends DraftNode {
|
|
|
504
956
|
const shape = this.shape.shapes[key];
|
|
505
957
|
Object.defineProperty(this, key, {
|
|
506
958
|
get: () => this.getOrCreateNode(key, shape),
|
|
507
|
-
set:
|
|
508
|
-
this.
|
|
509
|
-
|
|
510
|
-
|
|
959
|
+
set: (value) => {
|
|
960
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
961
|
+
if (isValueShape(shape)) {
|
|
962
|
+
this.container.set(key, value);
|
|
963
|
+
this.propertyCache.set(key, value);
|
|
964
|
+
} else {
|
|
965
|
+
if (value && typeof value === "object") {
|
|
966
|
+
const node = this.getOrCreateNode(key, shape);
|
|
967
|
+
if (assignPlainValueToDraftNode(node, value)) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
throw new Error(
|
|
972
|
+
"Cannot set container directly, modify the draft node instead"
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
511
976
|
});
|
|
512
977
|
}
|
|
513
978
|
}
|
|
@@ -516,12 +981,15 @@ var MapDraftNode = class extends DraftNode {
|
|
|
516
981
|
return this.container.get(key);
|
|
517
982
|
}
|
|
518
983
|
set(key, value) {
|
|
984
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
519
985
|
this.container.set(key, value);
|
|
520
986
|
}
|
|
521
987
|
setContainer(key, container) {
|
|
988
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
522
989
|
return this.container.setContainer(key, container);
|
|
523
990
|
}
|
|
524
991
|
delete(key) {
|
|
992
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
525
993
|
this.container.delete(key);
|
|
526
994
|
}
|
|
527
995
|
has(key) {
|
|
@@ -546,11 +1014,93 @@ var MovableListDraftNode = class extends ListDraftNodeBase {
|
|
|
546
1014
|
absorbValueAtIndex(index, value) {
|
|
547
1015
|
this.container.set(index, value);
|
|
548
1016
|
}
|
|
549
|
-
move(from, to) {
|
|
550
|
-
this.
|
|
1017
|
+
move(from, to) {
|
|
1018
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
1019
|
+
this.container.move(from, to);
|
|
1020
|
+
}
|
|
1021
|
+
set(index, item) {
|
|
1022
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
1023
|
+
return this.container.set(index, item);
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// src/draft-nodes/proxy-handlers.ts
|
|
1028
|
+
var recordProxyHandler = {
|
|
1029
|
+
get: (target, prop) => {
|
|
1030
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
1031
|
+
return target.get(prop);
|
|
1032
|
+
}
|
|
1033
|
+
return Reflect.get(target, prop);
|
|
1034
|
+
},
|
|
1035
|
+
set: (target, prop, value) => {
|
|
1036
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
1037
|
+
target.set(prop, value);
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
return Reflect.set(target, prop, value);
|
|
1041
|
+
},
|
|
1042
|
+
deleteProperty: (target, prop) => {
|
|
1043
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
1044
|
+
target.delete(prop);
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
return Reflect.deleteProperty(target, prop);
|
|
1048
|
+
},
|
|
1049
|
+
ownKeys: (target) => {
|
|
1050
|
+
return target.keys();
|
|
1051
|
+
},
|
|
1052
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
1053
|
+
if (typeof prop === "string" && target.has(prop)) {
|
|
1054
|
+
return {
|
|
1055
|
+
configurable: true,
|
|
1056
|
+
enumerable: true,
|
|
1057
|
+
value: target.get(prop)
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
var listProxyHandler = {
|
|
1064
|
+
get: (target, prop) => {
|
|
1065
|
+
if (typeof prop === "string") {
|
|
1066
|
+
const index = Number(prop);
|
|
1067
|
+
if (!Number.isNaN(index)) {
|
|
1068
|
+
return target.get(index);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return Reflect.get(target, prop);
|
|
1072
|
+
},
|
|
1073
|
+
set: (target, prop, value) => {
|
|
1074
|
+
if (typeof prop === "string") {
|
|
1075
|
+
const index = Number(prop);
|
|
1076
|
+
if (!Number.isNaN(index)) {
|
|
1077
|
+
target.delete(index, 1);
|
|
1078
|
+
target.insert(index, value);
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return Reflect.set(target, prop, value);
|
|
551
1083
|
}
|
|
552
|
-
|
|
553
|
-
|
|
1084
|
+
};
|
|
1085
|
+
var movableListProxyHandler = {
|
|
1086
|
+
get: (target, prop) => {
|
|
1087
|
+
if (typeof prop === "string") {
|
|
1088
|
+
const index = Number(prop);
|
|
1089
|
+
if (!Number.isNaN(index)) {
|
|
1090
|
+
return target.get(index);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return Reflect.get(target, prop);
|
|
1094
|
+
},
|
|
1095
|
+
set: (target, prop, value) => {
|
|
1096
|
+
if (typeof prop === "string") {
|
|
1097
|
+
const index = Number(prop);
|
|
1098
|
+
if (!Number.isNaN(index)) {
|
|
1099
|
+
target.set(index, value);
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return Reflect.set(target, prop, value);
|
|
554
1104
|
}
|
|
555
1105
|
};
|
|
556
1106
|
|
|
@@ -574,44 +1124,6 @@ var containerConstructor2 = {
|
|
|
574
1124
|
};
|
|
575
1125
|
var RecordDraftNode = class extends DraftNode {
|
|
576
1126
|
nodeCache = /* @__PURE__ */ new Map();
|
|
577
|
-
constructor(params) {
|
|
578
|
-
super(params);
|
|
579
|
-
return new Proxy(this, {
|
|
580
|
-
get: (target, prop) => {
|
|
581
|
-
if (typeof prop === "string" && !(prop in target)) {
|
|
582
|
-
return target.get(prop);
|
|
583
|
-
}
|
|
584
|
-
return Reflect.get(target, prop);
|
|
585
|
-
},
|
|
586
|
-
set: (target, prop, value) => {
|
|
587
|
-
if (typeof prop === "string" && !(prop in target)) {
|
|
588
|
-
target.set(prop, value);
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
return Reflect.set(target, prop, value);
|
|
592
|
-
},
|
|
593
|
-
deleteProperty: (target, prop) => {
|
|
594
|
-
if (typeof prop === "string" && !(prop in target)) {
|
|
595
|
-
target.delete(prop);
|
|
596
|
-
return true;
|
|
597
|
-
}
|
|
598
|
-
return Reflect.deleteProperty(target, prop);
|
|
599
|
-
},
|
|
600
|
-
ownKeys: (target) => {
|
|
601
|
-
return target.keys();
|
|
602
|
-
},
|
|
603
|
-
getOwnPropertyDescriptor: (target, prop) => {
|
|
604
|
-
if (typeof prop === "string" && target.has(prop)) {
|
|
605
|
-
return {
|
|
606
|
-
configurable: true,
|
|
607
|
-
enumerable: true,
|
|
608
|
-
value: target.get(prop)
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
1127
|
get shape() {
|
|
616
1128
|
return super.shape;
|
|
617
1129
|
}
|
|
@@ -628,12 +1140,13 @@ var RecordDraftNode = class extends DraftNode {
|
|
|
628
1140
|
}
|
|
629
1141
|
}
|
|
630
1142
|
getDraftNodeParams(key, shape) {
|
|
631
|
-
const
|
|
1143
|
+
const placeholder = this.placeholder?.[key];
|
|
632
1144
|
const LoroContainer = containerConstructor2[shape._type];
|
|
633
1145
|
return {
|
|
634
1146
|
shape,
|
|
635
|
-
|
|
636
|
-
getContainer: () => this.container.getOrCreateContainer(key, new LoroContainer())
|
|
1147
|
+
placeholder,
|
|
1148
|
+
getContainer: () => this.container.getOrCreateContainer(key, new LoroContainer()),
|
|
1149
|
+
readonly: this.readonly
|
|
637
1150
|
};
|
|
638
1151
|
}
|
|
639
1152
|
getOrCreateNode(key) {
|
|
@@ -644,21 +1157,31 @@ var RecordDraftNode = class extends DraftNode {
|
|
|
644
1157
|
node = createContainerDraftNode(
|
|
645
1158
|
this.getDraftNodeParams(key, shape)
|
|
646
1159
|
);
|
|
1160
|
+
this.nodeCache.set(key, node);
|
|
647
1161
|
} else {
|
|
648
1162
|
const containerValue = this.container.get(key);
|
|
649
1163
|
if (containerValue !== void 0) {
|
|
650
1164
|
node = containerValue;
|
|
651
1165
|
} else {
|
|
652
|
-
const
|
|
653
|
-
if (
|
|
1166
|
+
const placeholder = this.placeholder?.[key];
|
|
1167
|
+
if (placeholder === void 0) {
|
|
654
1168
|
node = shape._plain;
|
|
655
1169
|
} else {
|
|
656
|
-
node =
|
|
1170
|
+
node = placeholder;
|
|
657
1171
|
}
|
|
658
1172
|
}
|
|
1173
|
+
if (node !== void 0 && !this.readonly) {
|
|
1174
|
+
this.nodeCache.set(key, node);
|
|
1175
|
+
}
|
|
659
1176
|
}
|
|
660
|
-
|
|
661
|
-
|
|
1177
|
+
}
|
|
1178
|
+
if (this.readonly && isContainerShape(this.shape.shape)) {
|
|
1179
|
+
const shape = this.shape.shape;
|
|
1180
|
+
if (shape._type === "counter") {
|
|
1181
|
+
return node.value;
|
|
1182
|
+
}
|
|
1183
|
+
if (shape._type === "text") {
|
|
1184
|
+
return node.toString();
|
|
662
1185
|
}
|
|
663
1186
|
}
|
|
664
1187
|
return node;
|
|
@@ -667,19 +1190,28 @@ var RecordDraftNode = class extends DraftNode {
|
|
|
667
1190
|
return this.getOrCreateNode(key);
|
|
668
1191
|
}
|
|
669
1192
|
set(key, value) {
|
|
1193
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
670
1194
|
if (isValueShape(this.shape.shape)) {
|
|
671
1195
|
this.container.set(key, value);
|
|
672
1196
|
this.nodeCache.set(key, value);
|
|
673
1197
|
} else {
|
|
1198
|
+
if (value && typeof value === "object") {
|
|
1199
|
+
const node = this.getOrCreateNode(key);
|
|
1200
|
+
if (assignPlainValueToDraftNode(node, value)) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
674
1204
|
throw new Error(
|
|
675
1205
|
"Cannot set container directly, modify the draft node instead"
|
|
676
1206
|
);
|
|
677
1207
|
}
|
|
678
1208
|
}
|
|
679
1209
|
setContainer(key, container) {
|
|
1210
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
680
1211
|
return this.container.setContainer(key, container);
|
|
681
1212
|
}
|
|
682
1213
|
delete(key) {
|
|
1214
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc");
|
|
683
1215
|
this.container.delete(key);
|
|
684
1216
|
this.nodeCache.delete(key);
|
|
685
1217
|
}
|
|
@@ -760,16 +1292,23 @@ function createContainerDraftNode(params) {
|
|
|
760
1292
|
params
|
|
761
1293
|
);
|
|
762
1294
|
case "list":
|
|
763
|
-
return new
|
|
1295
|
+
return new Proxy(
|
|
1296
|
+
new ListDraftNode(params),
|
|
1297
|
+
listProxyHandler
|
|
1298
|
+
);
|
|
764
1299
|
case "map":
|
|
765
1300
|
return new MapDraftNode(params);
|
|
766
1301
|
case "movableList":
|
|
767
|
-
return new
|
|
768
|
-
|
|
1302
|
+
return new Proxy(
|
|
1303
|
+
new MovableListDraftNode(
|
|
1304
|
+
params
|
|
1305
|
+
),
|
|
1306
|
+
movableListProxyHandler
|
|
769
1307
|
);
|
|
770
1308
|
case "record":
|
|
771
|
-
return new
|
|
772
|
-
params
|
|
1309
|
+
return new Proxy(
|
|
1310
|
+
new RecordDraftNode(params),
|
|
1311
|
+
recordProxyHandler
|
|
773
1312
|
);
|
|
774
1313
|
case "text":
|
|
775
1314
|
return new TextDraftNode(params);
|
|
@@ -781,6 +1320,29 @@ function createContainerDraftNode(params) {
|
|
|
781
1320
|
);
|
|
782
1321
|
}
|
|
783
1322
|
}
|
|
1323
|
+
function assignPlainValueToDraftNode(node, value) {
|
|
1324
|
+
const shapeType = node.shape._type;
|
|
1325
|
+
if (shapeType === "map" || shapeType === "record") {
|
|
1326
|
+
for (const k in value) {
|
|
1327
|
+
;
|
|
1328
|
+
node[k] = value[k];
|
|
1329
|
+
}
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
if (shapeType === "list" || shapeType === "movableList") {
|
|
1333
|
+
if (Array.isArray(value)) {
|
|
1334
|
+
const listNode = node;
|
|
1335
|
+
if (listNode.length > 0) {
|
|
1336
|
+
listNode.delete(0, listNode.length);
|
|
1337
|
+
}
|
|
1338
|
+
for (const item of value) {
|
|
1339
|
+
listNode.push(item);
|
|
1340
|
+
}
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
784
1346
|
|
|
785
1347
|
// src/draft-nodes/doc.ts
|
|
786
1348
|
var containerGetter = {
|
|
@@ -795,7 +1357,7 @@ var containerGetter = {
|
|
|
795
1357
|
var DraftDoc = class extends DraftNode {
|
|
796
1358
|
doc;
|
|
797
1359
|
propertyCache = /* @__PURE__ */ new Map();
|
|
798
|
-
|
|
1360
|
+
requiredPlaceholder;
|
|
799
1361
|
constructor(_params) {
|
|
800
1362
|
super({
|
|
801
1363
|
..._params,
|
|
@@ -803,25 +1365,40 @@ var DraftDoc = class extends DraftNode {
|
|
|
803
1365
|
throw new Error("can't get container on DraftDoc");
|
|
804
1366
|
}
|
|
805
1367
|
});
|
|
806
|
-
if (!_params.
|
|
1368
|
+
if (!_params.placeholder) throw new Error("placeholder required");
|
|
807
1369
|
this.doc = _params.doc;
|
|
808
|
-
this.
|
|
1370
|
+
this.requiredPlaceholder = _params.placeholder;
|
|
809
1371
|
this.createLazyProperties();
|
|
810
1372
|
}
|
|
811
1373
|
getDraftNodeParams(key, shape) {
|
|
812
1374
|
const getter = this.doc[containerGetter[shape._type]].bind(this.doc);
|
|
813
1375
|
return {
|
|
814
1376
|
shape,
|
|
815
|
-
|
|
816
|
-
getContainer: () => getter(key)
|
|
1377
|
+
placeholder: this.requiredPlaceholder[key],
|
|
1378
|
+
getContainer: () => getter(key),
|
|
1379
|
+
readonly: this.readonly
|
|
817
1380
|
};
|
|
818
1381
|
}
|
|
819
1382
|
getOrCreateDraftNode(key, shape) {
|
|
1383
|
+
if (this.readonly && (shape._type === "counter" || shape._type === "text")) {
|
|
1384
|
+
const shallow = this.doc.getShallowValue();
|
|
1385
|
+
if (!shallow[key]) {
|
|
1386
|
+
return this.requiredPlaceholder[key];
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
820
1389
|
let node = this.propertyCache.get(key);
|
|
821
1390
|
if (!node) {
|
|
822
1391
|
node = createContainerDraftNode(this.getDraftNodeParams(key, shape));
|
|
823
1392
|
this.propertyCache.set(key, node);
|
|
824
1393
|
}
|
|
1394
|
+
if (this.readonly) {
|
|
1395
|
+
if (shape._type === "counter") {
|
|
1396
|
+
return node.value;
|
|
1397
|
+
}
|
|
1398
|
+
if (shape._type === "text") {
|
|
1399
|
+
return node.toString();
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
825
1402
|
return node;
|
|
826
1403
|
}
|
|
827
1404
|
createLazyProperties() {
|
|
@@ -1037,102 +1614,6 @@ var JsonPatchApplicator = class {
|
|
|
1037
1614
|
}
|
|
1038
1615
|
};
|
|
1039
1616
|
|
|
1040
|
-
// src/overlay.ts
|
|
1041
|
-
function overlayEmptyState(shape, crdtValue, emptyValue) {
|
|
1042
|
-
if (typeof crdtValue !== "object") {
|
|
1043
|
-
throw new Error("crdt object is required");
|
|
1044
|
-
}
|
|
1045
|
-
if (typeof emptyValue !== "object") {
|
|
1046
|
-
throw new Error("empty object is required");
|
|
1047
|
-
}
|
|
1048
|
-
const result = { ...emptyValue };
|
|
1049
|
-
for (const [key, propShape] of Object.entries(shape.shapes)) {
|
|
1050
|
-
const propCrdtValue = crdtValue[key];
|
|
1051
|
-
const propEmptyValue = emptyValue[key];
|
|
1052
|
-
result[key] = mergeValue(
|
|
1053
|
-
propShape,
|
|
1054
|
-
propCrdtValue,
|
|
1055
|
-
propEmptyValue
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
return result;
|
|
1059
|
-
}
|
|
1060
|
-
function mergeValue(shape, crdtValue, emptyValue) {
|
|
1061
|
-
if (crdtValue === void 0 && emptyValue === void 0) {
|
|
1062
|
-
throw new Error("either crdt or empty value must be defined");
|
|
1063
|
-
}
|
|
1064
|
-
switch (shape._type) {
|
|
1065
|
-
case "text":
|
|
1066
|
-
return crdtValue ?? emptyValue ?? "";
|
|
1067
|
-
case "counter":
|
|
1068
|
-
return crdtValue ?? emptyValue ?? 0;
|
|
1069
|
-
case "list":
|
|
1070
|
-
case "movableList":
|
|
1071
|
-
return crdtValue ?? emptyValue ?? [];
|
|
1072
|
-
case "map": {
|
|
1073
|
-
if (!isObjectValue(crdtValue) && crdtValue !== void 0) {
|
|
1074
|
-
throw new Error("map crdt must be object");
|
|
1075
|
-
}
|
|
1076
|
-
const crdtMapValue = crdtValue ?? {};
|
|
1077
|
-
if (!isObjectValue(emptyValue) && emptyValue !== void 0) {
|
|
1078
|
-
throw new Error("map empty state must be object");
|
|
1079
|
-
}
|
|
1080
|
-
const emptyMapValue = emptyValue ?? {};
|
|
1081
|
-
const result = { ...emptyMapValue };
|
|
1082
|
-
for (const [key, nestedShape] of Object.entries(shape.shapes)) {
|
|
1083
|
-
const nestedCrdtValue = crdtMapValue[key];
|
|
1084
|
-
const nestedEmptyValue = emptyMapValue[key];
|
|
1085
|
-
result[key] = mergeValue(
|
|
1086
|
-
nestedShape,
|
|
1087
|
-
nestedCrdtValue,
|
|
1088
|
-
nestedEmptyValue
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
return result;
|
|
1092
|
-
}
|
|
1093
|
-
case "tree":
|
|
1094
|
-
return crdtValue ?? emptyValue ?? [];
|
|
1095
|
-
default:
|
|
1096
|
-
if (shape._type === "value" && shape.valueType === "object") {
|
|
1097
|
-
const crdtObj = crdtValue ?? {};
|
|
1098
|
-
const emptyObj = emptyValue ?? {};
|
|
1099
|
-
const result = { ...emptyObj };
|
|
1100
|
-
if (typeof crdtObj !== "object" || crdtObj === null) {
|
|
1101
|
-
return crdtValue ?? emptyValue;
|
|
1102
|
-
}
|
|
1103
|
-
for (const [key, propShape] of Object.entries(shape.shape)) {
|
|
1104
|
-
const propCrdt = crdtObj[key];
|
|
1105
|
-
const propEmpty = emptyObj[key];
|
|
1106
|
-
result[key] = mergeValue(propShape, propCrdt, propEmpty);
|
|
1107
|
-
}
|
|
1108
|
-
return result;
|
|
1109
|
-
}
|
|
1110
|
-
if (shape._type === "value" && shape.valueType === "discriminatedUnion") {
|
|
1111
|
-
return mergeDiscriminatedUnion(
|
|
1112
|
-
shape,
|
|
1113
|
-
crdtValue,
|
|
1114
|
-
emptyValue
|
|
1115
|
-
);
|
|
1116
|
-
}
|
|
1117
|
-
return crdtValue ?? emptyValue;
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
function mergeDiscriminatedUnion(shape, crdtValue, emptyValue) {
|
|
1121
|
-
const crdtObj = crdtValue ?? {};
|
|
1122
|
-
const emptyObj = emptyValue ?? {};
|
|
1123
|
-
const discriminantValue = crdtObj[shape.discriminantKey] ?? emptyObj[shape.discriminantKey];
|
|
1124
|
-
if (typeof discriminantValue !== "string") {
|
|
1125
|
-
return emptyValue;
|
|
1126
|
-
}
|
|
1127
|
-
const variantShape = shape.variants[discriminantValue];
|
|
1128
|
-
if (!variantShape) {
|
|
1129
|
-
return crdtValue ?? emptyValue;
|
|
1130
|
-
}
|
|
1131
|
-
const emptyDiscriminant = emptyObj[shape.discriminantKey];
|
|
1132
|
-
const effectiveEmptyValue = emptyDiscriminant === discriminantValue ? emptyValue : void 0;
|
|
1133
|
-
return mergeValue(variantShape, crdtValue, effectiveEmptyValue);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
1617
|
// src/validation.ts
|
|
1137
1618
|
function validateValue(value, schema, path = "") {
|
|
1138
1619
|
if (!schema || typeof schema !== "object" || !("_type" in schema)) {
|
|
@@ -1343,53 +1824,71 @@ function validateValue(value, schema, path = "") {
|
|
|
1343
1824
|
}
|
|
1344
1825
|
throw new Error(`Unknown schema type: ${schema._type}`);
|
|
1345
1826
|
}
|
|
1346
|
-
function
|
|
1347
|
-
if (!
|
|
1348
|
-
throw new Error("
|
|
1827
|
+
function validatePlaceholder(placeholder, schema) {
|
|
1828
|
+
if (!placeholder || typeof placeholder !== "object" || Array.isArray(placeholder)) {
|
|
1829
|
+
throw new Error("Placeholder must be an object");
|
|
1349
1830
|
}
|
|
1350
1831
|
const result = {};
|
|
1351
1832
|
for (const [key, schemaValue] of Object.entries(schema.shapes)) {
|
|
1352
|
-
const value =
|
|
1833
|
+
const value = placeholder[key];
|
|
1353
1834
|
result[key] = validateValue(value, schemaValue, key);
|
|
1354
1835
|
}
|
|
1355
1836
|
return result;
|
|
1356
1837
|
}
|
|
1357
1838
|
|
|
1358
|
-
// src/
|
|
1839
|
+
// src/typed-doc.ts
|
|
1359
1840
|
var TypedDoc = class {
|
|
1841
|
+
shape;
|
|
1842
|
+
placeholder;
|
|
1843
|
+
doc;
|
|
1360
1844
|
/**
|
|
1361
|
-
* Creates a new TypedDoc with the given schema
|
|
1845
|
+
* Creates a new TypedDoc with the given schema.
|
|
1846
|
+
* Placeholder state is automatically derived from the schema's placeholder values.
|
|
1362
1847
|
*
|
|
1363
|
-
* @param shape - The document schema
|
|
1364
|
-
* @param emptyState - Default values for the document. For dynamic containers
|
|
1365
|
-
* (list, record, etc.), only empty values ([] or {}) are allowed. Use
|
|
1366
|
-
* `.change()` to add initial data after construction.
|
|
1848
|
+
* @param shape - The document schema (with optional .placeholder() values)
|
|
1367
1849
|
* @param doc - Optional existing LoroDoc to wrap
|
|
1368
1850
|
*/
|
|
1369
|
-
constructor(shape,
|
|
1851
|
+
constructor(shape, doc = new LoroDoc()) {
|
|
1370
1852
|
this.shape = shape;
|
|
1371
|
-
this.
|
|
1853
|
+
this.placeholder = derivePlaceholder(shape);
|
|
1372
1854
|
this.doc = doc;
|
|
1373
|
-
|
|
1855
|
+
validatePlaceholder(this.placeholder, this.shape);
|
|
1374
1856
|
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Returns a read-only, live view of the document.
|
|
1859
|
+
* Accessing properties on this object will read directly from the underlying CRDT.
|
|
1860
|
+
* This is efficient (O(1) per access) and always up-to-date.
|
|
1861
|
+
*/
|
|
1375
1862
|
get value() {
|
|
1863
|
+
return new DraftDoc({
|
|
1864
|
+
shape: this.shape,
|
|
1865
|
+
placeholder: this.placeholder,
|
|
1866
|
+
doc: this.doc,
|
|
1867
|
+
readonly: true
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Returns the full plain JavaScript object representation of the document.
|
|
1872
|
+
* This is an expensive O(N) operation that serializes the entire document.
|
|
1873
|
+
*/
|
|
1874
|
+
toJSON() {
|
|
1376
1875
|
const crdtValue = this.doc.toJSON();
|
|
1377
|
-
return
|
|
1876
|
+
return overlayPlaceholder(
|
|
1378
1877
|
this.shape,
|
|
1379
1878
|
crdtValue,
|
|
1380
|
-
this.
|
|
1879
|
+
this.placeholder
|
|
1381
1880
|
);
|
|
1382
1881
|
}
|
|
1383
1882
|
change(fn) {
|
|
1384
1883
|
const draft = new DraftDoc({
|
|
1385
1884
|
shape: this.shape,
|
|
1386
|
-
|
|
1885
|
+
placeholder: this.placeholder,
|
|
1387
1886
|
doc: this.doc
|
|
1388
1887
|
});
|
|
1389
1888
|
fn(draft);
|
|
1390
1889
|
draft.absorbPlainValues();
|
|
1391
1890
|
this.doc.commit();
|
|
1392
|
-
return this.
|
|
1891
|
+
return this.toJSON();
|
|
1393
1892
|
}
|
|
1394
1893
|
/**
|
|
1395
1894
|
* Apply JSON Patch operations to the document
|
|
@@ -1429,193 +1928,17 @@ var TypedDoc = class {
|
|
|
1429
1928
|
return this.doc.toJSON();
|
|
1430
1929
|
}
|
|
1431
1930
|
};
|
|
1432
|
-
function createTypedDoc(shape,
|
|
1433
|
-
return new TypedDoc(shape,
|
|
1931
|
+
function createTypedDoc(shape, existingDoc) {
|
|
1932
|
+
return new TypedDoc(shape, existingDoc || new LoroDoc());
|
|
1434
1933
|
}
|
|
1435
|
-
|
|
1436
|
-
// src/shape.ts
|
|
1437
|
-
var Shape = {
|
|
1438
|
-
doc: (shape) => ({
|
|
1439
|
-
_type: "doc",
|
|
1440
|
-
shapes: shape,
|
|
1441
|
-
_plain: {},
|
|
1442
|
-
_draft: {},
|
|
1443
|
-
_emptyState: {}
|
|
1444
|
-
}),
|
|
1445
|
-
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
1446
|
-
// various CRDT algorithms
|
|
1447
|
-
counter: () => ({
|
|
1448
|
-
_type: "counter",
|
|
1449
|
-
_plain: 0,
|
|
1450
|
-
_draft: {},
|
|
1451
|
-
_emptyState: 0
|
|
1452
|
-
}),
|
|
1453
|
-
list: (shape) => ({
|
|
1454
|
-
_type: "list",
|
|
1455
|
-
shape,
|
|
1456
|
-
_plain: [],
|
|
1457
|
-
_draft: {},
|
|
1458
|
-
_emptyState: []
|
|
1459
|
-
}),
|
|
1460
|
-
map: (shape) => ({
|
|
1461
|
-
_type: "map",
|
|
1462
|
-
shapes: shape,
|
|
1463
|
-
_plain: {},
|
|
1464
|
-
_draft: {},
|
|
1465
|
-
_emptyState: {}
|
|
1466
|
-
}),
|
|
1467
|
-
record: (shape) => ({
|
|
1468
|
-
_type: "record",
|
|
1469
|
-
shape,
|
|
1470
|
-
_plain: {},
|
|
1471
|
-
_draft: {},
|
|
1472
|
-
_emptyState: {}
|
|
1473
|
-
}),
|
|
1474
|
-
movableList: (shape) => ({
|
|
1475
|
-
_type: "movableList",
|
|
1476
|
-
shape,
|
|
1477
|
-
_plain: [],
|
|
1478
|
-
_draft: {},
|
|
1479
|
-
_emptyState: []
|
|
1480
|
-
}),
|
|
1481
|
-
text: () => ({
|
|
1482
|
-
_type: "text",
|
|
1483
|
-
_plain: "",
|
|
1484
|
-
_draft: {},
|
|
1485
|
-
_emptyState: ""
|
|
1486
|
-
}),
|
|
1487
|
-
tree: (shape) => ({
|
|
1488
|
-
_type: "tree",
|
|
1489
|
-
shape,
|
|
1490
|
-
_plain: {},
|
|
1491
|
-
_draft: {},
|
|
1492
|
-
_emptyState: []
|
|
1493
|
-
}),
|
|
1494
|
-
// Values are represented as plain JS objects, with the limitation that they MUST be
|
|
1495
|
-
// representable as a Loro "Value"--basically JSON. The behavior of a Value is basically
|
|
1496
|
-
// "Last Write Wins", meaning there is no subtle convergent behavior here, just taking
|
|
1497
|
-
// the most recent value based on the current available information.
|
|
1498
|
-
plain: {
|
|
1499
|
-
string: (...options) => ({
|
|
1500
|
-
_type: "value",
|
|
1501
|
-
valueType: "string",
|
|
1502
|
-
_plain: options[0] ?? "",
|
|
1503
|
-
_draft: options[0] ?? "",
|
|
1504
|
-
_emptyState: options[0] ?? "",
|
|
1505
|
-
options: options.length > 0 ? options : void 0
|
|
1506
|
-
}),
|
|
1507
|
-
number: () => ({
|
|
1508
|
-
_type: "value",
|
|
1509
|
-
valueType: "number",
|
|
1510
|
-
_plain: 0,
|
|
1511
|
-
_draft: 0,
|
|
1512
|
-
_emptyState: 0
|
|
1513
|
-
}),
|
|
1514
|
-
boolean: () => ({
|
|
1515
|
-
_type: "value",
|
|
1516
|
-
valueType: "boolean",
|
|
1517
|
-
_plain: false,
|
|
1518
|
-
_draft: false,
|
|
1519
|
-
_emptyState: false
|
|
1520
|
-
}),
|
|
1521
|
-
null: () => ({
|
|
1522
|
-
_type: "value",
|
|
1523
|
-
valueType: "null",
|
|
1524
|
-
_plain: null,
|
|
1525
|
-
_draft: null,
|
|
1526
|
-
_emptyState: null
|
|
1527
|
-
}),
|
|
1528
|
-
undefined: () => ({
|
|
1529
|
-
_type: "value",
|
|
1530
|
-
valueType: "undefined",
|
|
1531
|
-
_plain: void 0,
|
|
1532
|
-
_draft: void 0,
|
|
1533
|
-
_emptyState: void 0
|
|
1534
|
-
}),
|
|
1535
|
-
uint8Array: () => ({
|
|
1536
|
-
_type: "value",
|
|
1537
|
-
valueType: "uint8array",
|
|
1538
|
-
_plain: new Uint8Array(),
|
|
1539
|
-
_draft: new Uint8Array(),
|
|
1540
|
-
_emptyState: new Uint8Array()
|
|
1541
|
-
}),
|
|
1542
|
-
object: (shape) => ({
|
|
1543
|
-
_type: "value",
|
|
1544
|
-
valueType: "object",
|
|
1545
|
-
shape,
|
|
1546
|
-
_plain: {},
|
|
1547
|
-
_draft: {},
|
|
1548
|
-
_emptyState: {}
|
|
1549
|
-
}),
|
|
1550
|
-
record: (shape) => ({
|
|
1551
|
-
_type: "value",
|
|
1552
|
-
valueType: "record",
|
|
1553
|
-
shape,
|
|
1554
|
-
_plain: {},
|
|
1555
|
-
_draft: {},
|
|
1556
|
-
_emptyState: {}
|
|
1557
|
-
}),
|
|
1558
|
-
array: (shape) => ({
|
|
1559
|
-
_type: "value",
|
|
1560
|
-
valueType: "array",
|
|
1561
|
-
shape,
|
|
1562
|
-
_plain: [],
|
|
1563
|
-
_draft: [],
|
|
1564
|
-
_emptyState: []
|
|
1565
|
-
}),
|
|
1566
|
-
// Special value type that helps make things like `string | null` representable
|
|
1567
|
-
// TODO(duane): should this be a more general type for containers too?
|
|
1568
|
-
union: (shapes) => ({
|
|
1569
|
-
_type: "value",
|
|
1570
|
-
valueType: "union",
|
|
1571
|
-
shapes,
|
|
1572
|
-
_plain: {},
|
|
1573
|
-
_draft: {},
|
|
1574
|
-
_emptyState: {}
|
|
1575
|
-
}),
|
|
1576
|
-
/**
|
|
1577
|
-
* Creates a discriminated union shape for type-safe tagged unions.
|
|
1578
|
-
*
|
|
1579
|
-
* @example
|
|
1580
|
-
* ```typescript
|
|
1581
|
-
* const ClientPresenceShape = Shape.plain.object({
|
|
1582
|
-
* type: Shape.plain.string("client"),
|
|
1583
|
-
* name: Shape.plain.string(),
|
|
1584
|
-
* input: Shape.plain.object({ force: Shape.plain.number(), angle: Shape.plain.number() }),
|
|
1585
|
-
* })
|
|
1586
|
-
*
|
|
1587
|
-
* const ServerPresenceShape = Shape.plain.object({
|
|
1588
|
-
* type: Shape.plain.string("server"),
|
|
1589
|
-
* cars: Shape.plain.record(Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() })),
|
|
1590
|
-
* tick: Shape.plain.number(),
|
|
1591
|
-
* })
|
|
1592
|
-
*
|
|
1593
|
-
* const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
1594
|
-
* client: ClientPresenceShape,
|
|
1595
|
-
* server: ServerPresenceShape,
|
|
1596
|
-
* })
|
|
1597
|
-
* ```
|
|
1598
|
-
*
|
|
1599
|
-
* @param discriminantKey - The key used to discriminate between variants (e.g., "type")
|
|
1600
|
-
* @param variants - A record mapping discriminant values to their object shapes
|
|
1601
|
-
*/
|
|
1602
|
-
discriminatedUnion: (discriminantKey, variants) => ({
|
|
1603
|
-
_type: "value",
|
|
1604
|
-
valueType: "discriminatedUnion",
|
|
1605
|
-
discriminantKey,
|
|
1606
|
-
variants,
|
|
1607
|
-
_plain: {},
|
|
1608
|
-
_draft: {},
|
|
1609
|
-
_emptyState: {}
|
|
1610
|
-
})
|
|
1611
|
-
}
|
|
1612
|
-
};
|
|
1613
1934
|
export {
|
|
1614
1935
|
Shape,
|
|
1615
1936
|
TypedDoc,
|
|
1616
1937
|
createTypedDoc,
|
|
1938
|
+
derivePlaceholder,
|
|
1939
|
+
deriveShapePlaceholder,
|
|
1617
1940
|
mergeValue,
|
|
1618
|
-
|
|
1619
|
-
|
|
1941
|
+
overlayPlaceholder,
|
|
1942
|
+
validatePlaceholder
|
|
1620
1943
|
};
|
|
1621
1944
|
//# sourceMappingURL=index.js.map
|