@voidhash/mimic 0.0.4 → 0.0.5
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/.turbo/turbo-build.log +202 -198
- package/dist/Document.cjs +1 -2
- package/dist/Document.d.cts +9 -3
- package/dist/Document.d.cts.map +1 -1
- package/dist/Document.d.mts +9 -3
- package/dist/Document.d.mts.map +1 -1
- package/dist/Document.mjs +1 -2
- package/dist/Document.mjs.map +1 -1
- package/dist/Primitive.d.cts +2 -2
- package/dist/Primitive.d.mts +2 -2
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.cjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.mjs +15 -0
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.cjs +14 -0
- package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.mjs +13 -0
- package/dist/client/ClientDocument.cjs +17 -12
- package/dist/client/ClientDocument.d.mts.map +1 -1
- package/dist/client/ClientDocument.mjs +17 -12
- package/dist/client/ClientDocument.mjs.map +1 -1
- package/dist/client/WebSocketTransport.cjs +6 -6
- package/dist/client/WebSocketTransport.mjs +6 -6
- package/dist/client/WebSocketTransport.mjs.map +1 -1
- package/dist/primitives/Tree.cjs +55 -7
- package/dist/primitives/Tree.d.cts +97 -10
- package/dist/primitives/Tree.d.cts.map +1 -1
- package/dist/primitives/Tree.d.mts +97 -10
- package/dist/primitives/Tree.d.mts.map +1 -1
- package/dist/primitives/Tree.mjs +55 -7
- package/dist/primitives/Tree.mjs.map +1 -1
- package/dist/primitives/shared.d.cts +9 -0
- package/dist/primitives/shared.d.cts.map +1 -1
- package/dist/primitives/shared.d.mts +9 -0
- package/dist/primitives/shared.d.mts.map +1 -1
- package/dist/primitives/shared.mjs.map +1 -1
- package/dist/server/ServerDocument.cjs +1 -1
- package/dist/server/ServerDocument.d.cts +3 -3
- package/dist/server/ServerDocument.d.cts.map +1 -1
- package/dist/server/ServerDocument.d.mts +3 -3
- package/dist/server/ServerDocument.d.mts.map +1 -1
- package/dist/server/ServerDocument.mjs +1 -1
- package/dist/server/ServerDocument.mjs.map +1 -1
- package/package.json +2 -2
- package/src/Document.ts +18 -5
- package/src/client/ClientDocument.ts +20 -21
- package/src/client/WebSocketTransport.ts +9 -9
- package/src/primitives/Tree.ts +209 -19
- package/src/primitives/shared.ts +10 -1
- package/src/server/ServerDocument.ts +4 -3
- package/tests/client/ClientDocument.test.ts +309 -2
- package/tests/client/WebSocketTransport.test.ts +228 -3
- package/tests/primitives/Tree.test.ts +291 -17
- package/tests/server/ServerDocument.test.ts +1 -1
- package/tsconfig.json +1 -1
package/src/Document.ts
CHANGED
|
@@ -80,8 +80,14 @@ export interface Document<TSchema extends Primitive.AnyPrimitive> {
|
|
|
80
80
|
// =============================================================================
|
|
81
81
|
|
|
82
82
|
export interface DocumentOptions<TSchema extends Primitive.AnyPrimitive> {
|
|
83
|
-
/** Initial
|
|
84
|
-
readonly initial?: Primitive.
|
|
83
|
+
/** Initial value for the document (using set input format) */
|
|
84
|
+
readonly initial?: Primitive.InferSetInput<TSchema>;
|
|
85
|
+
/**
|
|
86
|
+
* Raw initial state for the document (already in internal state format).
|
|
87
|
+
* Use this when loading state from the server or storage.
|
|
88
|
+
* Takes precedence over `initial` if both are provided.
|
|
89
|
+
*/
|
|
90
|
+
readonly initialState?: Primitive.InferState<TSchema>;
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
// =============================================================================
|
|
@@ -95,9 +101,16 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
95
101
|
schema: TSchema,
|
|
96
102
|
options?: DocumentOptions<TSchema>
|
|
97
103
|
): Document<TSchema> => {
|
|
98
|
-
// Internal state
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
// Internal state - determine initial state based on options
|
|
105
|
+
// Priority: initialState (raw) > initial (needs conversion) > schema defaults
|
|
106
|
+
let _state: Primitive.InferState<TSchema> | undefined =
|
|
107
|
+
options?.initialState !== undefined
|
|
108
|
+
? options.initialState
|
|
109
|
+
: options?.initial !== undefined
|
|
110
|
+
? (schema._internal.convertSetInputToState
|
|
111
|
+
? schema._internal.convertSetInputToState(options.initial)
|
|
112
|
+
: options.initial as Primitive.InferState<TSchema>)
|
|
113
|
+
: schema._internal.getInitialState();
|
|
101
114
|
|
|
102
115
|
// Pending operations buffer (local changes not yet flushed)
|
|
103
116
|
let _pending: Operation.Operation<any, any, any>[] = [];
|
|
@@ -265,8 +265,8 @@ export const make = <
|
|
|
265
265
|
let _serverTransactionHistory: Transaction.Transaction[] = [];
|
|
266
266
|
const MAX_HISTORY_SIZE = 100;
|
|
267
267
|
|
|
268
|
-
// The underlying document for optimistic state
|
|
269
|
-
let _optimisticDoc = Document.make(schema, {
|
|
268
|
+
// The underlying document for optimistic state (use initialState for raw state format)
|
|
269
|
+
let _optimisticDoc = Document.make(schema, { initialState: _serverState });
|
|
270
270
|
|
|
271
271
|
// Subscription cleanup
|
|
272
272
|
let _unsubscribe: (() => void) | null = null;
|
|
@@ -467,8 +467,8 @@ export const make = <
|
|
|
467
467
|
serverState: _serverState,
|
|
468
468
|
});
|
|
469
469
|
|
|
470
|
-
// Create fresh document from server state
|
|
471
|
-
_optimisticDoc = Document.make(schema, {
|
|
470
|
+
// Create fresh document from server state (use initialState for raw state format)
|
|
471
|
+
_optimisticDoc = Document.make(schema, { initialState: _serverState });
|
|
472
472
|
|
|
473
473
|
// Apply all pending transactions
|
|
474
474
|
for (const pending of _pending) {
|
|
@@ -484,16 +484,14 @@ export const make = <
|
|
|
484
484
|
|
|
485
485
|
/**
|
|
486
486
|
* Adds a transaction to pending queue and sends to server.
|
|
487
|
+
* If disconnected, the transport will queue it for later submission.
|
|
487
488
|
*/
|
|
488
489
|
const submitTransaction = (tx: Transaction.Transaction): void => {
|
|
489
|
-
if (!transport.isConnected()) {
|
|
490
|
-
throw new NotConnectedError();
|
|
491
|
-
}
|
|
492
|
-
|
|
493
490
|
debugLog("submitTransaction", {
|
|
494
491
|
txId: tx.id,
|
|
495
492
|
ops: tx.ops,
|
|
496
493
|
pendingCount: _pending.length + 1,
|
|
494
|
+
isConnected: transport.isConnected(),
|
|
497
495
|
});
|
|
498
496
|
|
|
499
497
|
const pending: PendingTransaction = {
|
|
@@ -504,15 +502,18 @@ export const make = <
|
|
|
504
502
|
|
|
505
503
|
_pending.push(pending);
|
|
506
504
|
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
505
|
+
// Only set timeout if connected - otherwise the transport queues it
|
|
506
|
+
// and the timeout will start when the message is actually sent on reconnect
|
|
507
|
+
if (transport.isConnected()) {
|
|
508
|
+
const timeoutHandle = setTimeout(() => {
|
|
509
|
+
handleTransactionTimeout(tx.id);
|
|
510
|
+
}, transactionTimeout);
|
|
511
|
+
_timeoutHandles.set(tx.id, timeoutHandle);
|
|
512
|
+
}
|
|
512
513
|
|
|
513
|
-
// Send to server
|
|
514
|
+
// Send to server (transport queues if disconnected)
|
|
514
515
|
transport.send(tx);
|
|
515
|
-
debugLog("submitTransaction: sent to
|
|
516
|
+
debugLog("submitTransaction: sent to transport", { txId: tx.id, queued: !transport.isConnected() });
|
|
516
517
|
};
|
|
517
518
|
|
|
518
519
|
/**
|
|
@@ -585,7 +586,7 @@ export const make = <
|
|
|
585
586
|
_pending.splice(pendingIndex, 1);
|
|
586
587
|
|
|
587
588
|
// Apply to server state
|
|
588
|
-
const tempDoc = Document.make(schema, {
|
|
589
|
+
const tempDoc = Document.make(schema, { initialState: _serverState });
|
|
589
590
|
tempDoc.apply(serverTx.ops);
|
|
590
591
|
_serverState = tempDoc.get();
|
|
591
592
|
|
|
@@ -605,7 +606,7 @@ export const make = <
|
|
|
605
606
|
});
|
|
606
607
|
|
|
607
608
|
// Apply to server state
|
|
608
|
-
const tempDoc = Document.make(schema, {
|
|
609
|
+
const tempDoc = Document.make(schema, { initialState: _serverState });
|
|
609
610
|
tempDoc.apply(serverTx.ops);
|
|
610
611
|
_serverState = tempDoc.get();
|
|
611
612
|
|
|
@@ -937,10 +938,8 @@ export const make = <
|
|
|
937
938
|
pendingCount: _pending.length,
|
|
938
939
|
});
|
|
939
940
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
|
|
941
|
+
// Allow transactions even when disconnected - they will be queued
|
|
942
|
+
// Only require that we have been initialized at least once (have server state)
|
|
944
943
|
if (_initState.type !== "ready") {
|
|
945
944
|
throw new InvalidStateError("Client is not ready. Wait for initialization to complete.");
|
|
946
945
|
}
|
|
@@ -458,11 +458,10 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
|
|
|
458
458
|
|
|
459
459
|
if (_state.type === "connected") {
|
|
460
460
|
sendRaw(message);
|
|
461
|
-
} else
|
|
462
|
-
// Queue message for when we reconnect
|
|
461
|
+
} else {
|
|
462
|
+
// Queue message for when we reconnect (works for both reconnecting and disconnected states)
|
|
463
463
|
_messageQueue.push(message);
|
|
464
464
|
}
|
|
465
|
-
// If disconnected, silently drop (caller should check isConnected)
|
|
466
465
|
},
|
|
467
466
|
|
|
468
467
|
requestSnapshot: (): void => {
|
|
@@ -470,7 +469,8 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
|
|
|
470
469
|
|
|
471
470
|
if (_state.type === "connected") {
|
|
472
471
|
sendRaw(message);
|
|
473
|
-
} else
|
|
472
|
+
} else {
|
|
473
|
+
// Queue for later
|
|
474
474
|
_messageQueue.push(message);
|
|
475
475
|
}
|
|
476
476
|
},
|
|
@@ -546,9 +546,9 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
|
|
|
546
546
|
|
|
547
547
|
if (_state.type === "connected") {
|
|
548
548
|
sendRaw(message);
|
|
549
|
-
} else
|
|
550
|
-
// Remove all set messages from the message queue
|
|
551
|
-
_messageQueue = _messageQueue.filter((
|
|
549
|
+
} else {
|
|
550
|
+
// Remove all set messages from the message queue (only keep latest)
|
|
551
|
+
_messageQueue = _messageQueue.filter((m) => m.type !== "presence_set");
|
|
552
552
|
// Add the new presence set message to the queue
|
|
553
553
|
_messageQueue.push(message);
|
|
554
554
|
}
|
|
@@ -559,9 +559,9 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
|
|
|
559
559
|
|
|
560
560
|
if (_state.type === "connected") {
|
|
561
561
|
sendRaw(message);
|
|
562
|
-
} else
|
|
562
|
+
} else {
|
|
563
563
|
// Remove all clear messages from the message queue
|
|
564
|
-
_messageQueue = _messageQueue.filter((
|
|
564
|
+
_messageQueue = _messageQueue.filter((m) => m.type !== "presence_clear");
|
|
565
565
|
// Add the new presence clear message to the queue
|
|
566
566
|
_messageQueue.push(message);
|
|
567
567
|
}
|
package/src/primitives/Tree.ts
CHANGED
|
@@ -121,15 +121,128 @@ export type TreeNodeUpdateValue<TNode extends AnyTreeNodePrimitive> =
|
|
|
121
121
|
* Uses StructSetInput directly so that:
|
|
122
122
|
* - Fields that are required AND have no default must be provided
|
|
123
123
|
* - Fields that are optional OR have defaults can be omitted
|
|
124
|
-
*
|
|
124
|
+
*
|
|
125
125
|
* This bypasses the struct-level NeedsValue wrapper since tree inserts
|
|
126
126
|
* always require a data object (even if empty for all-optional fields).
|
|
127
127
|
*/
|
|
128
|
-
export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
|
|
128
|
+
export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
|
|
129
129
|
TNode["data"] extends StructPrimitive<infer TFields, any, any>
|
|
130
130
|
? StructSetInput<TFields>
|
|
131
131
|
: InferSetInput<TNode["data"]>;
|
|
132
132
|
|
|
133
|
+
// =============================================================================
|
|
134
|
+
// Nested Input Types for set() and default()
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Type guard to check if a type is `any` or unknown (structural check).
|
|
139
|
+
* Returns true if T is `any`, false otherwise.
|
|
140
|
+
*/
|
|
141
|
+
type IsAny<T> = 0 extends (1 & T) ? true : false;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get children types, with special handling for self-referential nodes.
|
|
145
|
+
* When InferTreeNodeChildren returns `any` (from TreeNodePrimitive<..., any>),
|
|
146
|
+
* we fall back to using TNode itself as its own child type.
|
|
147
|
+
* This handles the common case of self-referential nodes like:
|
|
148
|
+
* FolderNode = TreeNode("folder", { children: [TreeNodeSelf, FileNode] })
|
|
149
|
+
* Where FolderNode's TChildren is resolved to FolderNode | FileNode,
|
|
150
|
+
* but the self-referenced FolderNode has TChildren = any.
|
|
151
|
+
*/
|
|
152
|
+
type ResolveChildrenForInput<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
|
|
153
|
+
IsAny<InferTreeNodeChildren<TNode>> extends true
|
|
154
|
+
? TOriginalNode // Self-reference: use the original node's type
|
|
155
|
+
: InferTreeNodeChildren<TNode>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Helper type that creates a properly typed node input for a specific node type.
|
|
159
|
+
* This is the "strict" version that enforces exact property matching.
|
|
160
|
+
*
|
|
161
|
+
* The TOriginalNode parameter is used to track the original root node type
|
|
162
|
+
* for proper self-reference resolution in deeply nested structures.
|
|
163
|
+
*/
|
|
164
|
+
type TreeNodeSetInputStrictWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> = {
|
|
165
|
+
readonly type: InferTreeNodeType<TNode>;
|
|
166
|
+
readonly id?: string;
|
|
167
|
+
readonly children: TreeNodeSetInputUnionWithRoot<ResolveChildrenForInput<TNode, TOriginalNode>, TOriginalNode>[];
|
|
168
|
+
} & TreeNodeDataSetInput<TNode>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Distributive conditional type that creates a union of TreeNodeSetInputStrict
|
|
172
|
+
* for each node type in the union. This ensures proper type discrimination.
|
|
173
|
+
*
|
|
174
|
+
* When TNode is a union (e.g., FolderNode | FileNode), this distributes to:
|
|
175
|
+
* TreeNodeSetInputStrict<FolderNode> | TreeNodeSetInputStrict<FileNode>
|
|
176
|
+
*
|
|
177
|
+
* This creates a proper discriminated union where excess property checking works.
|
|
178
|
+
*/
|
|
179
|
+
type TreeNodeSetInputUnionWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
|
|
180
|
+
TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrictWithRoot<TNode, TOriginalNode> : never;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Helper type for single-parameter usage - uses TNode as its own original.
|
|
184
|
+
*/
|
|
185
|
+
type TreeNodeSetInputStrict<TNode extends AnyTreeNodePrimitive> =
|
|
186
|
+
TreeNodeSetInputStrictWithRoot<TNode, TNode>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Distributive conditional for single-parameter usage.
|
|
190
|
+
*/
|
|
191
|
+
export type TreeNodeSetInputUnion<TNode extends AnyTreeNodePrimitive> =
|
|
192
|
+
TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrict<TNode> : never;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Input type for a single node in a nested tree set/default operation.
|
|
196
|
+
*
|
|
197
|
+
* - `type` is REQUIRED - explicit type discriminator for the node
|
|
198
|
+
* - `id` is optional - auto-generated if not provided
|
|
199
|
+
* - `children` is a typed array of allowed child node inputs
|
|
200
|
+
* - Data fields are spread at the node level (like TreeNodeSnapshot)
|
|
201
|
+
*
|
|
202
|
+
* When TNode is a union type (e.g., from InferTreeNodeChildren), this properly
|
|
203
|
+
* distributes to create a discriminated union where:
|
|
204
|
+
* - Each variant has its specific `type` literal
|
|
205
|
+
* - Each variant has its specific data fields
|
|
206
|
+
* - Excess property checking works correctly
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const input: TreeNodeSetInput<BoardNode> = {
|
|
211
|
+
* type: "board",
|
|
212
|
+
* name: "My Board",
|
|
213
|
+
* children: [
|
|
214
|
+
* { type: "column", name: "Todo", children: [] }
|
|
215
|
+
* ]
|
|
216
|
+
* };
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export type TreeNodeSetInput<TNode extends AnyTreeNodePrimitive> = TreeNodeSetInputUnion<TNode>;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Input type for tree set() and default() operations.
|
|
223
|
+
* Accepts a nested tree structure that will be converted to flat TreeState internally.
|
|
224
|
+
*/
|
|
225
|
+
export type TreeSetInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Infer the set input type for a tree primitive.
|
|
229
|
+
*/
|
|
230
|
+
export type InferTreeSetInput<T extends TreePrimitive<any>> =
|
|
231
|
+
T extends TreePrimitive<infer TRoot> ? TreeSetInput<TRoot> : never;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Internal type for processing any node input during conversion.
|
|
235
|
+
* This is only used internally in _convertNestedToFlat and is not exported.
|
|
236
|
+
* Allows us to keep the public TreeNodeSetInput<TNode> fully type-safe while
|
|
237
|
+
* having a runtime-compatible type for internal processing.
|
|
238
|
+
*/
|
|
239
|
+
type InternalNodeInput = {
|
|
240
|
+
readonly type: string;
|
|
241
|
+
readonly id?: string;
|
|
242
|
+
readonly children: InternalNodeInput[];
|
|
243
|
+
readonly [key: string]: unknown;
|
|
244
|
+
};
|
|
245
|
+
|
|
133
246
|
/**
|
|
134
247
|
* Typed proxy for a specific node type - provides type-safe data access
|
|
135
248
|
*/
|
|
@@ -172,9 +285,9 @@ export interface TreeNodeProxyBase<_TRoot extends AnyTreeNodePrimitive> {
|
|
|
172
285
|
export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
|
|
173
286
|
/** Gets the entire tree state (flat array of nodes) */
|
|
174
287
|
get(): TreeState<TRoot>;
|
|
175
|
-
|
|
176
|
-
/** Replaces the entire tree */
|
|
177
|
-
set(
|
|
288
|
+
|
|
289
|
+
/** Replaces the entire tree with a nested input structure */
|
|
290
|
+
set(input: TreeSetInput<TRoot>): void;
|
|
178
291
|
|
|
179
292
|
/** Gets the root node state */
|
|
180
293
|
root(): TypedTreeNodeState<TRoot> | undefined;
|
|
@@ -258,16 +371,13 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
|
|
|
258
371
|
|
|
259
372
|
interface TreePrimitiveSchema<TRoot extends AnyTreeNodePrimitive> {
|
|
260
373
|
readonly required: boolean;
|
|
261
|
-
readonly
|
|
374
|
+
readonly defaultInput: TreeNodeSetInput<TRoot> | undefined;
|
|
262
375
|
readonly root: TRoot;
|
|
263
376
|
readonly validators: readonly Validator<TreeState<TRoot>>[];
|
|
264
377
|
}
|
|
265
378
|
|
|
266
|
-
/** Input type for tree
|
|
267
|
-
export type
|
|
268
|
-
|
|
269
|
-
/** Input type for tree update() - same as set() for trees */
|
|
270
|
-
export type TreeUpdateInput<TRoot extends AnyTreeNodePrimitive> = TreeState<TRoot>;
|
|
379
|
+
/** Input type for tree update() - same as set() for trees (nested format) */
|
|
380
|
+
export type TreeUpdateInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
|
|
271
381
|
|
|
272
382
|
export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends boolean = false, THasDefault extends boolean = false>
|
|
273
383
|
implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>, TRequired, THasDefault, TreeSetInput<TRoot>, TreeUpdateInput<TRoot>>
|
|
@@ -322,11 +432,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
322
432
|
});
|
|
323
433
|
}
|
|
324
434
|
|
|
325
|
-
/** Set a default value for this tree */
|
|
326
|
-
default(
|
|
435
|
+
/** Set a default value for this tree (nested format) */
|
|
436
|
+
default(defaultInput: TreeNodeSetInput<TRoot>): TreePrimitive<TRoot, TRequired, true> {
|
|
327
437
|
return new TreePrimitive({
|
|
328
438
|
...this._schema,
|
|
329
|
-
|
|
439
|
+
defaultInput,
|
|
330
440
|
});
|
|
331
441
|
}
|
|
332
442
|
|
|
@@ -408,6 +518,78 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
408
518
|
}
|
|
409
519
|
}
|
|
410
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Convert a nested TreeNodeSetInput to flat TreeState format.
|
|
523
|
+
* Recursively processes nodes, generating IDs and positions as needed.
|
|
524
|
+
*
|
|
525
|
+
* @param input - The root nested input to convert
|
|
526
|
+
* @param generateId - Optional ID generator (defaults to crypto.randomUUID)
|
|
527
|
+
* @returns Flat TreeState array
|
|
528
|
+
*/
|
|
529
|
+
private _convertNestedToFlat(
|
|
530
|
+
input: TreeNodeSetInput<TRoot>,
|
|
531
|
+
generateId: () => string = () => crypto.randomUUID()
|
|
532
|
+
): TreeState<TRoot> {
|
|
533
|
+
const result: TreeNodeState[] = [];
|
|
534
|
+
const seenIds = new Set<string>();
|
|
535
|
+
|
|
536
|
+
const processNode = (
|
|
537
|
+
nodeInput: InternalNodeInput,
|
|
538
|
+
parentId: string | null,
|
|
539
|
+
parentType: string | null,
|
|
540
|
+
leftPos: string | null,
|
|
541
|
+
rightPos: string | null
|
|
542
|
+
): void => {
|
|
543
|
+
// Validate node type
|
|
544
|
+
this._validateChildType(parentType, nodeInput.type);
|
|
545
|
+
|
|
546
|
+
// Get the node primitive for this type
|
|
547
|
+
const nodePrimitive = this._getNodeTypePrimitive(nodeInput.type);
|
|
548
|
+
|
|
549
|
+
// Generate or use provided ID
|
|
550
|
+
const id = nodeInput.id ?? generateId();
|
|
551
|
+
|
|
552
|
+
// Check for duplicate IDs
|
|
553
|
+
if (seenIds.has(id)) {
|
|
554
|
+
throw new ValidationError(`Duplicate node ID: ${id}`);
|
|
555
|
+
}
|
|
556
|
+
seenIds.add(id);
|
|
557
|
+
|
|
558
|
+
// Generate position
|
|
559
|
+
const pos = generateTreePosBetween(leftPos, rightPos);
|
|
560
|
+
|
|
561
|
+
// Extract data fields (everything except type, id, and children)
|
|
562
|
+
const { type: _type, id: _id, children, ...dataFields } = nodeInput;
|
|
563
|
+
|
|
564
|
+
// Apply defaults to node data
|
|
565
|
+
const mergedData = applyDefaults(nodePrimitive.data as AnyPrimitive, dataFields);
|
|
566
|
+
|
|
567
|
+
// Add this node to result
|
|
568
|
+
result.push({
|
|
569
|
+
id,
|
|
570
|
+
type: nodeInput.type,
|
|
571
|
+
parentId,
|
|
572
|
+
pos,
|
|
573
|
+
data: mergedData,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Process children recursively
|
|
577
|
+
let prevChildPos: string | null = null;
|
|
578
|
+
for (let i = 0; i < children.length; i++) {
|
|
579
|
+
const childInput = children[i]!;
|
|
580
|
+
// Each child gets a position after the previous child
|
|
581
|
+
processNode(childInput, id, nodeInput.type, prevChildPos, null);
|
|
582
|
+
// Update prevChildPos to the pos that was just assigned (it's the last item in result)
|
|
583
|
+
prevChildPos = result[result.length - 1]!.pos;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// Process root node (cast to InternalNodeInput for internal processing)
|
|
588
|
+
processNode(input as unknown as InternalNodeInput, null, null, null, null);
|
|
589
|
+
|
|
590
|
+
return result as TreeState<TRoot>;
|
|
591
|
+
}
|
|
592
|
+
|
|
411
593
|
readonly _internal: PrimitiveInternal<TreeState<TRoot>, TreeProxy<TRoot>> = {
|
|
412
594
|
createProxy: (
|
|
413
595
|
env: ProxyEnvironment.ProxyEnvironment,
|
|
@@ -496,9 +678,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
496
678
|
return getCurrentState();
|
|
497
679
|
},
|
|
498
680
|
|
|
499
|
-
set: (
|
|
681
|
+
set: (input: TreeSetInput<TRoot>) => {
|
|
682
|
+
// Convert nested input to flat TreeState using env.generateId for IDs
|
|
683
|
+
const flatState = this._convertNestedToFlat(input, env.generateId);
|
|
500
684
|
env.addOperation(
|
|
501
|
-
Operation.fromDefinition(operationPath, this._opDefinitions.set,
|
|
685
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, flatState)
|
|
502
686
|
);
|
|
503
687
|
},
|
|
504
688
|
|
|
@@ -1074,8 +1258,9 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
1074
1258
|
},
|
|
1075
1259
|
|
|
1076
1260
|
getInitialState: (): TreeState<TRoot> | undefined => {
|
|
1077
|
-
if (this._schema.
|
|
1078
|
-
|
|
1261
|
+
if (this._schema.defaultInput !== undefined) {
|
|
1262
|
+
// Convert nested input to flat TreeState
|
|
1263
|
+
return this._convertNestedToFlat(this._schema.defaultInput);
|
|
1079
1264
|
}
|
|
1080
1265
|
|
|
1081
1266
|
// Automatically create a root node with default data
|
|
@@ -1093,6 +1278,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
1093
1278
|
}] as TreeState<TRoot>;
|
|
1094
1279
|
},
|
|
1095
1280
|
|
|
1281
|
+
convertSetInputToState: (input: unknown): TreeState<TRoot> => {
|
|
1282
|
+
// Convert nested input format to flat TreeState
|
|
1283
|
+
return this._convertNestedToFlat(input as TreeNodeSetInput<TRoot>);
|
|
1284
|
+
},
|
|
1285
|
+
|
|
1096
1286
|
transformOperation: (
|
|
1097
1287
|
clientOp: Operation.Operation<any, any, any>,
|
|
1098
1288
|
serverOp: Operation.Operation<any, any, any>
|
|
@@ -1201,7 +1391,7 @@ export const Tree = <TRoot extends AnyTreeNodePrimitive>(
|
|
|
1201
1391
|
): TreePrimitive<TRoot, false, false> =>
|
|
1202
1392
|
new TreePrimitive({
|
|
1203
1393
|
required: false,
|
|
1204
|
-
|
|
1394
|
+
defaultInput: undefined,
|
|
1205
1395
|
root: options.root,
|
|
1206
1396
|
validators: [],
|
|
1207
1397
|
});
|
package/src/primitives/shared.ts
CHANGED
|
@@ -38,10 +38,19 @@ export interface Primitive<TState, TProxy, TRequired extends boolean = false, TH
|
|
|
38
38
|
readonly applyOperation: (state: TState | undefined, operation: Operation.Operation<any, any, any>) => TState;
|
|
39
39
|
/** Returns the initial/default state for this primitive */
|
|
40
40
|
readonly getInitialState: () => TState | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Converts a set input value to state format.
|
|
43
|
+
* For most primitives, this is a simple pass-through with defaults applied.
|
|
44
|
+
* For Tree primitives, this converts nested input to flat TreeState.
|
|
45
|
+
*
|
|
46
|
+
* @param input - The set input value
|
|
47
|
+
* @returns The corresponding state value
|
|
48
|
+
*/
|
|
49
|
+
readonly convertSetInputToState?: (input: unknown) => TState;
|
|
41
50
|
/**
|
|
42
51
|
* Transforms a client operation against a server operation.
|
|
43
52
|
* Used for operational transformation (OT) conflict resolution.
|
|
44
|
-
*
|
|
53
|
+
*
|
|
45
54
|
* @param clientOp - The client's operation to transform
|
|
46
55
|
* @param serverOp - The server's operation that has already been applied
|
|
47
56
|
* @returns TransformResult indicating how the client operation should be handled
|
|
@@ -60,8 +60,8 @@ export type SubmitResult =
|
|
|
60
60
|
export interface ServerDocumentOptions<TSchema extends Primitive.AnyPrimitive> {
|
|
61
61
|
/** The schema defining the document structure */
|
|
62
62
|
readonly schema: TSchema;
|
|
63
|
-
/** Initial
|
|
64
|
-
readonly initialState?: Primitive.
|
|
63
|
+
/** Initial value (optional, will use schema defaults if not provided) - uses set input format */
|
|
64
|
+
readonly initialState?: Primitive.InferSetInput<TSchema>;
|
|
65
65
|
/** Initial version number (optional, defaults to 0) */
|
|
66
66
|
readonly initialVersion?: number;
|
|
67
67
|
/** Called when a transaction is successfully applied and should be broadcast */
|
|
@@ -177,8 +177,9 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// Create a temporary document with current state to test the operations
|
|
180
|
+
// Use initialState (not initial) since currentState is already in flat state format
|
|
180
181
|
const currentState = _document.get();
|
|
181
|
-
const tempDoc = Document.make(schema, {
|
|
182
|
+
const tempDoc = Document.make(schema, { initialState: currentState });
|
|
182
183
|
|
|
183
184
|
try {
|
|
184
185
|
// Attempt to apply all operations
|