@voidhash/mimic 0.0.4 → 0.0.6
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/Presence.d.mts.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 +58 -8
- package/dist/primitives/Tree.d.cts +99 -10
- package/dist/primitives/Tree.d.cts.map +1 -1
- package/dist/primitives/Tree.d.mts +99 -10
- package/dist/primitives/Tree.d.mts.map +1 -1
- package/dist/primitives/Tree.mjs +58 -8
- 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 +213 -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 +296 -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
|
@@ -96,6 +96,8 @@ const generateTreePosBetween = (left: string | null, right: string | null): stri
|
|
|
96
96
|
*/
|
|
97
97
|
export type TreeNodeSnapshot<TNode extends AnyTreeNodePrimitive> = {
|
|
98
98
|
readonly id: string;
|
|
99
|
+
readonly pos: string;
|
|
100
|
+
readonly parentId: string | null;
|
|
99
101
|
readonly type: InferTreeNodeType<TNode>;
|
|
100
102
|
readonly children: TreeNodeSnapshot<InferTreeNodeChildren<TNode>>[];
|
|
101
103
|
} & InferTreeNodeDataState<TNode>;
|
|
@@ -121,15 +123,128 @@ export type TreeNodeUpdateValue<TNode extends AnyTreeNodePrimitive> =
|
|
|
121
123
|
* Uses StructSetInput directly so that:
|
|
122
124
|
* - Fields that are required AND have no default must be provided
|
|
123
125
|
* - Fields that are optional OR have defaults can be omitted
|
|
124
|
-
*
|
|
126
|
+
*
|
|
125
127
|
* This bypasses the struct-level NeedsValue wrapper since tree inserts
|
|
126
128
|
* always require a data object (even if empty for all-optional fields).
|
|
127
129
|
*/
|
|
128
|
-
export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
|
|
130
|
+
export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
|
|
129
131
|
TNode["data"] extends StructPrimitive<infer TFields, any, any>
|
|
130
132
|
? StructSetInput<TFields>
|
|
131
133
|
: InferSetInput<TNode["data"]>;
|
|
132
134
|
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Nested Input Types for set() and default()
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Type guard to check if a type is `any` or unknown (structural check).
|
|
141
|
+
* Returns true if T is `any`, false otherwise.
|
|
142
|
+
*/
|
|
143
|
+
type IsAny<T> = 0 extends (1 & T) ? true : false;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get children types, with special handling for self-referential nodes.
|
|
147
|
+
* When InferTreeNodeChildren returns `any` (from TreeNodePrimitive<..., any>),
|
|
148
|
+
* we fall back to using TNode itself as its own child type.
|
|
149
|
+
* This handles the common case of self-referential nodes like:
|
|
150
|
+
* FolderNode = TreeNode("folder", { children: [TreeNodeSelf, FileNode] })
|
|
151
|
+
* Where FolderNode's TChildren is resolved to FolderNode | FileNode,
|
|
152
|
+
* but the self-referenced FolderNode has TChildren = any.
|
|
153
|
+
*/
|
|
154
|
+
type ResolveChildrenForInput<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
|
|
155
|
+
IsAny<InferTreeNodeChildren<TNode>> extends true
|
|
156
|
+
? TOriginalNode // Self-reference: use the original node's type
|
|
157
|
+
: InferTreeNodeChildren<TNode>;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Helper type that creates a properly typed node input for a specific node type.
|
|
161
|
+
* This is the "strict" version that enforces exact property matching.
|
|
162
|
+
*
|
|
163
|
+
* The TOriginalNode parameter is used to track the original root node type
|
|
164
|
+
* for proper self-reference resolution in deeply nested structures.
|
|
165
|
+
*/
|
|
166
|
+
type TreeNodeSetInputStrictWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> = {
|
|
167
|
+
readonly type: InferTreeNodeType<TNode>;
|
|
168
|
+
readonly id?: string;
|
|
169
|
+
readonly children: TreeNodeSetInputUnionWithRoot<ResolveChildrenForInput<TNode, TOriginalNode>, TOriginalNode>[];
|
|
170
|
+
} & TreeNodeDataSetInput<TNode>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Distributive conditional type that creates a union of TreeNodeSetInputStrict
|
|
174
|
+
* for each node type in the union. This ensures proper type discrimination.
|
|
175
|
+
*
|
|
176
|
+
* When TNode is a union (e.g., FolderNode | FileNode), this distributes to:
|
|
177
|
+
* TreeNodeSetInputStrict<FolderNode> | TreeNodeSetInputStrict<FileNode>
|
|
178
|
+
*
|
|
179
|
+
* This creates a proper discriminated union where excess property checking works.
|
|
180
|
+
*/
|
|
181
|
+
type TreeNodeSetInputUnionWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
|
|
182
|
+
TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrictWithRoot<TNode, TOriginalNode> : never;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Helper type for single-parameter usage - uses TNode as its own original.
|
|
186
|
+
*/
|
|
187
|
+
type TreeNodeSetInputStrict<TNode extends AnyTreeNodePrimitive> =
|
|
188
|
+
TreeNodeSetInputStrictWithRoot<TNode, TNode>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Distributive conditional for single-parameter usage.
|
|
192
|
+
*/
|
|
193
|
+
export type TreeNodeSetInputUnion<TNode extends AnyTreeNodePrimitive> =
|
|
194
|
+
TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrict<TNode> : never;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Input type for a single node in a nested tree set/default operation.
|
|
198
|
+
*
|
|
199
|
+
* - `type` is REQUIRED - explicit type discriminator for the node
|
|
200
|
+
* - `id` is optional - auto-generated if not provided
|
|
201
|
+
* - `children` is a typed array of allowed child node inputs
|
|
202
|
+
* - Data fields are spread at the node level (like TreeNodeSnapshot)
|
|
203
|
+
*
|
|
204
|
+
* When TNode is a union type (e.g., from InferTreeNodeChildren), this properly
|
|
205
|
+
* distributes to create a discriminated union where:
|
|
206
|
+
* - Each variant has its specific `type` literal
|
|
207
|
+
* - Each variant has its specific data fields
|
|
208
|
+
* - Excess property checking works correctly
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* const input: TreeNodeSetInput<BoardNode> = {
|
|
213
|
+
* type: "board",
|
|
214
|
+
* name: "My Board",
|
|
215
|
+
* children: [
|
|
216
|
+
* { type: "column", name: "Todo", children: [] }
|
|
217
|
+
* ]
|
|
218
|
+
* };
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
export type TreeNodeSetInput<TNode extends AnyTreeNodePrimitive> = TreeNodeSetInputUnion<TNode>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Input type for tree set() and default() operations.
|
|
225
|
+
* Accepts a nested tree structure that will be converted to flat TreeState internally.
|
|
226
|
+
*/
|
|
227
|
+
export type TreeSetInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Infer the set input type for a tree primitive.
|
|
231
|
+
*/
|
|
232
|
+
export type InferTreeSetInput<T extends TreePrimitive<any>> =
|
|
233
|
+
T extends TreePrimitive<infer TRoot> ? TreeSetInput<TRoot> : never;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Internal type for processing any node input during conversion.
|
|
237
|
+
* This is only used internally in _convertNestedToFlat and is not exported.
|
|
238
|
+
* Allows us to keep the public TreeNodeSetInput<TNode> fully type-safe while
|
|
239
|
+
* having a runtime-compatible type for internal processing.
|
|
240
|
+
*/
|
|
241
|
+
type InternalNodeInput = {
|
|
242
|
+
readonly type: string;
|
|
243
|
+
readonly id?: string;
|
|
244
|
+
readonly children: InternalNodeInput[];
|
|
245
|
+
readonly [key: string]: unknown;
|
|
246
|
+
};
|
|
247
|
+
|
|
133
248
|
/**
|
|
134
249
|
* Typed proxy for a specific node type - provides type-safe data access
|
|
135
250
|
*/
|
|
@@ -172,9 +287,9 @@ export interface TreeNodeProxyBase<_TRoot extends AnyTreeNodePrimitive> {
|
|
|
172
287
|
export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
|
|
173
288
|
/** Gets the entire tree state (flat array of nodes) */
|
|
174
289
|
get(): TreeState<TRoot>;
|
|
175
|
-
|
|
176
|
-
/** Replaces the entire tree */
|
|
177
|
-
set(
|
|
290
|
+
|
|
291
|
+
/** Replaces the entire tree with a nested input structure */
|
|
292
|
+
set(input: TreeSetInput<TRoot>): void;
|
|
178
293
|
|
|
179
294
|
/** Gets the root node state */
|
|
180
295
|
root(): TypedTreeNodeState<TRoot> | undefined;
|
|
@@ -258,16 +373,13 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
|
|
|
258
373
|
|
|
259
374
|
interface TreePrimitiveSchema<TRoot extends AnyTreeNodePrimitive> {
|
|
260
375
|
readonly required: boolean;
|
|
261
|
-
readonly
|
|
376
|
+
readonly defaultInput: TreeNodeSetInput<TRoot> | undefined;
|
|
262
377
|
readonly root: TRoot;
|
|
263
378
|
readonly validators: readonly Validator<TreeState<TRoot>>[];
|
|
264
379
|
}
|
|
265
380
|
|
|
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>;
|
|
381
|
+
/** Input type for tree update() - same as set() for trees (nested format) */
|
|
382
|
+
export type TreeUpdateInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
|
|
271
383
|
|
|
272
384
|
export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends boolean = false, THasDefault extends boolean = false>
|
|
273
385
|
implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>, TRequired, THasDefault, TreeSetInput<TRoot>, TreeUpdateInput<TRoot>>
|
|
@@ -322,11 +434,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
322
434
|
});
|
|
323
435
|
}
|
|
324
436
|
|
|
325
|
-
/** Set a default value for this tree */
|
|
326
|
-
default(
|
|
437
|
+
/** Set a default value for this tree (nested format) */
|
|
438
|
+
default(defaultInput: TreeNodeSetInput<TRoot>): TreePrimitive<TRoot, TRequired, true> {
|
|
327
439
|
return new TreePrimitive({
|
|
328
440
|
...this._schema,
|
|
329
|
-
|
|
441
|
+
defaultInput,
|
|
330
442
|
});
|
|
331
443
|
}
|
|
332
444
|
|
|
@@ -408,6 +520,78 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
408
520
|
}
|
|
409
521
|
}
|
|
410
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Convert a nested TreeNodeSetInput to flat TreeState format.
|
|
525
|
+
* Recursively processes nodes, generating IDs and positions as needed.
|
|
526
|
+
*
|
|
527
|
+
* @param input - The root nested input to convert
|
|
528
|
+
* @param generateId - Optional ID generator (defaults to crypto.randomUUID)
|
|
529
|
+
* @returns Flat TreeState array
|
|
530
|
+
*/
|
|
531
|
+
private _convertNestedToFlat(
|
|
532
|
+
input: TreeNodeSetInput<TRoot>,
|
|
533
|
+
generateId: () => string = () => crypto.randomUUID()
|
|
534
|
+
): TreeState<TRoot> {
|
|
535
|
+
const result: TreeNodeState[] = [];
|
|
536
|
+
const seenIds = new Set<string>();
|
|
537
|
+
|
|
538
|
+
const processNode = (
|
|
539
|
+
nodeInput: InternalNodeInput,
|
|
540
|
+
parentId: string | null,
|
|
541
|
+
parentType: string | null,
|
|
542
|
+
leftPos: string | null,
|
|
543
|
+
rightPos: string | null
|
|
544
|
+
): void => {
|
|
545
|
+
// Validate node type
|
|
546
|
+
this._validateChildType(parentType, nodeInput.type);
|
|
547
|
+
|
|
548
|
+
// Get the node primitive for this type
|
|
549
|
+
const nodePrimitive = this._getNodeTypePrimitive(nodeInput.type);
|
|
550
|
+
|
|
551
|
+
// Generate or use provided ID
|
|
552
|
+
const id = nodeInput.id ?? generateId();
|
|
553
|
+
|
|
554
|
+
// Check for duplicate IDs
|
|
555
|
+
if (seenIds.has(id)) {
|
|
556
|
+
throw new ValidationError(`Duplicate node ID: ${id}`);
|
|
557
|
+
}
|
|
558
|
+
seenIds.add(id);
|
|
559
|
+
|
|
560
|
+
// Generate position
|
|
561
|
+
const pos = generateTreePosBetween(leftPos, rightPos);
|
|
562
|
+
|
|
563
|
+
// Extract data fields (everything except type, id, and children)
|
|
564
|
+
const { type: _type, id: _id, children, ...dataFields } = nodeInput;
|
|
565
|
+
|
|
566
|
+
// Apply defaults to node data
|
|
567
|
+
const mergedData = applyDefaults(nodePrimitive.data as AnyPrimitive, dataFields);
|
|
568
|
+
|
|
569
|
+
// Add this node to result
|
|
570
|
+
result.push({
|
|
571
|
+
id,
|
|
572
|
+
type: nodeInput.type,
|
|
573
|
+
parentId,
|
|
574
|
+
pos,
|
|
575
|
+
data: mergedData,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Process children recursively
|
|
579
|
+
let prevChildPos: string | null = null;
|
|
580
|
+
for (let i = 0; i < children.length; i++) {
|
|
581
|
+
const childInput = children[i]!;
|
|
582
|
+
// Each child gets a position after the previous child
|
|
583
|
+
processNode(childInput, id, nodeInput.type, prevChildPos, null);
|
|
584
|
+
// Update prevChildPos to the pos that was just assigned (it's the last item in result)
|
|
585
|
+
prevChildPos = result[result.length - 1]!.pos;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Process root node (cast to InternalNodeInput for internal processing)
|
|
590
|
+
processNode(input as unknown as InternalNodeInput, null, null, null, null);
|
|
591
|
+
|
|
592
|
+
return result as TreeState<TRoot>;
|
|
593
|
+
}
|
|
594
|
+
|
|
411
595
|
readonly _internal: PrimitiveInternal<TreeState<TRoot>, TreeProxy<TRoot>> = {
|
|
412
596
|
createProxy: (
|
|
413
597
|
env: ProxyEnvironment.ProxyEnvironment,
|
|
@@ -486,6 +670,8 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
486
670
|
return {
|
|
487
671
|
id: node.id,
|
|
488
672
|
type: node.type,
|
|
673
|
+
pos: node.pos,
|
|
674
|
+
parentId: node.parentId,
|
|
489
675
|
...(node.data as object),
|
|
490
676
|
children,
|
|
491
677
|
} as unknown as TreeNodeSnapshot<TRoot>;
|
|
@@ -496,9 +682,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
496
682
|
return getCurrentState();
|
|
497
683
|
},
|
|
498
684
|
|
|
499
|
-
set: (
|
|
685
|
+
set: (input: TreeSetInput<TRoot>) => {
|
|
686
|
+
// Convert nested input to flat TreeState using env.generateId for IDs
|
|
687
|
+
const flatState = this._convertNestedToFlat(input, env.generateId);
|
|
500
688
|
env.addOperation(
|
|
501
|
-
Operation.fromDefinition(operationPath, this._opDefinitions.set,
|
|
689
|
+
Operation.fromDefinition(operationPath, this._opDefinitions.set, flatState)
|
|
502
690
|
);
|
|
503
691
|
},
|
|
504
692
|
|
|
@@ -1074,8 +1262,9 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
1074
1262
|
},
|
|
1075
1263
|
|
|
1076
1264
|
getInitialState: (): TreeState<TRoot> | undefined => {
|
|
1077
|
-
if (this._schema.
|
|
1078
|
-
|
|
1265
|
+
if (this._schema.defaultInput !== undefined) {
|
|
1266
|
+
// Convert nested input to flat TreeState
|
|
1267
|
+
return this._convertNestedToFlat(this._schema.defaultInput);
|
|
1079
1268
|
}
|
|
1080
1269
|
|
|
1081
1270
|
// Automatically create a root node with default data
|
|
@@ -1093,6 +1282,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
|
|
|
1093
1282
|
}] as TreeState<TRoot>;
|
|
1094
1283
|
},
|
|
1095
1284
|
|
|
1285
|
+
convertSetInputToState: (input: unknown): TreeState<TRoot> => {
|
|
1286
|
+
// Convert nested input format to flat TreeState
|
|
1287
|
+
return this._convertNestedToFlat(input as TreeNodeSetInput<TRoot>);
|
|
1288
|
+
},
|
|
1289
|
+
|
|
1096
1290
|
transformOperation: (
|
|
1097
1291
|
clientOp: Operation.Operation<any, any, any>,
|
|
1098
1292
|
serverOp: Operation.Operation<any, any, any>
|
|
@@ -1201,7 +1395,7 @@ export const Tree = <TRoot extends AnyTreeNodePrimitive>(
|
|
|
1201
1395
|
): TreePrimitive<TRoot, false, false> =>
|
|
1202
1396
|
new TreePrimitive({
|
|
1203
1397
|
required: false,
|
|
1204
|
-
|
|
1398
|
+
defaultInput: undefined,
|
|
1205
1399
|
root: options.root,
|
|
1206
1400
|
validators: [],
|
|
1207
1401
|
});
|
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
|