@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.
- package/README.md +26 -31
- package/dist/index.d.ts +115 -53
- package/dist/index.js +743 -393
- 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 +74 -1
- 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 +14 -7
- package/src/json-patch.test.ts +33 -167
- package/src/overlay.ts +46 -39
- package/src/readonly.test.ts +92 -0
- package/src/shape.ts +131 -73
- package/src/{change.ts → typed-doc.ts} +50 -28
- package/src/types.test.ts +188 -0
- package/src/types.ts +37 -5
- package/src/utils/type-guards.ts +1 -0
- package/src/validation.ts +45 -12
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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):
|
|
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
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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 =
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/draft-nodes/utils.ts
CHANGED
|
@@ -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
|
|
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
|
|
40
|
-
|
|
47
|
+
return new Proxy(
|
|
48
|
+
new MovableListDraftNode(
|
|
49
|
+
params as DraftNodeParams<MovableListContainerShape>,
|
|
50
|
+
),
|
|
51
|
+
movableListProxyHandler,
|
|
41
52
|
)
|
|
42
53
|
case "record":
|
|
43
|
-
return new
|
|
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
|
-
|
|
3
|
-
export {
|
|
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
|
-
|
|
34
|
-
InferEmptyStateType,
|
|
35
|
-
InferPlainType,
|
|
42
|
+
InferPlaceholderType,
|
|
36
43
|
} from "./types.js"
|
|
37
44
|
// Utility exports
|
|
38
|
-
export {
|
|
45
|
+
export { validatePlaceholder } from "./validation.js"
|