jotai-state-tree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/map.ts ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Map type implementation
3
+ */
4
+
5
+ import type {
6
+ IMapType,
7
+ IMSTMap,
8
+ IType,
9
+ IValidationContext,
10
+ IValidationResult,
11
+ IAnyType,
12
+ } from './types';
13
+ import {
14
+ StateTreeNode,
15
+ $treenode,
16
+ getStateTreeNode,
17
+ } from './tree';
18
+
19
+ // ============================================================================
20
+ // MST Map Implementation
21
+ // ============================================================================
22
+
23
+ class MSTMap<V> extends Map<string, V> implements IMSTMap<V> {
24
+ private node!: StateTreeNode;
25
+ private valueType!: IAnyType;
26
+ private initialized = false;
27
+
28
+ constructor(node: StateTreeNode, valueType: IAnyType, entries?: [string, V][]) {
29
+ super();
30
+ this.node = node;
31
+ this.valueType = valueType;
32
+ this.initialized = true;
33
+
34
+ // Add entries after initialization
35
+ if (entries) {
36
+ for (const [key, value] of entries) {
37
+ super.set(key, value);
38
+ }
39
+ this.syncToNode();
40
+ }
41
+ }
42
+
43
+ put(value: V): V {
44
+ // Get the identifier from the value if it's a model with identifier
45
+ let key: string;
46
+ if (value && typeof value === 'object' && $treenode in value) {
47
+ const valueNode = getStateTreeNode(value);
48
+ if (valueNode.identifierValue !== undefined) {
49
+ key = String(valueNode.identifierValue);
50
+ } else {
51
+ throw new Error('[jotai-state-tree] Cannot put a value without an identifier into a map');
52
+ }
53
+ } else {
54
+ throw new Error('[jotai-state-tree] Cannot put a non-model value using put()');
55
+ }
56
+
57
+ this.set(key, value);
58
+ return value;
59
+ }
60
+
61
+ merge(values: Record<string, V> | Map<string, V>): this {
62
+ const entries = values instanceof Map ? values.entries() : Object.entries(values);
63
+ for (const [key, value] of entries) {
64
+ this.set(key, value);
65
+ }
66
+ return this;
67
+ }
68
+
69
+ replace(values: Record<string, V> | Map<string, V>): this {
70
+ this.clear();
71
+ return this.merge(values);
72
+ }
73
+
74
+ // Override mutating methods to sync
75
+ set(key: string, value: V): this {
76
+ super.set(key, value);
77
+ if (this.initialized) {
78
+ this.syncToNode();
79
+ }
80
+ return this;
81
+ }
82
+
83
+ // Override get to return the instance from child node for complex types
84
+ get(key: string): V | undefined {
85
+ if (this.valueType._kind === 'model' || this.valueType._kind === 'array' || this.valueType._kind === 'map') {
86
+ const childNode = this.node.getChild(key);
87
+ if (childNode) {
88
+ return childNode.getInstance() as V;
89
+ }
90
+ return undefined;
91
+ }
92
+ return super.get(key);
93
+ }
94
+
95
+ delete(key: string): boolean {
96
+ const result = super.delete(key);
97
+ if (result && this.initialized) {
98
+ this.syncToNode();
99
+ }
100
+ return result;
101
+ }
102
+
103
+ clear(): void {
104
+ super.clear();
105
+ if (this.initialized) {
106
+ this.syncToNode();
107
+ }
108
+ }
109
+
110
+ toJSON(): Record<string, V> {
111
+ const result: Record<string, V> = {};
112
+ this.forEach((value, key) => {
113
+ result[key] = value;
114
+ });
115
+ return result;
116
+ }
117
+
118
+ private syncToNode(): void {
119
+ // Get current children
120
+ const existingChildren = new Map(this.node.getChildren());
121
+ const newChildren = new Map<string, StateTreeNode>();
122
+
123
+ // Create new children for each entry
124
+ this.forEach((value, key) => {
125
+ if (this.valueType._kind === 'model' || this.valueType._kind === 'array' || this.valueType._kind === 'map') {
126
+ if (value && typeof value === 'object' && $treenode in value) {
127
+ const childNode = getStateTreeNode(value);
128
+ newChildren.set(key, childNode);
129
+ } else {
130
+ const childInstance = this.valueType.create(value);
131
+ const childNode = getStateTreeNode(childInstance);
132
+ newChildren.set(key, childNode);
133
+ super.set(key, childInstance as V);
134
+ }
135
+ } else {
136
+ const existingChild = existingChildren.get(key);
137
+ if (existingChild && existingChild.getValue() === value) {
138
+ newChildren.set(key, existingChild);
139
+ } else {
140
+ const childNode = new StateTreeNode(this.valueType, value, this.node.$env);
141
+ newChildren.set(key, childNode);
142
+ }
143
+ }
144
+ });
145
+
146
+ // Destroy children that are no longer in the map
147
+ for (const [key, child] of existingChildren) {
148
+ if (!newChildren.has(key)) {
149
+ child.destroy();
150
+ }
151
+ }
152
+
153
+ // Clear and set new children
154
+ this.node.getChildren().clear();
155
+ for (const [key, childNode] of newChildren) {
156
+ this.node.addChild(key, childNode);
157
+ }
158
+
159
+ // Update node value
160
+ this.node.setValue(this.toJSON());
161
+ }
162
+ }
163
+
164
+ // ============================================================================
165
+ // Map Type Implementation
166
+ // ============================================================================
167
+
168
+ class MapType<T extends IAnyType> implements IMapType<T> {
169
+ readonly _kind = 'map' as const;
170
+ readonly _subType: T;
171
+ readonly name: string;
172
+
173
+ readonly _C!: Record<string, T extends IType<infer C, unknown, unknown> ? C : never>;
174
+ readonly _S!: Record<string, T extends IType<unknown, infer S, unknown> ? S : never>;
175
+ readonly _T!: IMSTMap<T extends IType<unknown, unknown, infer I> ? I : never>;
176
+
177
+ constructor(valueType: T) {
178
+ this._subType = valueType;
179
+ this.name = `map<${valueType.name}>`;
180
+ }
181
+
182
+ create(
183
+ snapshot?: Record<string, T extends IType<infer C, unknown, unknown> ? C : never>,
184
+ env?: unknown
185
+ ): IMSTMap<T extends IType<unknown, unknown, infer I> ? I : never> {
186
+ const entries = snapshot ?? {};
187
+
188
+ // Create tree node
189
+ const node = new StateTreeNode(this, entries, env);
190
+
191
+ // Create instances for each entry
192
+ const instanceEntries: [string, unknown][] = Object.entries(entries).map(([key, value]) => {
193
+ const instance = this._subType.create(value, env);
194
+
195
+ // Add as child node
196
+ if (this._subType._kind === 'model' || this._subType._kind === 'array' || this._subType._kind === 'map') {
197
+ const childNode = getStateTreeNode(instance);
198
+ node.addChild(key, childNode);
199
+ } else {
200
+ const childNode = new StateTreeNode(this._subType, instance, env, node, key);
201
+ node.addChild(key, childNode);
202
+ }
203
+
204
+ return [key, instance];
205
+ });
206
+
207
+ // Create the MST map
208
+ const mstMap = new MSTMap(
209
+ node,
210
+ this._subType,
211
+ instanceEntries as [string, T extends IType<unknown, unknown, infer I> ? I : never][]
212
+ ) as IMSTMap<T extends IType<unknown, unknown, infer I> ? I : never>;
213
+
214
+ // Add tree node reference
215
+ Object.defineProperty(mstMap, $treenode, {
216
+ value: node,
217
+ writable: false,
218
+ enumerable: false,
219
+ });
220
+
221
+ node.setInstance(mstMap);
222
+
223
+ return mstMap;
224
+ }
225
+
226
+ is(value: unknown): value is IMSTMap<T extends IType<unknown, unknown, infer I> ? I : never> {
227
+ if (!(value instanceof Map)) return false;
228
+ return $treenode in value;
229
+ }
230
+
231
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
232
+ const errors: IValidationResult['errors'] = [];
233
+
234
+ if (typeof value !== 'object' || value === null) {
235
+ return {
236
+ valid: false,
237
+ errors: [
238
+ {
239
+ context,
240
+ value,
241
+ message: 'Value is not an object or Map',
242
+ },
243
+ ],
244
+ };
245
+ }
246
+
247
+ // Handle both plain objects and Maps
248
+ const entries = value instanceof Map ? Array.from(value.entries()) : Object.entries(value);
249
+
250
+ for (const [key, itemValue] of entries) {
251
+ const itemContext: IValidationContext = {
252
+ path: context.length > 0 ? `${context[0].path}/${key}` : `/${key}`,
253
+ type: this._subType,
254
+ parent: value,
255
+ };
256
+
257
+ const result = this._subType.validate(itemValue, [...context, itemContext]);
258
+ if (!result.valid) {
259
+ errors.push(...result.errors);
260
+ }
261
+ }
262
+
263
+ return {
264
+ valid: errors.length === 0,
265
+ errors,
266
+ };
267
+ }
268
+ }
269
+
270
+ // ============================================================================
271
+ // Factory Function
272
+ // ============================================================================
273
+
274
+ export function map<T extends IAnyType>(valueType: T): IMapType<T> {
275
+ return new MapType(valueType);
276
+ }