dirvi-lib 0.11.0-beta.6 → 0.11.0-beta.7
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/dist/app/app-api.d.ts +7 -25
- package/dist/app/app-api.d.ts.map +1 -1
- package/dist/app/app-api.js +1 -11
- package/dist/app/index.d.ts +1 -0
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +1 -0
- package/dist/app/node-presentation.d.ts +13 -0
- package/dist/app/node-presentation.d.ts.map +1 -0
- package/dist/app/reducer/effect-to-action.d.ts +2 -2
- package/dist/app/reducer/effect-to-action.d.ts.map +1 -1
- package/dist/app/reducer/effect-to-action.js +87 -32
- package/dist/app/reducer/reducer-action.d.ts +12 -6
- package/dist/app/reducer/reducer-action.d.ts.map +1 -1
- package/dist/app/reducer/reducer.d.ts +4 -2
- package/dist/app/reducer/reducer.d.ts.map +1 -1
- package/dist/app/reducer/reducer.js +102 -68
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/input/application/intent-to-effect.d.ts +2 -2
- package/dist/input/application/intent-to-effect.d.ts.map +1 -1
- package/dist/input/application/intent-to-effect.js +8 -9
- package/dist/input/application/parse-ex-command.d.ts +1 -1
- package/dist/input/application/parse-ex-command.d.ts.map +1 -1
- package/dist/input/application/parse-normal-command.d.ts +1 -1
- package/dist/input/application/parse-normal-command.d.ts.map +1 -1
- package/dist/input/domain/effect.d.ts +1 -2
- package/dist/input/domain/effect.d.ts.map +1 -1
- package/dist/state-and-nav/cursor.d.ts +89 -0
- package/dist/state-and-nav/cursor.d.ts.map +1 -0
- package/dist/state-and-nav/cursor.js +84 -0
- package/dist/{trees → state-and-nav}/fold-node/deprecated/fold-node.impl.d.ts +1 -1
- package/dist/state-and-nav/fold-node/deprecated/fold-node.impl.d.ts.map +1 -0
- package/dist/state-and-nav/fold-node/deprecated/fold-node.test.d.ts.map +1 -0
- package/dist/{trees → state-and-nav}/fold-node/deprecated/fold-node.test.js +2 -2
- package/dist/{trees → state-and-nav}/fold-node/deprecated/fold-node.types.d.ts +2 -2
- package/dist/state-and-nav/fold-node/deprecated/fold-node.types.d.ts.map +1 -0
- package/dist/{trees → state-and-nav}/fold-node/fold-node.impl.d.ts +3 -3
- package/dist/state-and-nav/fold-node/fold-node.impl.d.ts.map +1 -0
- package/dist/{trees → state-and-nav}/fold-node/fold-node.impl.js +1 -1
- package/dist/{trees → state-and-nav}/fold-node/fold-node.model.d.ts +1 -1
- package/dist/state-and-nav/fold-node/fold-node.model.d.ts.map +1 -0
- package/dist/{trees → state-and-nav}/fold-node/fold-node.types.d.ts +2 -2
- package/dist/state-and-nav/fold-node/fold-node.types.d.ts.map +1 -0
- package/dist/state-and-nav/fold-node/index.d.ts.map +1 -0
- package/dist/state-and-nav/index.d.ts +16 -0
- package/dist/state-and-nav/index.d.ts.map +1 -0
- package/dist/state-and-nav/index.js +6 -0
- package/dist/state-and-nav/nav/nav.ordered.impl.d.ts +12 -0
- package/dist/state-and-nav/nav/nav.ordered.impl.d.ts.map +1 -0
- package/dist/state-and-nav/nav/nav.ordered.impl.js +77 -0
- package/dist/state-and-nav/nav/nav.traversal.d.ts +11 -0
- package/dist/state-and-nav/nav/nav.traversal.d.ts.map +1 -0
- package/dist/state-and-nav/nav/nav.traversal.js +207 -0
- package/dist/{trees/nav-node/nav-node.types.d.ts → state-and-nav/nav/nav.types.d.ts} +32 -22
- package/dist/state-and-nav/nav/nav.types.d.ts.map +1 -0
- package/dist/state-and-nav/nav/nav.unordered.impl.d.ts +13 -0
- package/dist/state-and-nav/nav/nav.unordered.impl.d.ts.map +1 -0
- package/dist/state-and-nav/nav/nav.unordered.impl.js +77 -0
- package/dist/state-and-nav/state/state.impl.d.ts +9 -0
- package/dist/state-and-nav/state/state.impl.d.ts.map +1 -0
- package/dist/{trees → state-and-nav}/state/state.impl.js +62 -9
- package/dist/state-and-nav/state/state.schemas.d.ts +2 -0
- package/dist/state-and-nav/state/state.schemas.d.ts.map +1 -0
- package/dist/state-and-nav/state/state.schemas.js +39 -0
- package/dist/state-and-nav/state/state.types.d.ts +72 -0
- package/dist/state-and-nav/state/state.types.d.ts.map +1 -0
- package/dist/state-and-nav/tree/deprecated/tree-node.arb.d.ts.map +1 -0
- package/dist/state-and-nav/tree/deprecated/tree-node.arb.debug.test.d.ts.map +1 -0
- package/dist/state-and-nav/tree/deprecated/tree-node.impl.d.ts.map +1 -0
- package/dist/state-and-nav/tree/deprecated/tree-node.schemas.d.ts.map +1 -0
- package/dist/state-and-nav/tree/deprecated/tree-node.test.d.ts.map +1 -0
- package/dist/state-and-nav/tree/deprecated/tree-node.types.d.ts.map +1 -0
- package/dist/state-and-nav/tree/index.d.ts +7 -0
- package/dist/state-and-nav/tree/index.d.ts.map +1 -0
- package/dist/state-and-nav/tree/index.js +1 -0
- package/dist/state-and-nav/tree/tree-node.arb.d.ts +94 -0
- package/dist/state-and-nav/tree/tree-node.arb.d.ts.map +1 -0
- package/dist/state-and-nav/tree/tree-node.arb.debug.test.d.ts +2 -0
- package/dist/state-and-nav/tree/tree-node.arb.debug.test.d.ts.map +1 -0
- package/dist/state-and-nav/tree/tree-node.arb.debug.test.js +54 -0
- package/dist/state-and-nav/tree/tree-node.arb.js +188 -0
- package/dist/state-and-nav/tree/tree-node.model.d.ts +193 -0
- package/dist/state-and-nav/tree/tree-node.model.d.ts.map +1 -0
- package/dist/state-and-nav/tree/tree-node.model.js +110 -0
- package/dist/state-and-nav/tree/tree-node.test.d.ts +2 -0
- package/dist/state-and-nav/tree/tree-node.test.d.ts.map +1 -0
- package/dist/state-and-nav/tree/tree-node.test.js +234 -0
- package/dist/{trees/tree-node/tree-node.impl.d.ts → state-and-nav/tree/tree.impl.d.ts} +3 -3
- package/dist/state-and-nav/tree/tree.impl.d.ts.map +1 -0
- package/dist/{trees/tree-node/tree-node.impl.js → state-and-nav/tree/tree.impl.js} +19 -16
- package/dist/{trees/tree-node/tree-node.types.d.ts → state-and-nav/tree/tree.types.d.ts} +64 -44
- package/dist/state-and-nav/tree/tree.types.d.ts.map +1 -0
- package/dist/state-and-nav/tree/tree.types.js +1 -0
- package/package.json +1 -1
- package/dist/trees/cursor.d.ts +0 -59
- package/dist/trees/cursor.d.ts.map +0 -1
- package/dist/trees/cursor.js +0 -55
- package/dist/trees/fold-node/deprecated/fold-node.impl.d.ts.map +0 -1
- package/dist/trees/fold-node/deprecated/fold-node.test.d.ts.map +0 -1
- package/dist/trees/fold-node/deprecated/fold-node.types.d.ts.map +0 -1
- package/dist/trees/fold-node/fold-node.impl.d.ts.map +0 -1
- package/dist/trees/fold-node/fold-node.model.d.ts.map +0 -1
- package/dist/trees/fold-node/fold-node.types.d.ts.map +0 -1
- package/dist/trees/fold-node/index.d.ts.map +0 -1
- package/dist/trees/index.d.ts +0 -9
- package/dist/trees/index.d.ts.map +0 -1
- package/dist/trees/index.js +0 -8
- package/dist/trees/nav-node/nav-node.impl.d.ts +0 -29
- package/dist/trees/nav-node/nav-node.impl.d.ts.map +0 -1
- package/dist/trees/nav-node/nav-node.impl.js +0 -223
- package/dist/trees/nav-node/nav-node.types.d.ts.map +0 -1
- package/dist/trees/state/state.impl.d.ts +0 -8
- package/dist/trees/state/state.impl.d.ts.map +0 -1
- package/dist/trees/state/state.schemas.d.ts +0 -25
- package/dist/trees/state/state.schemas.d.ts.map +0 -1
- package/dist/trees/state/state.schemas.js +0 -17
- package/dist/trees/state/state.types.d.ts +0 -36
- package/dist/trees/state/state.types.d.ts.map +0 -1
- package/dist/trees/tree-node/deprecated/tree-node.arb.d.ts.map +0 -1
- package/dist/trees/tree-node/deprecated/tree-node.arb.debug.test.d.ts.map +0 -1
- package/dist/trees/tree-node/deprecated/tree-node.impl.d.ts.map +0 -1
- package/dist/trees/tree-node/deprecated/tree-node.schemas.d.ts.map +0 -1
- package/dist/trees/tree-node/deprecated/tree-node.test.d.ts.map +0 -1
- package/dist/trees/tree-node/deprecated/tree-node.types.d.ts.map +0 -1
- package/dist/trees/tree-node/index.d.ts +0 -5
- package/dist/trees/tree-node/index.d.ts.map +0 -1
- package/dist/trees/tree-node/index.js +0 -2
- package/dist/trees/tree-node/tree-node.arb.d.ts +0 -104
- package/dist/trees/tree-node/tree-node.arb.d.ts.map +0 -1
- package/dist/trees/tree-node/tree-node.arb.js +0 -188
- package/dist/trees/tree-node/tree-node.impl.d.ts.map +0 -1
- package/dist/trees/tree-node/tree-node.model.d.ts +0 -78
- package/dist/trees/tree-node/tree-node.model.d.ts.map +0 -1
- package/dist/trees/tree-node/tree-node.model.js +0 -37
- package/dist/trees/tree-node/tree-node.types.d.ts.map +0 -1
- /package/dist/{trees/fold-node/deprecated/fold-node.types.js → app/node-presentation.js} +0 -0
- /package/dist/{trees → state-and-nav}/fold-node/deprecated/fold-node.impl.js +0 -0
- /package/dist/{trees → state-and-nav}/fold-node/deprecated/fold-node.test.d.ts +0 -0
- /package/dist/{trees/fold-node → state-and-nav/fold-node/deprecated}/fold-node.types.js +0 -0
- /package/dist/{trees → state-and-nav}/fold-node/fold-node.model.js +0 -0
- /package/dist/{trees/nav-node/nav-node.types.js → state-and-nav/fold-node/fold-node.types.js} +0 -0
- /package/dist/{trees → state-and-nav}/fold-node/index.d.ts +0 -0
- /package/dist/{trees → state-and-nav}/fold-node/index.js +0 -0
- /package/dist/{trees/state/state.types.js → state-and-nav/nav/nav.types.js} +0 -0
- /package/dist/{trees/tree-node/deprecated/tree-node.types.js → state-and-nav/state/state.types.js} +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.arb.d.ts +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.arb.debug.test.d.ts +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.arb.debug.test.js +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.arb.js +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.impl.d.ts +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.impl.js +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.schemas.d.ts +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.schemas.js +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.test.d.ts +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.test.js +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree}/deprecated/tree-node.types.d.ts +0 -0
- /package/dist/{trees/tree-node → state-and-nav/tree/deprecated}/tree-node.types.js +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A serializable primitive suitable for use as a stable tree-node ID.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
*
|
|
7
|
+
* A node ID identifies a node among its direct siblings. It is not required
|
|
8
|
+
* to be globally unique across the whole tree: two nodes in different
|
|
9
|
+
* branches may have the same ID.
|
|
10
|
+
*
|
|
11
|
+
* Numeric IDs must be finite. TypeScript cannot represent the exclusion of
|
|
12
|
+
* `NaN`, `Infinity`, and `-Infinity` from `number`, so values received from
|
|
13
|
+
* an untrusted source should be validated at an application boundary, for
|
|
14
|
+
* example with `serializableKeySchema`.
|
|
15
|
+
*
|
|
16
|
+
* Applications should normally define a narrower domain-specific ID type.
|
|
17
|
+
* Branded ID types prevent IDs from unrelated domains from being mixed
|
|
18
|
+
* accidentally, even when both use strings at runtime.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const myIdSchema = z
|
|
24
|
+
* .string()
|
|
25
|
+
* .min(1)
|
|
26
|
+
* .brand<'MyId'>();
|
|
27
|
+
*
|
|
28
|
+
* type MyId = z.output<typeof myIdSchema>;
|
|
29
|
+
*
|
|
30
|
+
* type MyItem = Readonly<{
|
|
31
|
+
* label: string;
|
|
32
|
+
* href?: string;
|
|
33
|
+
* }>;
|
|
34
|
+
*
|
|
35
|
+
* type MyTreeModel = TreeNodeModel<MyId, MyItem>;
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export type SerializableKey = string | number;
|
|
39
|
+
/**
|
|
40
|
+
* Validates a serializable primitive suitable for use as a tree-node ID.
|
|
41
|
+
*
|
|
42
|
+
* Strings are accepted without additional restrictions. Numbers must be
|
|
43
|
+
* finite: `NaN`, `Infinity`, and `-Infinity` are rejected.
|
|
44
|
+
*
|
|
45
|
+
* Applications can derive narrower schemas to impose domain-specific
|
|
46
|
+
* restrictions or produce branded ID types.
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
*
|
|
50
|
+
* Requires Zod v4, where `z.number()` rejects non-finite values.
|
|
51
|
+
*/
|
|
52
|
+
export declare const serializableKeySchema: z.ZodType<SerializableKey>;
|
|
53
|
+
/**
|
|
54
|
+
* A structural model for persisting or exchanging a tree node.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
*
|
|
58
|
+
* This model is independent of the opaque runtime tree representation.
|
|
59
|
+
* It consists of ordinary objects and ordered child arrays.
|
|
60
|
+
*
|
|
61
|
+
* The presence and value of `children` distinguish three node states:
|
|
62
|
+
*
|
|
63
|
+
* - Absent: a leaf, which cannot contain children.
|
|
64
|
+
* - `null`: a branch whose children have not been loaded.
|
|
65
|
+
* - An array: a branch whose children are loaded, possibly with no children.
|
|
66
|
+
*
|
|
67
|
+
* Loaded children form an ordered sequence. Their array positions describe
|
|
68
|
+
* child order, while their IDs identify them independently of those positions.
|
|
69
|
+
* Direct siblings must have unique IDs.
|
|
70
|
+
*
|
|
71
|
+
* The model is suitable for JSON serialization when `Id` and `Value` are
|
|
72
|
+
* JSON-serializable. The generic `Value` type does not itself guarantee this.
|
|
73
|
+
*
|
|
74
|
+
* `Readonly` prevents structural mutation through these types at compile time.
|
|
75
|
+
* It does not freeze objects at runtime or make application-owned values
|
|
76
|
+
* deeply immutable.
|
|
77
|
+
*
|
|
78
|
+
* Models represent finite trees, not cyclic object graphs.
|
|
79
|
+
*
|
|
80
|
+
* @typeParam Id - The type identifying each node among its direct siblings.
|
|
81
|
+
* @typeParam Value - Application-owned data associated with each node.
|
|
82
|
+
*/
|
|
83
|
+
export type TreeNodeModel<Id extends SerializableKey, Value> = LeafTreeNodeModel<Id, Value> | UnloadedBranchTreeNodeModel<Id, Value> | LoadedBranchTreeNodeModel<Id, Value>;
|
|
84
|
+
/**
|
|
85
|
+
* A structural model of a leaf node.
|
|
86
|
+
*
|
|
87
|
+
* A leaf cannot contain children. This is distinct from a loaded branch
|
|
88
|
+
* whose child array is currently empty.
|
|
89
|
+
*
|
|
90
|
+
* @typeParam Id - The type identifying this node among its direct siblings.
|
|
91
|
+
* @typeParam Value - Application-owned data associated with this node.
|
|
92
|
+
*/
|
|
93
|
+
export type LeafTreeNodeModel<Id extends SerializableKey, Value> = Readonly<{
|
|
94
|
+
id: Id;
|
|
95
|
+
value: Value;
|
|
96
|
+
children?: never;
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* A structural model of a branch whose children have not been loaded.
|
|
100
|
+
*
|
|
101
|
+
* The branch may contain children, but their identities, order, and values
|
|
102
|
+
* are unavailable in this model. `null` does not mean the branch is empty.
|
|
103
|
+
*
|
|
104
|
+
* @typeParam Id - The type identifying this node among its direct siblings.
|
|
105
|
+
* @typeParam Value - Application-owned data associated with this node.
|
|
106
|
+
*/
|
|
107
|
+
export type UnloadedBranchTreeNodeModel<Id extends SerializableKey, Value> = Readonly<{
|
|
108
|
+
id: Id;
|
|
109
|
+
value: Value;
|
|
110
|
+
children: null;
|
|
111
|
+
}>;
|
|
112
|
+
/**
|
|
113
|
+
* A structural model of a branch with loaded children.
|
|
114
|
+
*
|
|
115
|
+
* An empty child array represents a loaded branch with no current children,
|
|
116
|
+
* not a leaf. The branch can still contain children.
|
|
117
|
+
*
|
|
118
|
+
* @typeParam Id - The type identifying this node among its direct siblings.
|
|
119
|
+
* @typeParam Value - Application-owned data associated with this node.
|
|
120
|
+
*/
|
|
121
|
+
export type LoadedBranchTreeNodeModel<Id extends SerializableKey, Value> = Readonly<{
|
|
122
|
+
id: Id;
|
|
123
|
+
value: Value;
|
|
124
|
+
children: readonly TreeNodeModel<Id, Value>[];
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* Creates a recursive schema for validating structural tree-node models.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
*
|
|
131
|
+
* The supplied schemas validate every node's ID and application-owned value.
|
|
132
|
+
* The returned schema validates the three node shapes recursively:
|
|
133
|
+
*
|
|
134
|
+
* - Leaves omit `children`.
|
|
135
|
+
* - Unloaded branches have `children: null`.
|
|
136
|
+
* - Loaded branches have an ordered array of valid child models.
|
|
137
|
+
*
|
|
138
|
+
* Node objects are strict: properties other than `id`, `value`, and the
|
|
139
|
+
* appropriate `children` property are rejected. Validation of properties
|
|
140
|
+
* inside `value` is controlled by `valueSchema`.
|
|
141
|
+
*
|
|
142
|
+
* Child arrays are parsed in their existing order; this schema does not sort
|
|
143
|
+
* or reorder children.
|
|
144
|
+
*
|
|
145
|
+
* This schema validates shape and field values, but does not enforce
|
|
146
|
+
* sibling-ID uniqueness. That invariant must be checked when constructing
|
|
147
|
+
* runtime tree nodes or by an additional validation step.
|
|
148
|
+
*
|
|
149
|
+
* The supplied ID schema is responsible for enforcing the runtime ID
|
|
150
|
+
* constraints, including finite numeric IDs. The generic constraint alone
|
|
151
|
+
* cannot enforce those restrictions.
|
|
152
|
+
*
|
|
153
|
+
* Inputs must describe finite trees. Cyclic object graphs are not supported.
|
|
154
|
+
*
|
|
155
|
+
* @typeParam Id - The validated node-ID type.
|
|
156
|
+
* @typeParam Value - The validated application-value type.
|
|
157
|
+
* @param idSchema - Schema used to validate every node's ID.
|
|
158
|
+
* @param valueSchema - Schema used to validate every node's application data.
|
|
159
|
+
* @returns A recursive schema for the application's tree-node models.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* const myIdSchema = serializableKeySchema
|
|
165
|
+
* .pipe(z.string().min(1))
|
|
166
|
+
* .brand<'MyId'>();
|
|
167
|
+
*
|
|
168
|
+
* type MyId = z.output<typeof myIdSchema>;
|
|
169
|
+
*
|
|
170
|
+
* const modelSchema = createTreeNodeModelSchema(
|
|
171
|
+
* myIdSchema,
|
|
172
|
+
* z.object({ label: z.string() }).strict(),
|
|
173
|
+
* );
|
|
174
|
+
*
|
|
175
|
+
* const model = modelSchema.parse({
|
|
176
|
+
* id: 'root',
|
|
177
|
+
* value: { label: 'My' },
|
|
178
|
+
* children: [
|
|
179
|
+
* {
|
|
180
|
+
* id: 'home',
|
|
181
|
+
* value: { label: 'Home' },
|
|
182
|
+
* },
|
|
183
|
+
* {
|
|
184
|
+
* id: 'settings',
|
|
185
|
+
* value: { label: 'Settings' },
|
|
186
|
+
* children: null,
|
|
187
|
+
* },
|
|
188
|
+
* ],
|
|
189
|
+
* });
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
export declare function createTreeNodeModelSchema<Id extends SerializableKey, Value>(idSchema: z.ZodType<Id>, valueSchema: z.ZodType<Value>): z.ZodType<TreeNodeModel<Id, Value>>;
|
|
193
|
+
//# sourceMappingURL=tree-node.model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-node.model.d.ts","sourceRoot":"","sources":["../../../src/state-and-nav/tree/tree-node.model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,CAG3D,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,aAAa,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,IACvD,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,GAC5B,2BAA2B,CAAC,EAAE,EAAE,KAAK,CAAC,GACtC,yBAAyB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAEzC;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,IAAI,QAAQ,CAAC;IAC1E,EAAE,EAAE,EAAE,CAAC;IACP,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,2BAA2B,CACrC,EAAE,SAAS,eAAe,EAC1B,KAAK,IACH,QAAQ,CAAC;IACX,EAAE,EAAE,EAAE,CAAC;IACP,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,IAAI,CAAC;CAChB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,yBAAyB,CACnC,EAAE,SAAS,eAAe,EAC1B,KAAK,IACH,QAAQ,CAAC;IACX,EAAE,EAAE,EAAE,CAAC;IACP,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,SAAS,aAAa,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC;CAC/C,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,SAAS,eAAe,EAAE,KAAK,EACzE,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EACvB,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,GAC5B,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CA+BrC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a serializable primitive suitable for use as a tree-node ID.
|
|
4
|
+
*
|
|
5
|
+
* Strings are accepted without additional restrictions. Numbers must be
|
|
6
|
+
* finite: `NaN`, `Infinity`, and `-Infinity` are rejected.
|
|
7
|
+
*
|
|
8
|
+
* Applications can derive narrower schemas to impose domain-specific
|
|
9
|
+
* restrictions or produce branded ID types.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
*
|
|
13
|
+
* Requires Zod v4, where `z.number()` rejects non-finite values.
|
|
14
|
+
*/
|
|
15
|
+
export const serializableKeySchema = z.union([
|
|
16
|
+
z.string(),
|
|
17
|
+
z.number(),
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Creates a recursive schema for validating structural tree-node models.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
*
|
|
24
|
+
* The supplied schemas validate every node's ID and application-owned value.
|
|
25
|
+
* The returned schema validates the three node shapes recursively:
|
|
26
|
+
*
|
|
27
|
+
* - Leaves omit `children`.
|
|
28
|
+
* - Unloaded branches have `children: null`.
|
|
29
|
+
* - Loaded branches have an ordered array of valid child models.
|
|
30
|
+
*
|
|
31
|
+
* Node objects are strict: properties other than `id`, `value`, and the
|
|
32
|
+
* appropriate `children` property are rejected. Validation of properties
|
|
33
|
+
* inside `value` is controlled by `valueSchema`.
|
|
34
|
+
*
|
|
35
|
+
* Child arrays are parsed in their existing order; this schema does not sort
|
|
36
|
+
* or reorder children.
|
|
37
|
+
*
|
|
38
|
+
* This schema validates shape and field values, but does not enforce
|
|
39
|
+
* sibling-ID uniqueness. That invariant must be checked when constructing
|
|
40
|
+
* runtime tree nodes or by an additional validation step.
|
|
41
|
+
*
|
|
42
|
+
* The supplied ID schema is responsible for enforcing the runtime ID
|
|
43
|
+
* constraints, including finite numeric IDs. The generic constraint alone
|
|
44
|
+
* cannot enforce those restrictions.
|
|
45
|
+
*
|
|
46
|
+
* Inputs must describe finite trees. Cyclic object graphs are not supported.
|
|
47
|
+
*
|
|
48
|
+
* @typeParam Id - The validated node-ID type.
|
|
49
|
+
* @typeParam Value - The validated application-value type.
|
|
50
|
+
* @param idSchema - Schema used to validate every node's ID.
|
|
51
|
+
* @param valueSchema - Schema used to validate every node's application data.
|
|
52
|
+
* @returns A recursive schema for the application's tree-node models.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* const myIdSchema = serializableKeySchema
|
|
58
|
+
* .pipe(z.string().min(1))
|
|
59
|
+
* .brand<'MyId'>();
|
|
60
|
+
*
|
|
61
|
+
* type MyId = z.output<typeof myIdSchema>;
|
|
62
|
+
*
|
|
63
|
+
* const modelSchema = createTreeNodeModelSchema(
|
|
64
|
+
* myIdSchema,
|
|
65
|
+
* z.object({ label: z.string() }).strict(),
|
|
66
|
+
* );
|
|
67
|
+
*
|
|
68
|
+
* const model = modelSchema.parse({
|
|
69
|
+
* id: 'root',
|
|
70
|
+
* value: { label: 'My' },
|
|
71
|
+
* children: [
|
|
72
|
+
* {
|
|
73
|
+
* id: 'home',
|
|
74
|
+
* value: { label: 'Home' },
|
|
75
|
+
* },
|
|
76
|
+
* {
|
|
77
|
+
* id: 'settings',
|
|
78
|
+
* value: { label: 'Settings' },
|
|
79
|
+
* children: null,
|
|
80
|
+
* },
|
|
81
|
+
* ],
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function createTreeNodeModelSchema(idSchema, valueSchema) {
|
|
86
|
+
let nodeSchema;
|
|
87
|
+
nodeSchema = z.lazy(() => z.union([
|
|
88
|
+
z
|
|
89
|
+
.object({
|
|
90
|
+
id: idSchema,
|
|
91
|
+
value: valueSchema,
|
|
92
|
+
})
|
|
93
|
+
.strict(),
|
|
94
|
+
z
|
|
95
|
+
.object({
|
|
96
|
+
id: idSchema,
|
|
97
|
+
value: valueSchema,
|
|
98
|
+
children: z.null(),
|
|
99
|
+
})
|
|
100
|
+
.strict(),
|
|
101
|
+
z
|
|
102
|
+
.object({
|
|
103
|
+
id: idSchema,
|
|
104
|
+
value: valueSchema,
|
|
105
|
+
children: z.array(nodeSchema),
|
|
106
|
+
})
|
|
107
|
+
.strict(),
|
|
108
|
+
]));
|
|
109
|
+
return nodeSchema;
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-node.test.d.ts","sourceRoot":"","sources":["../../../src/state-and-nav/tree/tree-node.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as fc from 'fast-check';
|
|
3
|
+
import { createTreeMethods } from './tree.impl.js';
|
|
4
|
+
import { childrenStateModelArb, modelAndBranchPathArb, modelAndReachablePathArb, } from './tree-node.arb.js';
|
|
5
|
+
const api = createTreeMethods();
|
|
6
|
+
/**
|
|
7
|
+
* Fail rather than silently skipping assertions when an operation that should
|
|
8
|
+
* succeed returns undefined.
|
|
9
|
+
*/
|
|
10
|
+
function requireDefined(value) {
|
|
11
|
+
if (value === undefined) {
|
|
12
|
+
throw new Error('Expected a defined result');
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function fromModel(model) {
|
|
17
|
+
return requireDefined(api.fromModel(model));
|
|
18
|
+
}
|
|
19
|
+
function loadedBranch(id, children) {
|
|
20
|
+
return requireDefined(api.createLoadedBranch(id, 0, children));
|
|
21
|
+
}
|
|
22
|
+
describe('findChildById', () => {
|
|
23
|
+
it('returns the existing matching direct child', () => {
|
|
24
|
+
const child = api.createLeaf('child', 1);
|
|
25
|
+
const root = loadedBranch('root', [child]);
|
|
26
|
+
expect(api.findChildById(root, 'child')).toBe(child);
|
|
27
|
+
});
|
|
28
|
+
it('does not search descendants', () => {
|
|
29
|
+
const descendant = api.createLeaf('descendant', 1);
|
|
30
|
+
const child = loadedBranch('child', [descendant]);
|
|
31
|
+
const root = loadedBranch('root', [child]);
|
|
32
|
+
expect(api.findChildById(root, 'descendant')).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
it.each([
|
|
35
|
+
{
|
|
36
|
+
name: 'an empty loaded branch',
|
|
37
|
+
root: loadedBranch('root', []),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'a leaf',
|
|
41
|
+
root: api.createLeaf('root', 0),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'an unloaded branch',
|
|
45
|
+
root: api.createUnloadedBranch('root', 0),
|
|
46
|
+
},
|
|
47
|
+
])('returns undefined for a missing child in $name', ({ root }) => {
|
|
48
|
+
expect(api.findChildById(root, 'missing')).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('findAtPath and selectAtPath', () => {
|
|
52
|
+
it('resolves generated reachable paths', () => {
|
|
53
|
+
fc.assert(fc.property(modelAndReachablePathArb, ({ root: rootModel, path, expected }) => {
|
|
54
|
+
const root = fromModel(rootModel);
|
|
55
|
+
const selected = requireDefined(api.findAtPath(root, path));
|
|
56
|
+
expect(api.toModel(selected)).toStrictEqual(expected);
|
|
57
|
+
expect(api.selectAtPath(root, path, (node) => node)).toBe(selected);
|
|
58
|
+
expect(api.selectAtPath(root, path, (node) => api.id(node))).toBe(expected.id);
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
it('calls the selector once with the existing resolved node', () => {
|
|
62
|
+
const child = api.createLeaf('child', 1);
|
|
63
|
+
const root = loadedBranch('root', [child]);
|
|
64
|
+
const selector = vi.fn((node) => api.id(node));
|
|
65
|
+
expect(api.selectAtPath(root, ['child'], selector)).toBe('child');
|
|
66
|
+
expect(selector).toHaveBeenCalledOnce();
|
|
67
|
+
expect(selector).toHaveBeenCalledWith(child);
|
|
68
|
+
});
|
|
69
|
+
it('resolves the root for an empty path', () => {
|
|
70
|
+
const root = api.createLeaf('root', 0);
|
|
71
|
+
const selector = vi.fn((node) => node);
|
|
72
|
+
expect(api.findAtPath(root, [])).toBe(root);
|
|
73
|
+
expect(api.selectAtPath(root, [], selector)).toBe(root);
|
|
74
|
+
expect(selector).toHaveBeenCalledOnce();
|
|
75
|
+
expect(selector).toHaveBeenCalledWith(root);
|
|
76
|
+
});
|
|
77
|
+
it.each([
|
|
78
|
+
{
|
|
79
|
+
name: 'a missing child',
|
|
80
|
+
root: loadedBranch('root', []),
|
|
81
|
+
path: ['missing'],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'a missing descendant',
|
|
85
|
+
root: loadedBranch('root', [loadedBranch('child', [])]),
|
|
86
|
+
path: ['child', 'missing'],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'a path through a leaf',
|
|
90
|
+
root: loadedBranch('root', [api.createLeaf('child', 1)]),
|
|
91
|
+
path: ['child', 'descendant'],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'a path through an unloaded branch',
|
|
95
|
+
root: loadedBranch('root', [api.createUnloadedBranch('child', 1)]),
|
|
96
|
+
path: ['child', 'descendant'],
|
|
97
|
+
},
|
|
98
|
+
])('returns undefined for $name without calling the selector', ({ root, path }) => {
|
|
99
|
+
const selector = vi.fn((node) => node);
|
|
100
|
+
expect(api.findAtPath(root, path)).toBeUndefined();
|
|
101
|
+
expect(api.selectAtPath(root, path, selector)).toBeUndefined();
|
|
102
|
+
expect(selector).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
it('returns undefined when the selector returns undefined', () => {
|
|
105
|
+
const root = api.createLeaf('root', 0);
|
|
106
|
+
const selector = vi.fn((_node) => undefined);
|
|
107
|
+
expect(api.selectAtPath(root, [], selector)).toBeUndefined();
|
|
108
|
+
expect(selector).toHaveBeenCalledOnce();
|
|
109
|
+
expect(selector).toHaveBeenCalledWith(root);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('updateAtPath', () => {
|
|
113
|
+
it('replaces a generated node without changing the original tree', () => {
|
|
114
|
+
fc.assert(fc.property(modelAndReachablePathArb, ({ root: rootModel, path, expected }) => {
|
|
115
|
+
const root = fromModel(rootModel);
|
|
116
|
+
const originalNode = requireDefined(api.findAtPath(root, path));
|
|
117
|
+
const replacement = api.createLeaf(expected.id, expected.value);
|
|
118
|
+
const updatedRoot = requireDefined(api.updateAtPath(root, path, () => replacement));
|
|
119
|
+
expect(api.findAtPath(updatedRoot, path)).toBe(replacement);
|
|
120
|
+
expect(api.findAtPath(root, path)).toBe(originalNode);
|
|
121
|
+
expect(api.toModel(root)).toStrictEqual(rootModel);
|
|
122
|
+
}));
|
|
123
|
+
});
|
|
124
|
+
it('replaces child loading state at a generated branch path', () => {
|
|
125
|
+
fc.assert(fc.property(modelAndBranchPathArb, childrenStateModelArb, ({ root: rootModel, path, expected }, newChildren) => {
|
|
126
|
+
const root = fromModel(rootModel);
|
|
127
|
+
const originalNode = requireDefined(api.findAtPath(root, path));
|
|
128
|
+
const updatedRoot = requireDefined(api.updateAtPath(root, path, (node) => newChildren === null
|
|
129
|
+
? api.toUnloadedBranch(node)
|
|
130
|
+
: api.withLoadedChildren(node, newChildren.map(fromModel))));
|
|
131
|
+
const updatedNode = requireDefined(api.findAtPath(updatedRoot, path));
|
|
132
|
+
expect(updatedNode).not.toBe(originalNode);
|
|
133
|
+
expect(api.toModel(updatedNode)).toStrictEqual({
|
|
134
|
+
id: expected.id,
|
|
135
|
+
value: expected.value,
|
|
136
|
+
children: newChildren,
|
|
137
|
+
});
|
|
138
|
+
expect(api.findAtPath(root, path)).toBe(originalNode);
|
|
139
|
+
expect(api.toModel(root)).toStrictEqual(rootModel);
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
it('calls the updater once and preserves position when the ID changes', () => {
|
|
143
|
+
const before = api.createLeaf('before', 1);
|
|
144
|
+
const child = api.createLeaf('child', 2);
|
|
145
|
+
const after = api.createLeaf('after', 3);
|
|
146
|
+
const root = loadedBranch('root', [before, child, after]);
|
|
147
|
+
const replacement = api.createLeaf('replacement', 4);
|
|
148
|
+
const update = vi.fn(() => replacement);
|
|
149
|
+
const updatedRoot = requireDefined(api.updateAtPath(root, ['child'], update));
|
|
150
|
+
expect(update).toHaveBeenCalledOnce();
|
|
151
|
+
expect(update).toHaveBeenCalledWith(child);
|
|
152
|
+
expect(api.findAtPath(updatedRoot, ['replacement'])).toBe(replacement);
|
|
153
|
+
expect(api.findAtPath(updatedRoot, ['child'])).toBeUndefined();
|
|
154
|
+
const children = [...requireDefined(api.getLoadedChildren(updatedRoot))];
|
|
155
|
+
expect(children.map((node) => api.id(node))).toStrictEqual([
|
|
156
|
+
'before',
|
|
157
|
+
'replacement',
|
|
158
|
+
'after',
|
|
159
|
+
]);
|
|
160
|
+
expect(children[0]).toBe(before);
|
|
161
|
+
expect(children[1]).toBe(replacement);
|
|
162
|
+
expect(children[2]).toBe(after);
|
|
163
|
+
expect(api.findAtPath(root, ['child'])).toBe(child);
|
|
164
|
+
});
|
|
165
|
+
it('rejects a replacement ID that duplicates a sibling ID', () => {
|
|
166
|
+
const child = api.createLeaf('child', 1);
|
|
167
|
+
const sibling = api.createLeaf('sibling', 2);
|
|
168
|
+
const root = loadedBranch('root', [child, sibling]);
|
|
169
|
+
const replacement = api.createLeaf('sibling', 3);
|
|
170
|
+
const update = vi.fn(() => replacement);
|
|
171
|
+
expect(api.updateAtPath(root, ['child'], update)).toBeUndefined();
|
|
172
|
+
expect(update).toHaveBeenCalledOnce();
|
|
173
|
+
expect(update).toHaveBeenCalledWith(child);
|
|
174
|
+
expect(api.findAtPath(root, ['child'])).toBe(child);
|
|
175
|
+
expect(api.findAtPath(root, ['sibling'])).toBe(sibling);
|
|
176
|
+
});
|
|
177
|
+
it('rebuilds ancestors while preserving unrelated node identities', () => {
|
|
178
|
+
const target = api.createLeaf('target', 1);
|
|
179
|
+
const sibling = api.createLeaf('sibling', 2);
|
|
180
|
+
const branch = loadedBranch('branch', [target, sibling]);
|
|
181
|
+
const otherBranch = loadedBranch('other', []);
|
|
182
|
+
const root = loadedBranch('root', [branch, otherBranch]);
|
|
183
|
+
const replacement = api.createLeaf('target', 3);
|
|
184
|
+
const updatedRoot = requireDefined(api.updateAtPath(root, ['branch', 'target'], () => replacement));
|
|
185
|
+
expect(updatedRoot).not.toBe(root);
|
|
186
|
+
expect(api.findAtPath(updatedRoot, ['branch'])).not.toBe(branch);
|
|
187
|
+
expect(api.findAtPath(updatedRoot, ['branch', 'target'])).toBe(replacement);
|
|
188
|
+
expect(api.findAtPath(updatedRoot, ['branch', 'sibling'])).toBe(sibling);
|
|
189
|
+
expect(api.findAtPath(updatedRoot, ['other'])).toBe(otherBranch);
|
|
190
|
+
expect(api.findAtPath(root, ['branch', 'target'])).toBe(target);
|
|
191
|
+
});
|
|
192
|
+
it('replaces the root for an empty path', () => {
|
|
193
|
+
const root = api.createLeaf('root', 0);
|
|
194
|
+
const replacement = api.createLeaf('replacement', 1);
|
|
195
|
+
const update = vi.fn(() => replacement);
|
|
196
|
+
expect(api.updateAtPath(root, [], update)).toBe(replacement);
|
|
197
|
+
expect(update).toHaveBeenCalledOnce();
|
|
198
|
+
expect(update).toHaveBeenCalledWith(root);
|
|
199
|
+
});
|
|
200
|
+
it.each([
|
|
201
|
+
{
|
|
202
|
+
name: 'a missing child',
|
|
203
|
+
root: loadedBranch('root', []),
|
|
204
|
+
path: ['missing'],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'a path through a leaf',
|
|
208
|
+
root: api.createLeaf('root', 0),
|
|
209
|
+
path: ['child'],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'a path through an unloaded branch',
|
|
213
|
+
root: api.createUnloadedBranch('root', 0),
|
|
214
|
+
path: ['child'],
|
|
215
|
+
},
|
|
216
|
+
])('returns undefined for $name without calling the updater', ({ root, path }) => {
|
|
217
|
+
const update = vi.fn((node) => node);
|
|
218
|
+
expect(api.updateAtPath(root, path, update)).toBeUndefined();
|
|
219
|
+
expect(update).not.toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
it.each([
|
|
222
|
+
{ name: 'the root', path: [] },
|
|
223
|
+
{ name: 'a descendant', path: ['child'] },
|
|
224
|
+
])('returns undefined when updating $name is aborted', ({ path }) => {
|
|
225
|
+
const child = api.createLeaf('child', 1);
|
|
226
|
+
const root = loadedBranch('root', [child]);
|
|
227
|
+
const selected = requireDefined(api.findAtPath(root, path));
|
|
228
|
+
const update = vi.fn(() => undefined);
|
|
229
|
+
expect(api.updateAtPath(root, path, update)).toBeUndefined();
|
|
230
|
+
expect(update).toHaveBeenCalledOnce();
|
|
231
|
+
expect(update).toHaveBeenCalledWith(selected);
|
|
232
|
+
expect(api.findAtPath(root, ['child'])).toBe(child);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TreeMethods } from './tree.types.js';
|
|
2
2
|
import type { SerializableKey } from './tree-node.model.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates an API for constructing, inspecting, and immutably updating opaque
|
|
@@ -11,5 +11,5 @@ import type { SerializableKey } from './tree-node.model.js';
|
|
|
11
11
|
* Tree updates preserve object identity for unrelated nodes. New ancestor
|
|
12
12
|
* nodes and child collections are created only along an updated path.
|
|
13
13
|
*/
|
|
14
|
-
export declare function
|
|
15
|
-
//# sourceMappingURL=tree
|
|
14
|
+
export declare function createTreeMethods<Id extends SerializableKey, Value>(): TreeMethods<Id, Value>;
|
|
15
|
+
//# sourceMappingURL=tree.impl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.impl.d.ts","sourceRoot":"","sources":["../../../src/state-and-nav/tree/tree.impl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,WAAW,EAGZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,sBAAsB,CAAC;AA2J3E;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,SAAS,eAAe,EAC1B,KAAK,KACF,WAAW,CAAC,EAAE,EAAE,KAAK,CAAC,CAgR1B"}
|
|
@@ -13,6 +13,9 @@ function toData(node) {
|
|
|
13
13
|
function fromData(data) {
|
|
14
14
|
return data;
|
|
15
15
|
}
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Helpers
|
|
18
|
+
// =============================================================================
|
|
16
19
|
function isLeafData(node) {
|
|
17
20
|
return !('children' in node);
|
|
18
21
|
}
|
|
@@ -23,11 +26,8 @@ function isLoadedBranchData(node) {
|
|
|
23
26
|
return 'children' in node && node.children !== null;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
|
-
* Returns a reusable iterable over child nodes
|
|
27
|
-
* child
|
|
28
|
-
*
|
|
29
|
-
* The implementation currently stores children in an array, but callers only
|
|
30
|
-
* receive an `Iterable` and must not rely on iteration order.
|
|
29
|
+
* Returns a reusable iterable over child nodes in their stored order without
|
|
30
|
+
* exposing the internal child array.
|
|
31
31
|
*/
|
|
32
32
|
function asChildrenIterable(children) {
|
|
33
33
|
return Object.freeze({
|
|
@@ -45,14 +45,15 @@ function asChildrenIterable(children) {
|
|
|
45
45
|
* for `createLoadedBranch` and `withLoadedChildren`.
|
|
46
46
|
*/
|
|
47
47
|
function copyUniqueChildren(children, getId) {
|
|
48
|
-
const copiedChildren = [
|
|
48
|
+
const copiedChildren = [];
|
|
49
49
|
const seenIds = new Set();
|
|
50
|
-
for (const child of
|
|
50
|
+
for (const child of children) {
|
|
51
51
|
const id = getId(child);
|
|
52
52
|
if (seenIds.has(id)) {
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
55
55
|
seenIds.add(id);
|
|
56
|
+
copiedChildren.push(child);
|
|
56
57
|
}
|
|
57
58
|
return Object.freeze(copiedChildren);
|
|
58
59
|
}
|
|
@@ -76,6 +77,9 @@ function createLoadedBranchData(id, value, children) {
|
|
|
76
77
|
children,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Generic Tree Methods Init
|
|
82
|
+
// =============================================================================
|
|
79
83
|
/**
|
|
80
84
|
* Creates an API for constructing, inspecting, and immutably updating opaque
|
|
81
85
|
* tree nodes.
|
|
@@ -87,8 +91,8 @@ function createLoadedBranchData(id, value, children) {
|
|
|
87
91
|
* Tree updates preserve object identity for unrelated nodes. New ancestor
|
|
88
92
|
* nodes and child collections are created only along an updated path.
|
|
89
93
|
*/
|
|
90
|
-
export function
|
|
91
|
-
function
|
|
94
|
+
export function createTreeMethods() {
|
|
95
|
+
function getLoadedChildrenData(node) {
|
|
92
96
|
const data = toData(node);
|
|
93
97
|
return isLoadedBranchData(data) ? data.children : undefined;
|
|
94
98
|
}
|
|
@@ -143,11 +147,11 @@ export function createTreeNodeApi() {
|
|
|
143
147
|
});
|
|
144
148
|
},
|
|
145
149
|
getLoadedChildren(node) {
|
|
146
|
-
const children =
|
|
150
|
+
const children = getLoadedChildrenData(node);
|
|
147
151
|
return children === undefined ? undefined : asChildrenIterable(children);
|
|
148
152
|
},
|
|
149
153
|
findChildById(node, id) {
|
|
150
|
-
const children =
|
|
154
|
+
const children = getLoadedChildrenData(node);
|
|
151
155
|
if (children === undefined) {
|
|
152
156
|
return undefined;
|
|
153
157
|
}
|
|
@@ -245,7 +249,7 @@ export function createTreeNodeApi() {
|
|
|
245
249
|
};
|
|
246
250
|
function modifyChildAtPath(node, path, modifier) {
|
|
247
251
|
const childId = path[0];
|
|
248
|
-
const children =
|
|
252
|
+
const children = getLoadedChildrenData(node);
|
|
249
253
|
if (childId === undefined || children === undefined) {
|
|
250
254
|
return undefined;
|
|
251
255
|
}
|
|
@@ -270,14 +274,13 @@ export function createTreeNodeApi() {
|
|
|
270
274
|
return undefined;
|
|
271
275
|
}
|
|
272
276
|
/*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
* this is solely an implementation detail.
|
|
277
|
+
* Replace the child at its existing position, preserving sibling order even
|
|
278
|
+
* when the replacement has a different ID.
|
|
276
279
|
*/
|
|
277
280
|
const updatedChildren = [...children];
|
|
278
281
|
updatedChildren[childIndex] = updatedChild;
|
|
279
282
|
/*
|
|
280
|
-
* `
|
|
283
|
+
* `withLoadedChildren` revalidates sibling-ID uniqueness. This matters when
|
|
281
284
|
* `modifier` replaces a node with another node having a different ID.
|
|
282
285
|
*/
|
|
283
286
|
return api.withLoadedChildren(node, updatedChildren);
|