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/model.ts
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model type implementation
|
|
3
|
+
* This is the core of jotai-state-tree
|
|
4
|
+
*
|
|
5
|
+
* MEMORY MANAGEMENT:
|
|
6
|
+
* - View and action caches are bounded with LRU eviction
|
|
7
|
+
* - Caches are instance-scoped, so they're GC'd with the instance
|
|
8
|
+
* - No global caches that could accumulate entries
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { atom, type WritableAtom } from "jotai";
|
|
12
|
+
import type {
|
|
13
|
+
IModelType,
|
|
14
|
+
ModelProperties,
|
|
15
|
+
ModelCreationType,
|
|
16
|
+
ModelSnapshotType,
|
|
17
|
+
ModelInstance,
|
|
18
|
+
ModelViews,
|
|
19
|
+
ModelActions,
|
|
20
|
+
ModelVolatile,
|
|
21
|
+
IType,
|
|
22
|
+
IValidationContext,
|
|
23
|
+
IValidationResult,
|
|
24
|
+
IAnyType,
|
|
25
|
+
} from "./types";
|
|
26
|
+
import {
|
|
27
|
+
StateTreeNode,
|
|
28
|
+
$treenode,
|
|
29
|
+
getStateTreeNode,
|
|
30
|
+
trackAction,
|
|
31
|
+
getGlobalStore,
|
|
32
|
+
} from "./tree";
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// LRU Cache Implementation
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/** Maximum entries in view/action caches per instance */
|
|
39
|
+
const MAX_CACHE_SIZE = 100;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Simple LRU cache with bounded size
|
|
43
|
+
* When capacity is reached, oldest entries are evicted
|
|
44
|
+
*/
|
|
45
|
+
class LRUCache<K, V> {
|
|
46
|
+
private cache = new Map<K, V>();
|
|
47
|
+
private maxSize: number;
|
|
48
|
+
|
|
49
|
+
constructor(maxSize: number = MAX_CACHE_SIZE) {
|
|
50
|
+
this.maxSize = maxSize;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get(key: K): V | undefined {
|
|
54
|
+
const value = this.cache.get(key);
|
|
55
|
+
if (value !== undefined) {
|
|
56
|
+
// Move to end (most recently used)
|
|
57
|
+
this.cache.delete(key);
|
|
58
|
+
this.cache.set(key, value);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
set(key: K, value: V): void {
|
|
64
|
+
// Delete first if exists to update position
|
|
65
|
+
if (this.cache.has(key)) {
|
|
66
|
+
this.cache.delete(key);
|
|
67
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
68
|
+
// Evict oldest (first) entry
|
|
69
|
+
const firstKey = this.cache.keys().next().value;
|
|
70
|
+
if (firstKey !== undefined) {
|
|
71
|
+
this.cache.delete(firstKey);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
this.cache.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
has(key: K): boolean {
|
|
78
|
+
return this.cache.has(key);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clear(): void {
|
|
82
|
+
this.cache.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get size(): number {
|
|
86
|
+
return this.cache.size;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Model Type Factory
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
interface LifecycleHooks<Self> {
|
|
95
|
+
afterCreate?: (self: Self) => void;
|
|
96
|
+
afterAttach?: (self: Self) => void;
|
|
97
|
+
beforeDetach?: (self: Self) => void;
|
|
98
|
+
beforeDestroy?: (self: Self) => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface ModelTypeConfig<
|
|
102
|
+
P extends ModelProperties,
|
|
103
|
+
V extends object,
|
|
104
|
+
A extends object,
|
|
105
|
+
Vol extends object,
|
|
106
|
+
> {
|
|
107
|
+
name: string;
|
|
108
|
+
properties: P;
|
|
109
|
+
views: ModelViews<ModelInstance<P, V, A, Vol> & V & A & Vol, V>[];
|
|
110
|
+
actions: ModelActions<ModelInstance<P, V, A, Vol> & V & A & Vol, A>[];
|
|
111
|
+
volatiles: ModelVolatile<ModelInstance<P, V, A, Vol> & V & A & Vol, Vol>[];
|
|
112
|
+
preProcessor?: (snapshot: unknown) => ModelCreationType<P>;
|
|
113
|
+
postProcessor?: (snapshot: ModelSnapshotType<P>) => unknown;
|
|
114
|
+
initializers: Array<
|
|
115
|
+
(self: ModelInstance<P, V, A, Vol> & V & A & Vol) => void
|
|
116
|
+
>;
|
|
117
|
+
hooks: LifecycleHooks<ModelInstance<P, V, A, Vol> & V & A & Vol>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class ModelType<
|
|
121
|
+
P extends ModelProperties,
|
|
122
|
+
V extends object,
|
|
123
|
+
A extends object,
|
|
124
|
+
Vol extends object,
|
|
125
|
+
> implements IModelType<P, V, A, Vol>
|
|
126
|
+
{
|
|
127
|
+
readonly _kind = "model" as const;
|
|
128
|
+
readonly _C!: ModelCreationType<P>;
|
|
129
|
+
readonly _S!: ModelSnapshotType<P>;
|
|
130
|
+
readonly _T!: ModelInstance<P, V, A, Vol> & V & A & Vol;
|
|
131
|
+
|
|
132
|
+
readonly name: string;
|
|
133
|
+
readonly properties: P;
|
|
134
|
+
readonly identifierAttribute?: string;
|
|
135
|
+
|
|
136
|
+
private config: ModelTypeConfig<P, V, A, Vol>;
|
|
137
|
+
|
|
138
|
+
constructor(config: ModelTypeConfig<P, V, A, Vol>) {
|
|
139
|
+
this.config = config;
|
|
140
|
+
this.name = config.name;
|
|
141
|
+
this.properties = config.properties;
|
|
142
|
+
|
|
143
|
+
// Find identifier attribute
|
|
144
|
+
for (const [key, type] of Object.entries(config.properties)) {
|
|
145
|
+
if (
|
|
146
|
+
(type as IAnyType)._kind === "identifier" ||
|
|
147
|
+
(type as IAnyType)._kind === "identifierNumber"
|
|
148
|
+
) {
|
|
149
|
+
this.identifierAttribute = key;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
create(
|
|
156
|
+
snapshot?: ModelCreationType<P>,
|
|
157
|
+
env?: unknown,
|
|
158
|
+
): ModelInstance<P, V, A, Vol> & V & A & Vol {
|
|
159
|
+
// Apply pre-processor if exists
|
|
160
|
+
let processedSnapshot = snapshot ?? {};
|
|
161
|
+
if (this.config.preProcessor) {
|
|
162
|
+
processedSnapshot = this.config.preProcessor(
|
|
163
|
+
processedSnapshot,
|
|
164
|
+
) as ModelCreationType<P>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create the tree node
|
|
168
|
+
const node = new StateTreeNode(this, processedSnapshot, env);
|
|
169
|
+
node.preProcessor = this.config.preProcessor as
|
|
170
|
+
| ((snapshot: unknown) => unknown)
|
|
171
|
+
| undefined;
|
|
172
|
+
node.postProcessor = this.config.postProcessor as
|
|
173
|
+
| ((snapshot: unknown) => unknown)
|
|
174
|
+
| undefined;
|
|
175
|
+
|
|
176
|
+
// Create property atoms and child nodes
|
|
177
|
+
const propertyAtoms = new Map<
|
|
178
|
+
string,
|
|
179
|
+
WritableAtom<unknown, [unknown], void>
|
|
180
|
+
>();
|
|
181
|
+
const store = getGlobalStore();
|
|
182
|
+
|
|
183
|
+
for (const [key, propType] of Object.entries(this.properties)) {
|
|
184
|
+
const type = propType as IAnyType;
|
|
185
|
+
const initialValue = (processedSnapshot as Record<string, unknown>)?.[
|
|
186
|
+
key
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// Check if this is a complex type that creates its own tree node
|
|
190
|
+
// This includes direct model/array/map, or wrapper types like maybe/late that contain them
|
|
191
|
+
const isComplexType =
|
|
192
|
+
type._kind === "model" ||
|
|
193
|
+
type._kind === "array" ||
|
|
194
|
+
type._kind === "map";
|
|
195
|
+
|
|
196
|
+
if (isComplexType) {
|
|
197
|
+
// Complex types create their own nodes
|
|
198
|
+
const childInstance = type.create(initialValue, env);
|
|
199
|
+
const childNode = getStateTreeNode(childInstance);
|
|
200
|
+
node.addChild(key, childNode);
|
|
201
|
+
propertyAtoms.set(key, childNode.valueAtom);
|
|
202
|
+
} else {
|
|
203
|
+
// For wrapper types (maybe, late, optional, etc.), create the value first
|
|
204
|
+
// and check if it has a tree node (meaning it wraps a complex type)
|
|
205
|
+
const value = type.create(initialValue, env);
|
|
206
|
+
|
|
207
|
+
// Check if the created value has a tree node (complex type inside wrapper)
|
|
208
|
+
if (value && typeof value === "object" && $treenode in value) {
|
|
209
|
+
const childNode = getStateTreeNode(value);
|
|
210
|
+
node.addChild(key, childNode);
|
|
211
|
+
propertyAtoms.set(key, childNode.valueAtom);
|
|
212
|
+
} else {
|
|
213
|
+
// Simple/primitive types use direct atoms
|
|
214
|
+
const propAtom = atom(value);
|
|
215
|
+
propertyAtoms.set(key, propAtom);
|
|
216
|
+
|
|
217
|
+
// Create a "virtual" child node for the property
|
|
218
|
+
const childNode = new StateTreeNode(type, value, env, node, key);
|
|
219
|
+
childNode.valueAtom = propAtom as unknown as WritableAtom<
|
|
220
|
+
unknown,
|
|
221
|
+
[unknown],
|
|
222
|
+
void
|
|
223
|
+
>;
|
|
224
|
+
node.addChild(key, childNode);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Build the instance proxy
|
|
230
|
+
const instance = this.createInstanceProxy(node, propertyAtoms, store);
|
|
231
|
+
|
|
232
|
+
// Register identifier if present
|
|
233
|
+
if (this.identifierAttribute) {
|
|
234
|
+
const idValue = (processedSnapshot as Record<string, unknown>)?.[
|
|
235
|
+
this.identifierAttribute
|
|
236
|
+
];
|
|
237
|
+
if (idValue !== undefined) {
|
|
238
|
+
node.registerIdentifier(this.name, idValue as string | number);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Set instance on node
|
|
243
|
+
node.setInstance(instance);
|
|
244
|
+
|
|
245
|
+
// Run initializers (afterCreate hooks)
|
|
246
|
+
for (const initializer of this.config.initializers) {
|
|
247
|
+
initializer(instance);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Run afterCreate lifecycle hook
|
|
251
|
+
if (this.config.hooks.afterCreate) {
|
|
252
|
+
this.config.hooks.afterCreate(instance);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return instance;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private createInstanceProxy(
|
|
259
|
+
node: StateTreeNode,
|
|
260
|
+
propertyAtoms: Map<string, WritableAtom<unknown, [unknown], void>>,
|
|
261
|
+
store: ReturnType<typeof getGlobalStore>,
|
|
262
|
+
): ModelInstance<P, V, A, Vol> & V & A & Vol {
|
|
263
|
+
const self = this;
|
|
264
|
+
// Use bounded LRU caches to prevent unbounded memory growth
|
|
265
|
+
// These caches are instance-scoped and will be GC'd with the instance
|
|
266
|
+
const viewCache = new LRUCache<string, unknown>(MAX_CACHE_SIZE);
|
|
267
|
+
const actionCache = new LRUCache<string, Function>(MAX_CACHE_SIZE);
|
|
268
|
+
|
|
269
|
+
// Collect all views
|
|
270
|
+
const allViews: Record<string, PropertyDescriptor> = {};
|
|
271
|
+
|
|
272
|
+
// Collect all actions
|
|
273
|
+
const allActions: Record<string, Function> = {};
|
|
274
|
+
|
|
275
|
+
// Collect volatile state
|
|
276
|
+
const volatileState: Record<string, unknown> = {};
|
|
277
|
+
|
|
278
|
+
// Create base object with tree node reference
|
|
279
|
+
const base = {
|
|
280
|
+
[$treenode]: node,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Create the proxy
|
|
284
|
+
const proxy = new Proxy(base, {
|
|
285
|
+
get(target, prop) {
|
|
286
|
+
// Handle symbol access
|
|
287
|
+
if (prop === $treenode) {
|
|
288
|
+
return node;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Handle $treenode string access
|
|
292
|
+
if (prop === "$treenode") {
|
|
293
|
+
return node;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const propStr = String(prop);
|
|
297
|
+
|
|
298
|
+
// Check properties first
|
|
299
|
+
if (propertyAtoms.has(propStr)) {
|
|
300
|
+
const childNode = node.getChild(propStr);
|
|
301
|
+
if (childNode) {
|
|
302
|
+
// Check if the child node has an instance (complex types like model, array, map)
|
|
303
|
+
// This handles both direct complex types and wrapper types (maybe, late, optional)
|
|
304
|
+
// that contain complex types
|
|
305
|
+
const instance = childNode.getInstance();
|
|
306
|
+
if (instance !== undefined) {
|
|
307
|
+
// Check if instance is a state tree node (complex type)
|
|
308
|
+
if (
|
|
309
|
+
instance &&
|
|
310
|
+
typeof instance === "object" &&
|
|
311
|
+
$treenode in instance
|
|
312
|
+
) {
|
|
313
|
+
return instance;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// For primitive types, get from atom
|
|
317
|
+
return store.get(propertyAtoms.get(propStr)!);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check volatile state
|
|
322
|
+
if (propStr in node.volatileState) {
|
|
323
|
+
return node.volatileState[propStr];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check views
|
|
327
|
+
if (propStr in allViews) {
|
|
328
|
+
const descriptor = allViews[propStr];
|
|
329
|
+
if (descriptor.get) {
|
|
330
|
+
return descriptor.get.call(proxy);
|
|
331
|
+
}
|
|
332
|
+
if (typeof descriptor.value === "function") {
|
|
333
|
+
return descriptor.value.bind(proxy);
|
|
334
|
+
}
|
|
335
|
+
return descriptor.value;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check actions
|
|
339
|
+
if (propStr in allActions) {
|
|
340
|
+
return allActions[propStr];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return undefined;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
set(target, prop, value) {
|
|
347
|
+
const propStr = String(prop);
|
|
348
|
+
|
|
349
|
+
// Check if it's a property
|
|
350
|
+
if (propertyAtoms.has(propStr)) {
|
|
351
|
+
const propType = (self.properties as Record<string, IAnyType>)[
|
|
352
|
+
propStr
|
|
353
|
+
];
|
|
354
|
+
const existingChildNode = node.getChild(propStr);
|
|
355
|
+
|
|
356
|
+
// Handle direct complex types
|
|
357
|
+
if (propType._kind === "model") {
|
|
358
|
+
if (existingChildNode) {
|
|
359
|
+
// For models, apply snapshot
|
|
360
|
+
const { applySnapshotToNode } = require("./tree");
|
|
361
|
+
applySnapshotToNode(existingChildNode, value);
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (propType._kind === "array" || propType._kind === "map") {
|
|
367
|
+
if (existingChildNode) {
|
|
368
|
+
// For arrays/maps, replace content
|
|
369
|
+
existingChildNode.setValue(value);
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Handle wrapper types (maybe, late, optional, etc.) and primitives
|
|
375
|
+
// These may contain complex types that need proper lifecycle management
|
|
376
|
+
|
|
377
|
+
// Get old value for patch
|
|
378
|
+
const oldValue = existingChildNode?.getValue();
|
|
379
|
+
|
|
380
|
+
// Destroy the old child node if it exists
|
|
381
|
+
if (existingChildNode) {
|
|
382
|
+
existingChildNode.destroy();
|
|
383
|
+
node.getChildren().delete(propStr);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Create new value through the type
|
|
387
|
+
const newValue = propType.create(value, node.$env);
|
|
388
|
+
|
|
389
|
+
// Check if the new value is a complex type (has tree node)
|
|
390
|
+
if (
|
|
391
|
+
newValue &&
|
|
392
|
+
typeof newValue === "object" &&
|
|
393
|
+
$treenode in newValue
|
|
394
|
+
) {
|
|
395
|
+
const newChildNode = getStateTreeNode(newValue);
|
|
396
|
+
node.addChild(propStr, newChildNode);
|
|
397
|
+
propertyAtoms.set(propStr, newChildNode.valueAtom);
|
|
398
|
+
} else {
|
|
399
|
+
// Primitive value - create a new child node for it
|
|
400
|
+
const newChildNode = new StateTreeNode(
|
|
401
|
+
propType,
|
|
402
|
+
newValue,
|
|
403
|
+
node.$env,
|
|
404
|
+
node,
|
|
405
|
+
propStr,
|
|
406
|
+
);
|
|
407
|
+
const propAtom = atom(newValue);
|
|
408
|
+
newChildNode.valueAtom = propAtom as unknown as WritableAtom<
|
|
409
|
+
unknown,
|
|
410
|
+
[unknown],
|
|
411
|
+
void
|
|
412
|
+
>;
|
|
413
|
+
node.addChild(propStr, newChildNode);
|
|
414
|
+
propertyAtoms.set(propStr, propAtom);
|
|
415
|
+
store.set(propAtom, newValue);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Notify about the change - use node's notification methods
|
|
419
|
+
node.notifyPropertyChange(propStr, newValue, oldValue);
|
|
420
|
+
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if it's volatile state
|
|
425
|
+
if (propStr in node.volatileState) {
|
|
426
|
+
node.volatileState[propStr] = value;
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return false;
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
has(target, prop) {
|
|
434
|
+
const propStr = String(prop);
|
|
435
|
+
return (
|
|
436
|
+
prop === $treenode ||
|
|
437
|
+
propertyAtoms.has(propStr) ||
|
|
438
|
+
propStr in allViews ||
|
|
439
|
+
propStr in allActions ||
|
|
440
|
+
propStr in node.volatileState
|
|
441
|
+
);
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
ownKeys() {
|
|
445
|
+
return [
|
|
446
|
+
...propertyAtoms.keys(),
|
|
447
|
+
...Object.keys(allViews),
|
|
448
|
+
...Object.keys(allActions),
|
|
449
|
+
...Object.keys(node.volatileState),
|
|
450
|
+
];
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
454
|
+
const propStr = String(prop);
|
|
455
|
+
if (
|
|
456
|
+
propertyAtoms.has(propStr) ||
|
|
457
|
+
propStr in allViews ||
|
|
458
|
+
propStr in allActions ||
|
|
459
|
+
propStr in node.volatileState
|
|
460
|
+
) {
|
|
461
|
+
return {
|
|
462
|
+
configurable: true,
|
|
463
|
+
enumerable: true,
|
|
464
|
+
writable: true,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
},
|
|
469
|
+
}) as unknown as ModelInstance<P, V, A, Vol> & V & A & Vol;
|
|
470
|
+
|
|
471
|
+
// Initialize views
|
|
472
|
+
for (const viewFn of this.config.views) {
|
|
473
|
+
const views = viewFn(proxy);
|
|
474
|
+
for (const [key, value] of Object.entries(
|
|
475
|
+
Object.getOwnPropertyDescriptors(views),
|
|
476
|
+
)) {
|
|
477
|
+
allViews[key] = value;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Initialize actions
|
|
482
|
+
for (const actionFn of this.config.actions) {
|
|
483
|
+
const actions = actionFn(proxy);
|
|
484
|
+
for (const [key, value] of Object.entries(actions)) {
|
|
485
|
+
if (typeof value === "function") {
|
|
486
|
+
// Wrap action with tracking
|
|
487
|
+
allActions[key] = (...args: unknown[]) => {
|
|
488
|
+
return trackAction(node, key, args, () => {
|
|
489
|
+
return (value as Function).apply(proxy, args);
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Initialize volatile state
|
|
497
|
+
for (const volatileFn of this.config.volatiles) {
|
|
498
|
+
const volatile = volatileFn(proxy);
|
|
499
|
+
Object.assign(node.volatileState, volatile);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return proxy;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
is(value: unknown): value is ModelInstance<P, V, A, Vol> & V & A & Vol {
|
|
506
|
+
if (!value || typeof value !== "object") return false;
|
|
507
|
+
if (!($treenode in value)) return false;
|
|
508
|
+
const node = (value as Record<typeof $treenode, StateTreeNode>)[$treenode];
|
|
509
|
+
return node.$type === this || node.$type.name === this.name;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
validate(value: unknown, context: IValidationContext[]): IValidationResult {
|
|
513
|
+
const errors: IValidationResult["errors"] = [];
|
|
514
|
+
|
|
515
|
+
if (!value || typeof value !== "object") {
|
|
516
|
+
return {
|
|
517
|
+
valid: false,
|
|
518
|
+
errors: [
|
|
519
|
+
{
|
|
520
|
+
context,
|
|
521
|
+
value,
|
|
522
|
+
message: `Value is not an object`,
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Validate each property
|
|
529
|
+
for (const [key, propType] of Object.entries(this.properties)) {
|
|
530
|
+
const propValue = (value as Record<string, unknown>)[key];
|
|
531
|
+
const propContext: IValidationContext = {
|
|
532
|
+
path: context.length > 0 ? `${context[0].path}/${key}` : `/${key}`,
|
|
533
|
+
type: propType as IAnyType,
|
|
534
|
+
parent: value,
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const result = (propType as IAnyType).validate(propValue, [
|
|
538
|
+
...context,
|
|
539
|
+
propContext,
|
|
540
|
+
]);
|
|
541
|
+
if (!result.valid) {
|
|
542
|
+
errors.push(...result.errors);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
valid: errors.length === 0,
|
|
548
|
+
errors,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ============================================================================
|
|
553
|
+
// Model Modifiers
|
|
554
|
+
// ============================================================================
|
|
555
|
+
|
|
556
|
+
named(name: string): IModelType<P, V, A, Vol> {
|
|
557
|
+
return new ModelType({
|
|
558
|
+
...this.config,
|
|
559
|
+
name,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
props<P2 extends ModelProperties>(
|
|
564
|
+
properties: P2,
|
|
565
|
+
): IModelType<P & P2, V, A, Vol> {
|
|
566
|
+
return new ModelType({
|
|
567
|
+
...this.config,
|
|
568
|
+
properties: { ...this.config.properties, ...properties },
|
|
569
|
+
}) as unknown as IModelType<P & P2, V, A, Vol>;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
views<V2 extends object>(
|
|
573
|
+
fn: ModelViews<ModelInstance<P, V, A, Vol> & V & A & Vol, V2>,
|
|
574
|
+
): IModelType<P, V & V2, A, Vol> {
|
|
575
|
+
return new ModelType({
|
|
576
|
+
...this.config,
|
|
577
|
+
views: [
|
|
578
|
+
...this.config.views,
|
|
579
|
+
fn as unknown as ModelViews<
|
|
580
|
+
ModelInstance<P, V, A, Vol> & V & A & Vol,
|
|
581
|
+
V
|
|
582
|
+
>,
|
|
583
|
+
],
|
|
584
|
+
}) as unknown as IModelType<P, V & V2, A, Vol>;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
actions<A2 extends object>(
|
|
588
|
+
fn: ModelActions<ModelInstance<P, V, A, Vol> & V & A & Vol, A2>,
|
|
589
|
+
): IModelType<P, V, A & A2, Vol> {
|
|
590
|
+
return new ModelType({
|
|
591
|
+
...this.config,
|
|
592
|
+
actions: [
|
|
593
|
+
...this.config.actions,
|
|
594
|
+
fn as unknown as ModelActions<
|
|
595
|
+
ModelInstance<P, V, A, Vol> & V & A & Vol,
|
|
596
|
+
A
|
|
597
|
+
>,
|
|
598
|
+
],
|
|
599
|
+
}) as unknown as IModelType<P, V, A & A2, Vol>;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
volatile<Vol2 extends object>(
|
|
603
|
+
fn: ModelVolatile<ModelInstance<P, V, A, Vol> & V & A & Vol, Vol2>,
|
|
604
|
+
): IModelType<P, V, A, Vol & Vol2> {
|
|
605
|
+
return new ModelType({
|
|
606
|
+
...this.config,
|
|
607
|
+
volatiles: [
|
|
608
|
+
...this.config.volatiles,
|
|
609
|
+
fn as unknown as ModelVolatile<
|
|
610
|
+
ModelInstance<P, V, A, Vol> & V & A & Vol,
|
|
611
|
+
Vol
|
|
612
|
+
>,
|
|
613
|
+
],
|
|
614
|
+
}) as unknown as IModelType<P, V, A, Vol & Vol2>;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
preProcessSnapshot<NewC>(
|
|
618
|
+
fn: (snapshot: NewC) => ModelCreationType<P>,
|
|
619
|
+
): IModelType<P, V, A, Vol> {
|
|
620
|
+
return new ModelType({
|
|
621
|
+
...this.config,
|
|
622
|
+
preProcessor: fn as unknown as (
|
|
623
|
+
snapshot: unknown,
|
|
624
|
+
) => ModelCreationType<P>,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
postProcessSnapshot<NewS>(
|
|
629
|
+
fn: (snapshot: ModelSnapshotType<P>) => NewS,
|
|
630
|
+
): IModelType<P, V, A, Vol> {
|
|
631
|
+
return new ModelType({
|
|
632
|
+
...this.config,
|
|
633
|
+
postProcessor: fn as unknown as (
|
|
634
|
+
snapshot: ModelSnapshotType<P>,
|
|
635
|
+
) => unknown,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
extend<
|
|
640
|
+
V2 extends object = object,
|
|
641
|
+
A2 extends object = object,
|
|
642
|
+
Vol2 extends object = object,
|
|
643
|
+
>(
|
|
644
|
+
fn: (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => {
|
|
645
|
+
views?: V2;
|
|
646
|
+
actions?: A2;
|
|
647
|
+
state?: Vol2;
|
|
648
|
+
},
|
|
649
|
+
): IModelType<P, V & V2, A & A2, Vol & Vol2> {
|
|
650
|
+
// Create wrapper functions for views, actions, and volatile
|
|
651
|
+
const viewsFn = (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => {
|
|
652
|
+
const result = fn(self);
|
|
653
|
+
return (result.views ?? {}) as V2;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const actionsFn = (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => {
|
|
657
|
+
const result = fn(self);
|
|
658
|
+
return (result.actions ?? {}) as A2;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const volatileFn = (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => {
|
|
662
|
+
const result = fn(self);
|
|
663
|
+
return (result.state ?? {}) as Vol2;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
return new ModelType({
|
|
667
|
+
...this.config,
|
|
668
|
+
views: [
|
|
669
|
+
...this.config.views,
|
|
670
|
+
viewsFn as unknown as ModelViews<
|
|
671
|
+
ModelInstance<P, V, A, Vol> & V & A & Vol,
|
|
672
|
+
V
|
|
673
|
+
>,
|
|
674
|
+
],
|
|
675
|
+
actions: [
|
|
676
|
+
...this.config.actions,
|
|
677
|
+
actionsFn as unknown as ModelActions<
|
|
678
|
+
ModelInstance<P, V, A, Vol> & V & A & Vol,
|
|
679
|
+
A
|
|
680
|
+
>,
|
|
681
|
+
],
|
|
682
|
+
volatiles: [
|
|
683
|
+
...this.config.volatiles,
|
|
684
|
+
volatileFn as unknown as ModelVolatile<
|
|
685
|
+
ModelInstance<P, V, A, Vol> & V & A & Vol,
|
|
686
|
+
Vol
|
|
687
|
+
>,
|
|
688
|
+
],
|
|
689
|
+
}) as unknown as IModelType<P, V & V2, A & A2, Vol & Vol2>;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Add afterCreate lifecycle hook
|
|
694
|
+
*/
|
|
695
|
+
afterCreate(
|
|
696
|
+
fn: (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => void,
|
|
697
|
+
): IModelType<P, V, A, Vol> {
|
|
698
|
+
return new ModelType({
|
|
699
|
+
...this.config,
|
|
700
|
+
hooks: {
|
|
701
|
+
...this.config.hooks,
|
|
702
|
+
afterCreate: fn,
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Add afterAttach lifecycle hook
|
|
709
|
+
*/
|
|
710
|
+
afterAttach(
|
|
711
|
+
fn: (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => void,
|
|
712
|
+
): IModelType<P, V, A, Vol> {
|
|
713
|
+
return new ModelType({
|
|
714
|
+
...this.config,
|
|
715
|
+
hooks: {
|
|
716
|
+
...this.config.hooks,
|
|
717
|
+
afterAttach: fn,
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Add beforeDetach lifecycle hook
|
|
724
|
+
*/
|
|
725
|
+
beforeDetach(
|
|
726
|
+
fn: (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => void,
|
|
727
|
+
): IModelType<P, V, A, Vol> {
|
|
728
|
+
return new ModelType({
|
|
729
|
+
...this.config,
|
|
730
|
+
hooks: {
|
|
731
|
+
...this.config.hooks,
|
|
732
|
+
beforeDetach: fn,
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Add beforeDestroy lifecycle hook
|
|
739
|
+
*/
|
|
740
|
+
beforeDestroy(
|
|
741
|
+
fn: (self: ModelInstance<P, V, A, Vol> & V & A & Vol) => void,
|
|
742
|
+
): IModelType<P, V, A, Vol> {
|
|
743
|
+
return new ModelType({
|
|
744
|
+
...this.config,
|
|
745
|
+
hooks: {
|
|
746
|
+
...this.config.hooks,
|
|
747
|
+
beforeDestroy: fn,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ============================================================================
|
|
754
|
+
// Model Factory Function
|
|
755
|
+
// ============================================================================
|
|
756
|
+
|
|
757
|
+
export function model<P extends ModelProperties>(
|
|
758
|
+
name: string,
|
|
759
|
+
properties: P,
|
|
760
|
+
): IModelType<P, object, object, object>;
|
|
761
|
+
|
|
762
|
+
export function model<P extends ModelProperties>(
|
|
763
|
+
properties: P,
|
|
764
|
+
): IModelType<P, object, object, object>;
|
|
765
|
+
|
|
766
|
+
export function model<P extends ModelProperties>(
|
|
767
|
+
nameOrProperties: string | P,
|
|
768
|
+
maybeProperties?: P,
|
|
769
|
+
): IModelType<P, object, object, object> {
|
|
770
|
+
const name =
|
|
771
|
+
typeof nameOrProperties === "string" ? nameOrProperties : "AnonymousModel";
|
|
772
|
+
const properties =
|
|
773
|
+
typeof nameOrProperties === "string" ? maybeProperties! : nameOrProperties;
|
|
774
|
+
|
|
775
|
+
return new ModelType({
|
|
776
|
+
name,
|
|
777
|
+
properties,
|
|
778
|
+
views: [],
|
|
779
|
+
actions: [],
|
|
780
|
+
volatiles: [],
|
|
781
|
+
initializers: [],
|
|
782
|
+
hooks: {},
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ============================================================================
|
|
787
|
+
// Compose Models
|
|
788
|
+
// ============================================================================
|
|
789
|
+
|
|
790
|
+
export function compose<
|
|
791
|
+
PA extends ModelProperties,
|
|
792
|
+
PB extends ModelProperties,
|
|
793
|
+
VA extends object,
|
|
794
|
+
VB extends object,
|
|
795
|
+
AA extends object,
|
|
796
|
+
AB extends object,
|
|
797
|
+
VolA extends object,
|
|
798
|
+
VolB extends object,
|
|
799
|
+
>(
|
|
800
|
+
name: string,
|
|
801
|
+
a: IModelType<PA, VA, AA, VolA>,
|
|
802
|
+
b: IModelType<PB, VB, AB, VolB>,
|
|
803
|
+
): IModelType<PA & PB, VA & VB, AA & AB, VolA & VolB>;
|
|
804
|
+
|
|
805
|
+
export function compose<
|
|
806
|
+
PA extends ModelProperties,
|
|
807
|
+
PB extends ModelProperties,
|
|
808
|
+
VA extends object,
|
|
809
|
+
VB extends object,
|
|
810
|
+
AA extends object,
|
|
811
|
+
AB extends object,
|
|
812
|
+
VolA extends object,
|
|
813
|
+
VolB extends object,
|
|
814
|
+
>(
|
|
815
|
+
a: IModelType<PA, VA, AA, VolA>,
|
|
816
|
+
b: IModelType<PB, VB, AB, VolB>,
|
|
817
|
+
): IModelType<PA & PB, VA & VB, AA & AB, VolA & VolB>;
|
|
818
|
+
|
|
819
|
+
export function compose(...args: unknown[]): unknown {
|
|
820
|
+
const name = typeof args[0] === "string" ? args[0] : "ComposedModel";
|
|
821
|
+
const types = (
|
|
822
|
+
typeof args[0] === "string" ? args.slice(1) : args
|
|
823
|
+
) as IModelType<ModelProperties, object, object, object>[];
|
|
824
|
+
|
|
825
|
+
// Merge all properties
|
|
826
|
+
const mergedProperties: ModelProperties = {};
|
|
827
|
+
for (const type of types) {
|
|
828
|
+
Object.assign(mergedProperties, type.properties);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return model(name, mergedProperties);
|
|
832
|
+
}
|