@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/dist/index.js CHANGED
@@ -1,4 +1,446 @@
1
- // src/change.ts
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 emptyState() {
14
- return this._params.emptyState;
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
- emptyState: void 0,
246
- // List items don't have empty state
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.itemCache.set(index, cachedItem);
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 emptyState = this.emptyState?.[key];
908
+ const placeholder = this.placeholder?.[key];
473
909
  const LoroContainer = containerConstructor[shape._type];
474
910
  return {
475
911
  shape,
476
- emptyState,
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 emptyState = this.emptyState?.[key];
491
- if (emptyState === void 0) {
492
- throw new Error("empty state required");
928
+ const placeholder = this.placeholder?.[key];
929
+ if (placeholder === void 0) {
930
+ throw new Error("placeholder required");
493
931
  }
494
- node = emptyState;
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
- this.propertyCache.set(key, node);
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: isValueShape(shape) ? (value) => {
508
- this.container.set(key, value);
509
- this.propertyCache.set(key, value);
510
- } : void 0
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.container.move(from, to);
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
- set(index, item) {
553
- return this.container.set(index, item);
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 emptyState = this.emptyState?.[key];
1143
+ const placeholder = this.placeholder?.[key];
632
1144
  const LoroContainer = containerConstructor2[shape._type];
633
1145
  return {
634
1146
  shape,
635
- emptyState,
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 emptyState = this.emptyState?.[key];
653
- if (emptyState === void 0) {
1166
+ const placeholder = this.placeholder?.[key];
1167
+ if (placeholder === void 0) {
654
1168
  node = shape._plain;
655
1169
  } else {
656
- node = emptyState;
1170
+ node = placeholder;
657
1171
  }
658
1172
  }
1173
+ if (node !== void 0 && !this.readonly) {
1174
+ this.nodeCache.set(key, node);
1175
+ }
659
1176
  }
660
- if (node !== void 0) {
661
- this.nodeCache.set(key, node);
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 ListDraftNode(params);
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 MovableListDraftNode(
768
- params
1302
+ return new Proxy(
1303
+ new MovableListDraftNode(
1304
+ params
1305
+ ),
1306
+ movableListProxyHandler
769
1307
  );
770
1308
  case "record":
771
- return new RecordDraftNode(
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
- requiredEmptyState;
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.emptyState) throw new Error("emptyState required");
1368
+ if (!_params.placeholder) throw new Error("placeholder required");
807
1369
  this.doc = _params.doc;
808
- this.requiredEmptyState = _params.emptyState;
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
- emptyState: this.requiredEmptyState[key],
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 validateEmptyState(emptyState, schema) {
1347
- if (!emptyState || typeof emptyState !== "object" || Array.isArray(emptyState)) {
1348
- throw new Error("Empty state must be an object");
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 = emptyState[key];
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/change.ts
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 and empty state.
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, emptyState, doc = new LoroDoc()) {
1851
+ constructor(shape, doc = new LoroDoc()) {
1370
1852
  this.shape = shape;
1371
- this.emptyState = emptyState;
1853
+ this.placeholder = derivePlaceholder(shape);
1372
1854
  this.doc = doc;
1373
- validateEmptyState(emptyState, shape);
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 overlayEmptyState(
1876
+ return overlayPlaceholder(
1378
1877
  this.shape,
1379
1878
  crdtValue,
1380
- this.emptyState
1879
+ this.placeholder
1381
1880
  );
1382
1881
  }
1383
1882
  change(fn) {
1384
1883
  const draft = new DraftDoc({
1385
1884
  shape: this.shape,
1386
- emptyState: this.emptyState,
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.value;
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, emptyState, existingDoc) {
1433
- return new TypedDoc(shape, emptyState, existingDoc || new LoroDoc());
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
- overlayEmptyState,
1619
- validateEmptyState
1941
+ overlayPlaceholder,
1942
+ validatePlaceholder
1620
1943
  };
1621
1944
  //# sourceMappingURL=index.js.map