@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.
@@ -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
+ }