@loro-extended/change 0.6.0 → 0.8.0

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.
@@ -13,10 +13,13 @@ import type {
13
13
  ContainerShape,
14
14
  RecordContainerShape,
15
15
  } from "../shape.js"
16
- import type { InferDraftType } from "../types.js"
16
+ import type { Infer, InferDraftType } from "../types.js"
17
17
  import { isContainerShape, isValueShape } from "../utils/type-guards.js"
18
18
  import { DraftNode, type DraftNodeParams } from "./base.js"
19
- import { createContainerDraftNode } from "./utils.js"
19
+ import {
20
+ assignPlainValueToDraftNode,
21
+ createContainerDraftNode,
22
+ } from "./utils.js"
20
23
 
21
24
  const containerConstructor = {
22
25
  counter: LoroCounter,
@@ -32,54 +35,9 @@ const containerConstructor = {
32
35
  export class RecordDraftNode<
33
36
  NestedShape extends ContainerOrValueShape,
34
37
  > extends DraftNode<any> {
38
+ [key: string]: Infer<NestedShape> | any
35
39
  private nodeCache = new Map<string, DraftNode<ContainerShape> | Value>()
36
40
 
37
- constructor(params: DraftNodeParams<RecordContainerShape<NestedShape>>) {
38
- super(params)
39
- // We don't need to create lazy properties because keys are dynamic
40
- // But we could use a Proxy if we wanted property access syntax like record.key
41
- // However, for now let's stick to get/set methods or maybe Proxy for better DX?
42
- // The requirement says "records with uniform specific key type and value".
43
- // Usually records are accessed via keys.
44
- // If we want `draft.record.key`, we need a Proxy.
45
- // biome-ignore lint/correctness/noConstructorReturn: Proxy return is intentional
46
- return new Proxy(this, {
47
- get: (target, prop) => {
48
- if (typeof prop === "string" && !(prop in target)) {
49
- return target.get(prop)
50
- }
51
- return Reflect.get(target, prop)
52
- },
53
- set: (target, prop, value) => {
54
- if (typeof prop === "string" && !(prop in target)) {
55
- target.set(prop, value)
56
- return true
57
- }
58
- return Reflect.set(target, prop, value)
59
- },
60
- deleteProperty: (target, prop) => {
61
- if (typeof prop === "string" && !(prop in target)) {
62
- target.delete(prop)
63
- return true
64
- }
65
- return Reflect.deleteProperty(target, prop)
66
- },
67
- ownKeys: target => {
68
- return target.keys()
69
- },
70
- getOwnPropertyDescriptor: (target, prop) => {
71
- if (typeof prop === "string" && target.has(prop)) {
72
- return {
73
- configurable: true,
74
- enumerable: true,
75
- value: target.get(prop),
76
- }
77
- }
78
- return Reflect.getOwnPropertyDescriptor(target, prop)
79
- },
80
- })
81
- }
82
-
83
41
  protected get shape(): RecordContainerShape<NestedShape> {
84
42
  return super.shape as RecordContainerShape<NestedShape>
85
43
  }
@@ -105,19 +63,20 @@ export class RecordDraftNode<
105
63
  key: string,
106
64
  shape: S,
107
65
  ): DraftNodeParams<ContainerShape> {
108
- const emptyState = (this.emptyState as any)?.[key]
66
+ const placeholder = (this.placeholder as any)?.[key]
109
67
 
110
68
  const LoroContainer = containerConstructor[shape._type]
111
69
 
112
70
  return {
113
71
  shape,
114
- emptyState,
72
+ placeholder,
115
73
  getContainer: () =>
116
74
  this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
75
+ readonly: this.readonly,
117
76
  }
118
77
  }
119
78
 
120
- getOrCreateNode(key: string): InferDraftType<NestedShape> {
79
+ getOrCreateNode(key: string): any {
121
80
  let node = this.nodeCache.get(key)
122
81
  if (!node) {
123
82
  const shape = this.shape.shape
@@ -125,36 +84,38 @@ export class RecordDraftNode<
125
84
  node = createContainerDraftNode(
126
85
  this.getDraftNodeParams(key, shape as ContainerShape),
127
86
  )
87
+ // Cache container nodes
88
+ this.nodeCache.set(key, node)
128
89
  } else {
129
90
  // For value shapes, first try to get the value from the container
130
91
  const containerValue = this.container.get(key)
131
92
  if (containerValue !== undefined) {
132
93
  node = containerValue as Value
133
94
  } else {
134
- // Only fall back to empty state if the container doesn't have the value
135
- const emptyState = (this.emptyState as any)?.[key]
136
- // For records, empty state might not have the key, which is fine?
137
- // But if we are accessing it, maybe we expect it to exist or be created?
138
- // If it's a value type, we can't really "create" it without a value.
139
- // So if it's undefined in container and empty state, we return undefined?
140
- // But the return type expects Value.
141
- // Let's check MapDraftNode.
142
- // MapDraftNode throws "empty state required" if not found.
143
- // But for Record, keys are dynamic.
144
- if (emptyState === undefined) {
145
- // If it's a value type and not in container or empty state,
146
- // we should probably return undefined if the type allows it,
147
- // or maybe the default value for that type?
148
- // But we don't have a default value generator for shapes.
149
- // Actually Shape.plain.* factories have _plain and _draft which are defaults.
95
+ // Only fall back to placeholder if the container doesn't have the value
96
+ const placeholder = (this.placeholder as any)?.[key]
97
+ if (placeholder === undefined) {
98
+ // If it's a value type and not in container or placeholder,
99
+ // fallback to the default value from the shape
150
100
  node = (shape as any)._plain
151
101
  } else {
152
- node = emptyState as Value
102
+ node = placeholder as Value
153
103
  }
154
104
  }
105
+ // Only cache primitive values if NOT readonly
106
+ if (node !== undefined && !this.readonly) {
107
+ this.nodeCache.set(key, node)
108
+ }
155
109
  }
156
- if (node !== undefined) {
157
- this.nodeCache.set(key, node)
110
+ }
111
+
112
+ if (this.readonly && isContainerShape(this.shape.shape)) {
113
+ const shape = this.shape.shape as ContainerShape
114
+ if (shape._type === "counter") {
115
+ return (node as any).value
116
+ }
117
+ if (shape._type === "text") {
118
+ return (node as any).toString()
158
119
  }
159
120
  }
160
121
 
@@ -166,22 +127,21 @@ export class RecordDraftNode<
166
127
  }
167
128
 
168
129
  set(key: string, value: any): void {
130
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
169
131
  if (isValueShape(this.shape.shape)) {
170
132
  this.container.set(key, value)
171
- // Update cache if needed?
172
- // MapDraftNode updates container directly for values.
173
- // But we also cache values in nodeCache for consistency?
174
- // MapDraftNode doesn't cache values in propertyCache if they are set via setter?
175
- // Actually MapDraftNode setter:
176
- // set: isValueShape(shape) ? value => this.container.set(key, value) : undefined
177
- // It doesn't update propertyCache.
178
- // But getOrCreateNode checks propertyCache first.
179
- // So if we set it, we should probably update propertyCache or clear it for that key.
180
133
  this.nodeCache.set(key, value)
181
134
  } else {
182
135
  // For containers, we can't set them directly usually.
183
136
  // But if the user passes a plain object that matches the shape, maybe we should convert it?
184
- // But typically we modify the draft node.
137
+ if (value && typeof value === "object") {
138
+ const node = this.getOrCreateNode(key)
139
+
140
+ if (assignPlainValueToDraftNode(node, value)) {
141
+ return
142
+ }
143
+ }
144
+
185
145
  throw new Error(
186
146
  "Cannot set container directly, modify the draft node instead",
187
147
  )
@@ -189,10 +149,12 @@ export class RecordDraftNode<
189
149
  }
190
150
 
191
151
  setContainer<C extends Container>(key: string, container: C): C {
152
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
192
153
  return this.container.setContainer(key, container)
193
154
  }
194
155
 
195
156
  delete(key: string): void {
157
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
196
158
  this.container.delete(key)
197
159
  this.nodeCache.delete(key)
198
160
  }
@@ -13,6 +13,11 @@ import { CounterDraftNode } from "./counter.js"
13
13
  import { ListDraftNode } from "./list.js"
14
14
  import { MapDraftNode } from "./map.js"
15
15
  import { MovableListDraftNode } from "./movable-list.js"
16
+ import {
17
+ listProxyHandler,
18
+ movableListProxyHandler,
19
+ recordProxyHandler,
20
+ } from "./proxy-handlers.js"
16
21
  import { RecordDraftNode } from "./record.js"
17
22
  import { TextDraftNode } from "./text.js"
18
23
  import { TreeDraftNode } from "./tree.js"
@@ -32,16 +37,23 @@ export function createContainerDraftNode(
32
37
  params as DraftNodeParams<CounterContainerShape>,
33
38
  )
34
39
  case "list":
35
- return new ListDraftNode(params as DraftNodeParams<ListContainerShape>)
40
+ return new Proxy(
41
+ new ListDraftNode(params as DraftNodeParams<ListContainerShape>),
42
+ listProxyHandler,
43
+ )
36
44
  case "map":
37
45
  return new MapDraftNode(params as DraftNodeParams<MapContainerShape>)
38
46
  case "movableList":
39
- return new MovableListDraftNode(
40
- params as DraftNodeParams<MovableListContainerShape>,
47
+ return new Proxy(
48
+ new MovableListDraftNode(
49
+ params as DraftNodeParams<MovableListContainerShape>,
50
+ ),
51
+ movableListProxyHandler,
41
52
  )
42
53
  case "record":
43
- return new RecordDraftNode(
44
- params as DraftNodeParams<RecordContainerShape>,
54
+ return new Proxy(
55
+ new RecordDraftNode(params as DraftNodeParams<RecordContainerShape>),
56
+ recordProxyHandler,
45
57
  )
46
58
  case "text":
47
59
  return new TextDraftNode(params as DraftNodeParams<TextContainerShape>)
@@ -53,3 +65,32 @@ export function createContainerDraftNode(
53
65
  )
54
66
  }
55
67
  }
68
+
69
+ export function assignPlainValueToDraftNode(
70
+ node: DraftNode<any>,
71
+ value: any,
72
+ ): boolean {
73
+ const shapeType = (node as any).shape._type
74
+
75
+ if (shapeType === "map" || shapeType === "record") {
76
+ for (const k in value) {
77
+ ;(node as any)[k] = value[k]
78
+ }
79
+ return true
80
+ }
81
+
82
+ if (shapeType === "list" || shapeType === "movableList") {
83
+ if (Array.isArray(value)) {
84
+ const listNode = node as any
85
+ if (listNode.length > 0) {
86
+ listNode.delete(0, listNode.length)
87
+ }
88
+ for (const item of value) {
89
+ listNode.push(item)
90
+ }
91
+ return true
92
+ }
93
+ }
94
+
95
+ return false
96
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { Shape } from "./shape.js"
3
+ import { createTypedDoc } from "./typed-doc.js"
4
+
5
+ describe("Equality Check", () => {
6
+ const schema = Shape.doc({
7
+ counter: Shape.counter().placeholder(1),
8
+ })
9
+
10
+ it("should compare equal to plain object", () => {
11
+ const doc = createTypedDoc(schema)
12
+ expect(doc.value.counter).toEqual(1)
13
+ })
14
+
15
+ it("should compare equal using toJSON", () => {
16
+ const doc = createTypedDoc(schema)
17
+ expect(doc.toJSON()).toEqual({ counter: 1 })
18
+ })
19
+ })
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  // Main API exports
2
- export { createTypedDoc, TypedDoc } from "./change.js"
3
- export { mergeValue, overlayEmptyState } from "./overlay.js"
2
+
3
+ export {
4
+ derivePlaceholder,
5
+ deriveShapePlaceholder,
6
+ } from "./derive-placeholder.js"
7
+ export { mergeValue, overlayPlaceholder } from "./overlay.js"
4
8
  export type {
5
9
  ArrayValueShape,
6
10
  ContainerOrValueShape,
@@ -23,16 +27,19 @@ export type {
23
27
  UnionValueShape,
24
28
  // Value shapes
25
29
  ValueShape,
26
- // ...
30
+ // WithPlaceholder type for shapes that support .placeholder()
31
+ WithPlaceholder,
27
32
  } from "./shape.js"
28
33
  // Schema and type exports
29
34
  export { Shape } from "./shape.js"
35
+ export { createTypedDoc, TypedDoc } from "./typed-doc.js"
30
36
  export type {
37
+ DeepReadonly,
31
38
  Draft,
39
+ // Type inference - Infer<T> is the recommended unified helper
40
+ Infer,
32
41
  InferDraftType,
33
- // Type inference
34
- InferEmptyStateType,
35
- InferPlainType,
42
+ InferPlaceholderType,
36
43
  } from "./types.js"
37
44
  // Utility exports
38
- export { validateEmptyState } from "./validation.js"
45
+ export { validatePlaceholder } from "./validation.js"