@loro-extended/change 0.2.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/LICENSE +21 -0
- package/README.md +565 -0
- package/dist/index.d.ts +339 -0
- package/dist/index.js +1491 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/change.test.ts +2006 -0
- package/src/change.ts +105 -0
- package/src/conversion.test.ts +728 -0
- package/src/conversion.ts +220 -0
- package/src/draft-nodes/base.ts +34 -0
- package/src/draft-nodes/counter.ts +21 -0
- package/src/draft-nodes/doc.ts +81 -0
- package/src/draft-nodes/list-base.ts +326 -0
- package/src/draft-nodes/list.ts +18 -0
- package/src/draft-nodes/map.ts +156 -0
- package/src/draft-nodes/movable-list.ts +26 -0
- package/src/draft-nodes/record.ts +215 -0
- package/src/draft-nodes/text.ts +48 -0
- package/src/draft-nodes/tree.ts +31 -0
- package/src/draft-nodes/utils.ts +55 -0
- package/src/index.ts +33 -0
- package/src/json-patch.test.ts +697 -0
- package/src/json-patch.ts +391 -0
- package/src/overlay.ts +90 -0
- package/src/record.test.ts +188 -0
- package/src/schema.fixtures.ts +138 -0
- package/src/shape.ts +348 -0
- package/src/types.ts +15 -0
- package/src/utils/type-guards.ts +210 -0
- package/src/validation.ts +261 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Shape } from "./shape.js"
|
|
2
|
+
|
|
3
|
+
const crdt = Shape
|
|
4
|
+
const value = Shape.plain
|
|
5
|
+
|
|
6
|
+
// Pattern 1: List with POJO objects (leaf nodes)
|
|
7
|
+
export const simpleList = crdt.list(value.object({ title: value.string() }))
|
|
8
|
+
|
|
9
|
+
// Pattern 2: List with LoroMap containers
|
|
10
|
+
export const containerListDoc = Shape.doc({
|
|
11
|
+
title: crdt.text(),
|
|
12
|
+
list: crdt.list(
|
|
13
|
+
crdt.map({
|
|
14
|
+
title: value.string(),
|
|
15
|
+
tags: value.array(value.string()),
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Pattern 3: Fully nested containers
|
|
21
|
+
export const deeplyNested = crdt.list(
|
|
22
|
+
crdt.map({
|
|
23
|
+
title: value.string(),
|
|
24
|
+
tags: crdt.list(value.string()), // LoroList of strings, not array
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// Example: Complex document schema with deeply nested Loro and POJO types
|
|
29
|
+
export const complexDocSchema = Shape.doc({
|
|
30
|
+
// Simple Loro containers
|
|
31
|
+
title: crdt.text(),
|
|
32
|
+
viewCount: crdt.counter(),
|
|
33
|
+
|
|
34
|
+
// Mixed content: LoroList containing POJO objects
|
|
35
|
+
articles: crdt.list(
|
|
36
|
+
value.object({
|
|
37
|
+
id: value.string(),
|
|
38
|
+
title: value.string(),
|
|
39
|
+
publishedAt: value.string(),
|
|
40
|
+
tags: value.array(value.string()), // POJO array (leaf node)
|
|
41
|
+
metadata: value.object({
|
|
42
|
+
wordCount: value.number(),
|
|
43
|
+
readingTime: value.number(),
|
|
44
|
+
featured: value.boolean(),
|
|
45
|
+
}),
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
// LoroMovableList for reorderable content
|
|
50
|
+
priorityTasks: crdt.movableList(
|
|
51
|
+
value.object({
|
|
52
|
+
id: value.string(),
|
|
53
|
+
title: value.string(),
|
|
54
|
+
priority: value.number(),
|
|
55
|
+
completed: value.boolean(),
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
|
|
59
|
+
// Deeply nested: LoroList containing LoroMap containers
|
|
60
|
+
collaborativeArticles: crdt.list(
|
|
61
|
+
crdt.map({
|
|
62
|
+
// Each article is a LoroMap with mixed content
|
|
63
|
+
title: crdt.text(), // Collaborative text editing
|
|
64
|
+
content: crdt.text(), // Collaborative content editing
|
|
65
|
+
|
|
66
|
+
// POJO metadata (leaf nodes)
|
|
67
|
+
publishedAt: value.string(),
|
|
68
|
+
authorId: value.string(),
|
|
69
|
+
|
|
70
|
+
// Nested LoroMovableList for reorderable collaborative tag management
|
|
71
|
+
tags: crdt.movableList(value.string()),
|
|
72
|
+
|
|
73
|
+
// Even deeper nesting: LoroList of LoroMap for comments
|
|
74
|
+
comments: crdt.list(
|
|
75
|
+
crdt.map({
|
|
76
|
+
id: value.string(), // POJO leaf
|
|
77
|
+
authorId: value.string(), // POJO leaf
|
|
78
|
+
content: crdt.text(), // Collaborative comment editing
|
|
79
|
+
timestamp: value.string(), // POJO leaf
|
|
80
|
+
|
|
81
|
+
// Nested replies as LoroMovableList of POJO objects
|
|
82
|
+
replies: crdt.movableList(
|
|
83
|
+
value.object({
|
|
84
|
+
id: value.string(),
|
|
85
|
+
authorId: value.string(),
|
|
86
|
+
content: value.string(), // Non-collaborative reply content
|
|
87
|
+
timestamp: value.string(),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
|
|
95
|
+
// Complex metadata structure
|
|
96
|
+
siteMetadata: crdt.map({
|
|
97
|
+
// POJO configuration
|
|
98
|
+
config: value.object({
|
|
99
|
+
siteName: value.string(),
|
|
100
|
+
baseUrl: value.string(),
|
|
101
|
+
theme: value.string(),
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
// Collaborative analytics
|
|
105
|
+
analytics: crdt.map({
|
|
106
|
+
totalViews: crdt.counter(),
|
|
107
|
+
uniqueVisitors: crdt.counter(),
|
|
108
|
+
|
|
109
|
+
// Daily stats as LoroMovableList of POJO objects (reorderable by date)
|
|
110
|
+
dailyStats: crdt.movableList(
|
|
111
|
+
value.object({
|
|
112
|
+
date: value.string(),
|
|
113
|
+
views: value.number(),
|
|
114
|
+
visitors: value.number(),
|
|
115
|
+
bounceRate: value.number(),
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
}),
|
|
119
|
+
|
|
120
|
+
// Collaborative feature flags
|
|
121
|
+
features: crdt.map({
|
|
122
|
+
commentsEnabled: value.boolean(),
|
|
123
|
+
darkModeEnabled: value.boolean(),
|
|
124
|
+
|
|
125
|
+
// Nested collaborative settings
|
|
126
|
+
moderationSettings: crdt.map({
|
|
127
|
+
autoModeration: value.boolean(),
|
|
128
|
+
bannedWords: crdt.movableList(value.string()), // Reorderable banned words
|
|
129
|
+
moderators: crdt.list(
|
|
130
|
+
value.object({
|
|
131
|
+
userId: value.string(),
|
|
132
|
+
rules: value.array(value.string()),
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
})
|
package/src/shape.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: required
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
LoroCounter,
|
|
5
|
+
LoroList,
|
|
6
|
+
LoroMap,
|
|
7
|
+
LoroMovableList,
|
|
8
|
+
LoroText,
|
|
9
|
+
LoroTree,
|
|
10
|
+
} from "loro-crdt"
|
|
11
|
+
|
|
12
|
+
import type { CounterDraftNode } from "./draft-nodes/counter.js"
|
|
13
|
+
import type { ListDraftNode } from "./draft-nodes/list.js"
|
|
14
|
+
import type { MapDraftNode } from "./draft-nodes/map.js"
|
|
15
|
+
import type { MovableListDraftNode } from "./draft-nodes/movable-list.js"
|
|
16
|
+
import type { RecordDraftNode } from "./draft-nodes/record.js"
|
|
17
|
+
import type { TextDraftNode } from "./draft-nodes/text.js"
|
|
18
|
+
|
|
19
|
+
export interface Shape<Plain, Draft> {
|
|
20
|
+
readonly _type: string
|
|
21
|
+
readonly _plain: Plain
|
|
22
|
+
readonly _draft: Draft
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DocShape<
|
|
26
|
+
NestedShapes extends Record<string, ContainerShape> = Record<
|
|
27
|
+
string,
|
|
28
|
+
ContainerShape
|
|
29
|
+
>,
|
|
30
|
+
> extends Shape<
|
|
31
|
+
{ [K in keyof NestedShapes]: NestedShapes[K]["_plain"] },
|
|
32
|
+
{ [K in keyof NestedShapes]: NestedShapes[K]["_draft"] }
|
|
33
|
+
> {
|
|
34
|
+
readonly _type: "doc"
|
|
35
|
+
// A doc's root containers each separately has its own shape, hence 'shapes'
|
|
36
|
+
readonly shapes: NestedShapes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TextContainerShape extends Shape<string, TextDraftNode> {
|
|
40
|
+
readonly _type: "text"
|
|
41
|
+
}
|
|
42
|
+
export interface CounterContainerShape extends Shape<number, CounterDraftNode> {
|
|
43
|
+
readonly _type: "counter"
|
|
44
|
+
}
|
|
45
|
+
export interface TreeContainerShape<NestedShape = ContainerOrValueShape>
|
|
46
|
+
extends Shape<any, any> {
|
|
47
|
+
readonly _type: "tree"
|
|
48
|
+
// TODO(duane): What does a tree contain? One type, or many?
|
|
49
|
+
readonly shape: NestedShape
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Container schemas using interfaces for recursive references
|
|
53
|
+
export interface ListContainerShape<
|
|
54
|
+
NestedShape extends ContainerOrValueShape = ContainerOrValueShape,
|
|
55
|
+
> extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape>> {
|
|
56
|
+
readonly _type: "list"
|
|
57
|
+
// A list contains many elements, all of the same 'shape'
|
|
58
|
+
readonly shape: NestedShape
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MovableListContainerShape<
|
|
62
|
+
NestedShape extends ContainerOrValueShape = ContainerOrValueShape,
|
|
63
|
+
> extends Shape<NestedShape["_plain"][], MovableListDraftNode<NestedShape>> {
|
|
64
|
+
readonly _type: "movableList"
|
|
65
|
+
// A list contains many elements, all of the same 'shape'
|
|
66
|
+
readonly shape: NestedShape
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface MapContainerShape<
|
|
70
|
+
NestedShapes extends Record<string, ContainerOrValueShape> = Record<
|
|
71
|
+
string,
|
|
72
|
+
ContainerOrValueShape
|
|
73
|
+
>,
|
|
74
|
+
> extends Shape<
|
|
75
|
+
{ [K in keyof NestedShapes]: NestedShapes[K]["_plain"] },
|
|
76
|
+
MapDraftNode<NestedShapes> & {
|
|
77
|
+
[K in keyof NestedShapes]: NestedShapes[K]["_draft"]
|
|
78
|
+
}
|
|
79
|
+
> {
|
|
80
|
+
readonly _type: "map"
|
|
81
|
+
// Each map property has its own shape, hence 'shapes'
|
|
82
|
+
readonly shapes: NestedShapes
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RecordContainerShape<
|
|
86
|
+
NestedShape extends ContainerOrValueShape = ContainerOrValueShape,
|
|
87
|
+
> extends Shape<
|
|
88
|
+
Record<string, NestedShape["_plain"]>,
|
|
89
|
+
RecordDraftNode<NestedShape>
|
|
90
|
+
> {
|
|
91
|
+
readonly _type: "record"
|
|
92
|
+
readonly shape: NestedShape
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type ContainerShape =
|
|
96
|
+
| CounterContainerShape
|
|
97
|
+
| ListContainerShape
|
|
98
|
+
| MapContainerShape
|
|
99
|
+
| MovableListContainerShape
|
|
100
|
+
| RecordContainerShape
|
|
101
|
+
| TextContainerShape
|
|
102
|
+
| TreeContainerShape
|
|
103
|
+
|
|
104
|
+
export type ContainerType = ContainerShape["_type"]
|
|
105
|
+
|
|
106
|
+
// LoroValue shape types - a shape for each of Loro's Value types
|
|
107
|
+
export interface StringValueShape extends Shape<string, string> {
|
|
108
|
+
readonly _type: "value"
|
|
109
|
+
readonly valueType: "string"
|
|
110
|
+
}
|
|
111
|
+
export interface NumberValueShape extends Shape<number, number> {
|
|
112
|
+
readonly _type: "value"
|
|
113
|
+
readonly valueType: "number"
|
|
114
|
+
}
|
|
115
|
+
export interface BooleanValueShape extends Shape<boolean, boolean> {
|
|
116
|
+
readonly _type: "value"
|
|
117
|
+
readonly valueType: "boolean"
|
|
118
|
+
}
|
|
119
|
+
export interface NullValueShape extends Shape<null, null> {
|
|
120
|
+
readonly _type: "value"
|
|
121
|
+
readonly valueType: "null"
|
|
122
|
+
}
|
|
123
|
+
export interface UndefinedValueShape extends Shape<undefined, undefined> {
|
|
124
|
+
readonly _type: "value"
|
|
125
|
+
readonly valueType: "undefined"
|
|
126
|
+
}
|
|
127
|
+
export interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array> {
|
|
128
|
+
readonly _type: "value"
|
|
129
|
+
readonly valueType: "uint8array"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ObjectValueShape<
|
|
133
|
+
T extends Record<string, ValueShape> = Record<string, ValueShape>,
|
|
134
|
+
> extends Shape<
|
|
135
|
+
{ [K in keyof T]: T[K]["_plain"] },
|
|
136
|
+
{ [K in keyof T]: T[K]["_draft"] }
|
|
137
|
+
> {
|
|
138
|
+
readonly _type: "value"
|
|
139
|
+
readonly valueType: "object"
|
|
140
|
+
readonly shape: T
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface RecordValueShape<T extends ValueShape = ValueShape>
|
|
144
|
+
extends Shape<Record<string, T["_plain"]>, Record<string, T["_draft"]>> {
|
|
145
|
+
readonly _type: "value"
|
|
146
|
+
readonly valueType: "record"
|
|
147
|
+
readonly shape: T
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ArrayValueShape<T extends ValueShape = ValueShape>
|
|
151
|
+
extends Shape<T["_plain"][], T["_draft"][]> {
|
|
152
|
+
readonly _type: "value"
|
|
153
|
+
readonly valueType: "array"
|
|
154
|
+
readonly shape: T
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface UnionValueShape<T extends ValueShape[] = ValueShape[]>
|
|
158
|
+
extends Shape<T[number]["_plain"], T[number]["_draft"]> {
|
|
159
|
+
readonly _type: "value"
|
|
160
|
+
readonly valueType: "union"
|
|
161
|
+
readonly shapes: T
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Union of all ValueShapes - these can only contain other ValueShapes, not ContainerShapes
|
|
165
|
+
export type ValueShape =
|
|
166
|
+
| StringValueShape
|
|
167
|
+
| NumberValueShape
|
|
168
|
+
| BooleanValueShape
|
|
169
|
+
| NullValueShape
|
|
170
|
+
| UndefinedValueShape
|
|
171
|
+
| Uint8ArrayValueShape
|
|
172
|
+
| ObjectValueShape
|
|
173
|
+
| RecordValueShape
|
|
174
|
+
| ArrayValueShape
|
|
175
|
+
| UnionValueShape
|
|
176
|
+
|
|
177
|
+
export type ContainerOrValueShape = ContainerShape | ValueShape
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The LoroShape factory object
|
|
181
|
+
*
|
|
182
|
+
* If a container has a `shape` type variable, it refers to the shape it contains--
|
|
183
|
+
* so for example, a `LoroShape.list(LoroShape.text())` would return a value of type
|
|
184
|
+
* `ListContainerShape<TextContainerShape>`.
|
|
185
|
+
*/
|
|
186
|
+
export const Shape = {
|
|
187
|
+
doc: <T extends Record<string, ContainerShape>>(shape: T): DocShape<T> => ({
|
|
188
|
+
_type: "doc" as const,
|
|
189
|
+
shapes: shape,
|
|
190
|
+
_plain: {} as any,
|
|
191
|
+
_draft: {} as any,
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
195
|
+
// various CRDT algorithms
|
|
196
|
+
counter: (): CounterContainerShape => ({
|
|
197
|
+
_type: "counter" as const,
|
|
198
|
+
_plain: 0,
|
|
199
|
+
_draft: {} as CounterDraftNode,
|
|
200
|
+
}),
|
|
201
|
+
|
|
202
|
+
list: <T extends ContainerOrValueShape>(shape: T): ListContainerShape<T> => ({
|
|
203
|
+
_type: "list" as const,
|
|
204
|
+
shape,
|
|
205
|
+
_plain: [] as any,
|
|
206
|
+
_draft: {} as any,
|
|
207
|
+
}),
|
|
208
|
+
|
|
209
|
+
map: <T extends Record<string, ContainerOrValueShape>>(
|
|
210
|
+
shape: T,
|
|
211
|
+
): MapContainerShape<T> => ({
|
|
212
|
+
_type: "map" as const,
|
|
213
|
+
shapes: shape,
|
|
214
|
+
_plain: {} as any,
|
|
215
|
+
_draft: {} as any,
|
|
216
|
+
}),
|
|
217
|
+
|
|
218
|
+
record: <T extends ContainerOrValueShape>(
|
|
219
|
+
shape: T,
|
|
220
|
+
): RecordContainerShape<T> => ({
|
|
221
|
+
_type: "record" as const,
|
|
222
|
+
shape,
|
|
223
|
+
_plain: {} as any,
|
|
224
|
+
_draft: {} as any,
|
|
225
|
+
}),
|
|
226
|
+
|
|
227
|
+
movableList: <T extends ContainerOrValueShape>(
|
|
228
|
+
shape: T,
|
|
229
|
+
): MovableListContainerShape<T> => ({
|
|
230
|
+
_type: "movableList" as const,
|
|
231
|
+
shape,
|
|
232
|
+
_plain: [] as any,
|
|
233
|
+
_draft: {} as any,
|
|
234
|
+
}),
|
|
235
|
+
|
|
236
|
+
text: (): TextContainerShape => ({
|
|
237
|
+
_type: "text" as const,
|
|
238
|
+
_plain: "",
|
|
239
|
+
_draft: {} as TextDraftNode,
|
|
240
|
+
}),
|
|
241
|
+
|
|
242
|
+
tree: <T extends MapContainerShape>(shape: T): TreeContainerShape => ({
|
|
243
|
+
_type: "tree" as const,
|
|
244
|
+
shape,
|
|
245
|
+
_plain: {} as any,
|
|
246
|
+
_draft: {} as any,
|
|
247
|
+
}),
|
|
248
|
+
|
|
249
|
+
// Values are represented as plain JS objects, with the limitation that they MUST be
|
|
250
|
+
// representable as a Loro "Value"--basically JSON. The behavior of a Value is basically
|
|
251
|
+
// "Last Write Wins", meaning there is no subtle convergent behavior here, just taking
|
|
252
|
+
// the most recent value based on the current available information.
|
|
253
|
+
plain: {
|
|
254
|
+
string: (): StringValueShape => ({
|
|
255
|
+
_type: "value" as const,
|
|
256
|
+
valueType: "string" as const,
|
|
257
|
+
_plain: "",
|
|
258
|
+
_draft: "",
|
|
259
|
+
}),
|
|
260
|
+
|
|
261
|
+
number: (): NumberValueShape => ({
|
|
262
|
+
_type: "value" as const,
|
|
263
|
+
valueType: "number" as const,
|
|
264
|
+
_plain: 0,
|
|
265
|
+
_draft: 0,
|
|
266
|
+
}),
|
|
267
|
+
|
|
268
|
+
boolean: (): BooleanValueShape => ({
|
|
269
|
+
_type: "value" as const,
|
|
270
|
+
valueType: "boolean" as const,
|
|
271
|
+
_plain: false,
|
|
272
|
+
_draft: false,
|
|
273
|
+
}),
|
|
274
|
+
|
|
275
|
+
null: (): NullValueShape => ({
|
|
276
|
+
_type: "value" as const,
|
|
277
|
+
valueType: "null" as const,
|
|
278
|
+
_plain: null,
|
|
279
|
+
_draft: null,
|
|
280
|
+
}),
|
|
281
|
+
|
|
282
|
+
undefined: (): UndefinedValueShape => ({
|
|
283
|
+
_type: "value" as const,
|
|
284
|
+
valueType: "undefined" as const,
|
|
285
|
+
_plain: undefined,
|
|
286
|
+
_draft: undefined,
|
|
287
|
+
}),
|
|
288
|
+
|
|
289
|
+
uint8Array: (): Uint8ArrayValueShape => ({
|
|
290
|
+
_type: "value" as const,
|
|
291
|
+
valueType: "uint8array" as const,
|
|
292
|
+
_plain: new Uint8Array(),
|
|
293
|
+
_draft: new Uint8Array(),
|
|
294
|
+
}),
|
|
295
|
+
|
|
296
|
+
object: <T extends Record<string, ValueShape>>(
|
|
297
|
+
shape: T,
|
|
298
|
+
): ObjectValueShape<T> => ({
|
|
299
|
+
_type: "value" as const,
|
|
300
|
+
valueType: "object" as const,
|
|
301
|
+
shape,
|
|
302
|
+
_plain: {} as any,
|
|
303
|
+
_draft: {} as any,
|
|
304
|
+
}),
|
|
305
|
+
|
|
306
|
+
record: <T extends ValueShape>(shape: T): RecordValueShape<T> => ({
|
|
307
|
+
_type: "value" as const,
|
|
308
|
+
valueType: "record" as const,
|
|
309
|
+
shape,
|
|
310
|
+
_plain: {} as any,
|
|
311
|
+
_draft: {} as any,
|
|
312
|
+
}),
|
|
313
|
+
|
|
314
|
+
array: <T extends ValueShape>(shape: T): ArrayValueShape<T> => ({
|
|
315
|
+
_type: "value" as const,
|
|
316
|
+
valueType: "array" as const,
|
|
317
|
+
shape,
|
|
318
|
+
_plain: [] as any,
|
|
319
|
+
_draft: [] as any,
|
|
320
|
+
}),
|
|
321
|
+
|
|
322
|
+
// Special value type that helps make things like `string | null` representable
|
|
323
|
+
// TODO(duane): should this be a more general type for containers too?
|
|
324
|
+
union: <T extends ValueShape[]>(shapes: T): UnionValueShape<T> => ({
|
|
325
|
+
_type: "value" as const,
|
|
326
|
+
valueType: "union" as const,
|
|
327
|
+
shapes,
|
|
328
|
+
_plain: {} as any,
|
|
329
|
+
_draft: {} as any,
|
|
330
|
+
}),
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Add this type mapping near the top of your file, after the imports
|
|
335
|
+
export type ShapeToContainer<T extends DocShape | ContainerShape> =
|
|
336
|
+
T extends TextContainerShape
|
|
337
|
+
? LoroText
|
|
338
|
+
: T extends CounterContainerShape
|
|
339
|
+
? LoroCounter
|
|
340
|
+
: T extends ListContainerShape
|
|
341
|
+
? LoroList
|
|
342
|
+
: T extends MovableListContainerShape
|
|
343
|
+
? LoroMovableList
|
|
344
|
+
: T extends MapContainerShape | RecordContainerShape
|
|
345
|
+
? LoroMap
|
|
346
|
+
: T extends TreeContainerShape
|
|
347
|
+
? LoroTree
|
|
348
|
+
: never // not a container
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
* UNIFIED BASE SCHEMA MAPPER SYSTEM
|
|
3
|
+
* =============================================================================
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ContainerShape, DocShape, Shape } from "./shape.js"
|
|
7
|
+
|
|
8
|
+
// Input type inference - what developers can pass to push/insert methods
|
|
9
|
+
export type InferPlainType<T> = T extends Shape<infer P, any> ? P : never
|
|
10
|
+
|
|
11
|
+
export type InferDraftType<T> = T extends Shape<any, infer D> ? D : never
|
|
12
|
+
|
|
13
|
+
// Draft-specific type inference that properly handles the draft context
|
|
14
|
+
export type Draft<T extends DocShape<Record<string, ContainerShape>>> =
|
|
15
|
+
InferDraftType<T>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Container,
|
|
3
|
+
LoroCounter,
|
|
4
|
+
LoroList,
|
|
5
|
+
LoroMap,
|
|
6
|
+
LoroMovableList,
|
|
7
|
+
LoroText,
|
|
8
|
+
LoroTree,
|
|
9
|
+
LoroTreeNode,
|
|
10
|
+
Value,
|
|
11
|
+
} from "loro-crdt"
|
|
12
|
+
import type {
|
|
13
|
+
ContainerOrValueShape,
|
|
14
|
+
ContainerShape,
|
|
15
|
+
CounterContainerShape,
|
|
16
|
+
ListContainerShape,
|
|
17
|
+
MapContainerShape,
|
|
18
|
+
MovableListContainerShape,
|
|
19
|
+
RecordContainerShape,
|
|
20
|
+
TextContainerShape,
|
|
21
|
+
TreeContainerShape,
|
|
22
|
+
ValueShape,
|
|
23
|
+
} from "../shape.js"
|
|
24
|
+
|
|
25
|
+
export { isContainer, isContainerId } from "loro-crdt"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Type guard to check if a container is a LoroCounter
|
|
29
|
+
*/
|
|
30
|
+
export function isLoroCounter(container: Container): container is LoroCounter {
|
|
31
|
+
return container.kind() === "Counter"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Type guard to check if a container is a LoroList
|
|
36
|
+
*/
|
|
37
|
+
export function isLoroList(container: Container): container is LoroList {
|
|
38
|
+
return container.kind() === "List"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Type guard to check if a container is a LoroMap
|
|
43
|
+
*/
|
|
44
|
+
export function isLoroMap(container: Container): container is LoroMap {
|
|
45
|
+
return container.kind() === "Map"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Type guard to check if a container is a LoroMovableList
|
|
50
|
+
*/
|
|
51
|
+
export function isLoroMovableList(
|
|
52
|
+
container: Container,
|
|
53
|
+
): container is LoroMovableList {
|
|
54
|
+
return container.kind() === "MovableList"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Type guard to check if a container is a LoroText
|
|
59
|
+
*/
|
|
60
|
+
export function isLoroText(container: Container): container is LoroText {
|
|
61
|
+
return container.kind() === "Text"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Type guard to check if a container is a LoroTree
|
|
66
|
+
*/
|
|
67
|
+
export function isLoroTree(container: Container): container is LoroTree {
|
|
68
|
+
return container.kind() === "Tree"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Type guard to check if an object is a LoroTreeNode
|
|
73
|
+
* Note: LoroTreeNode is not a Container, so we check for its specific properties
|
|
74
|
+
*/
|
|
75
|
+
export function isLoroTreeNode(obj: any): obj is LoroTreeNode {
|
|
76
|
+
return (
|
|
77
|
+
obj &&
|
|
78
|
+
typeof obj === "object" &&
|
|
79
|
+
typeof obj.id === "string" &&
|
|
80
|
+
typeof obj.data === "object" &&
|
|
81
|
+
typeof obj.parent === "function" &&
|
|
82
|
+
typeof obj.children === "function" &&
|
|
83
|
+
typeof obj.createNode === "function"
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Type guard to ensure cached container matches expected type using kind() method
|
|
89
|
+
*/
|
|
90
|
+
export function assertContainerType<T extends Container>(
|
|
91
|
+
cached: Container,
|
|
92
|
+
expected: T,
|
|
93
|
+
context: string = "container operation",
|
|
94
|
+
): asserts cached is T {
|
|
95
|
+
if (cached.kind() !== expected.kind()) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Type safety violation in ${context}: ` +
|
|
98
|
+
`cached container kind '${cached.kind()}' does not match ` +
|
|
99
|
+
`expected kind '${expected.kind()}'`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Additional safety check: ensure IDs match
|
|
104
|
+
if (cached.id !== expected.id) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Container ID mismatch in ${context}: ` +
|
|
107
|
+
`cached ID '${cached.id}' does not match expected ID '${expected.id}'`,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Type guard to check if a schema is for TextDraftNode
|
|
114
|
+
*/
|
|
115
|
+
export function isTextShape(
|
|
116
|
+
schema: ContainerOrValueShape,
|
|
117
|
+
): schema is TextContainerShape {
|
|
118
|
+
return schema && typeof schema === "object" && schema._type === "text"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Type guard to check if a schema is for CounterDraftNode
|
|
123
|
+
*/
|
|
124
|
+
export function isCounterShape(
|
|
125
|
+
schema: ContainerOrValueShape,
|
|
126
|
+
): schema is CounterContainerShape {
|
|
127
|
+
return schema && typeof schema === "object" && schema._type === "counter"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Type guard to check if a schema is for ListDraftNode
|
|
132
|
+
*/
|
|
133
|
+
export function isListShape(
|
|
134
|
+
schema: ContainerOrValueShape,
|
|
135
|
+
): schema is ListContainerShape {
|
|
136
|
+
return schema && typeof schema === "object" && schema._type === "list"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Type guard to check if a schema is for MovableListDraftNode
|
|
141
|
+
*/
|
|
142
|
+
export function isMovableListShape(
|
|
143
|
+
schema: ContainerOrValueShape,
|
|
144
|
+
): schema is MovableListContainerShape {
|
|
145
|
+
return schema && typeof schema === "object" && schema._type === "movableList"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Type guard to check if a schema is for MapDraftNode
|
|
150
|
+
*/
|
|
151
|
+
export function isMapShape(
|
|
152
|
+
schema: ContainerOrValueShape,
|
|
153
|
+
): schema is MapContainerShape {
|
|
154
|
+
return schema && typeof schema === "object" && schema._type === "map"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Type guard to check if a schema is for RecordDraftNode
|
|
159
|
+
*/
|
|
160
|
+
export function isRecordShape(
|
|
161
|
+
schema: ContainerOrValueShape,
|
|
162
|
+
): schema is RecordContainerShape {
|
|
163
|
+
return schema && typeof schema === "object" && schema._type === "record"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Type guard to check if a schema is for TreeDraftNode
|
|
168
|
+
*/
|
|
169
|
+
export function isTreeShape(
|
|
170
|
+
schema: ContainerOrValueShape,
|
|
171
|
+
): schema is TreeContainerShape {
|
|
172
|
+
return schema && typeof schema === "object" && schema._type === "tree"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function isContainerShape(
|
|
176
|
+
schema: ContainerOrValueShape,
|
|
177
|
+
): schema is ContainerShape {
|
|
178
|
+
return schema._type && schema._type !== "value"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Type guard to check if a schema is any of the Value shapes
|
|
183
|
+
*/
|
|
184
|
+
export function isValueShape(
|
|
185
|
+
schema: ContainerOrValueShape,
|
|
186
|
+
): schema is ValueShape {
|
|
187
|
+
return (
|
|
188
|
+
schema._type === "value" &&
|
|
189
|
+
[
|
|
190
|
+
"string",
|
|
191
|
+
"number",
|
|
192
|
+
"boolean",
|
|
193
|
+
"null",
|
|
194
|
+
"undefined",
|
|
195
|
+
"uint8array",
|
|
196
|
+
"object",
|
|
197
|
+
"record",
|
|
198
|
+
"array",
|
|
199
|
+
].includes(schema.valueType)
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function isObjectValue(value: Value): value is { [key: string]: Value } {
|
|
204
|
+
return (
|
|
205
|
+
typeof value === "object" &&
|
|
206
|
+
value !== null &&
|
|
207
|
+
!Array.isArray(value) &&
|
|
208
|
+
!(value instanceof Uint8Array)
|
|
209
|
+
)
|
|
210
|
+
}
|