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/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/chunk-XXZK62DD.mjs +931 -0
- package/dist/index.d.mts +1109 -0
- package/dist/index.d.ts +1109 -0
- package/dist/index.js +3579 -0
- package/dist/index.mjs +2625 -0
- package/dist/react.d.mts +144 -0
- package/dist/react.d.ts +144 -0
- package/dist/react.js +1259 -0
- package/dist/react.mjs +372 -0
- package/package.json +77 -0
- package/src/__tests__/index.test.ts +1371 -0
- package/src/__tests__/memory.test.ts +681 -0
- package/src/__tests__/performance.test.ts +667 -0
- package/src/__tests__/react.react.test.tsx +811 -0
- package/src/__tests__/registry.test.ts +589 -0
- package/src/array.ts +335 -0
- package/src/compat.ts +294 -0
- package/src/index.ts +647 -0
- package/src/lifecycle.ts +580 -0
- package/src/map.ts +276 -0
- package/src/model.ts +832 -0
- package/src/primitives.ts +400 -0
- package/src/react.ts +626 -0
- package/src/registry.ts +741 -0
- package/src/tree.ts +1275 -0
- package/src/types.ts +520 -0
- package/src/undo.ts +566 -0
- package/src/utilities.ts +616 -0
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
|
+
}
|