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.
@@ -0,0 +1,616 @@
1
+ /**
2
+ * Utility types implementation
3
+ * optional, maybe, maybeNull, union, late, refinement, reference, safeReference
4
+ */
5
+
6
+ import type {
7
+ IOptionalType,
8
+ IMaybeType,
9
+ IMaybeNullType,
10
+ IUnionType,
11
+ ILateType,
12
+ IRefinementType,
13
+ IReferenceType,
14
+ ISafeReferenceType,
15
+ ReferenceOptions,
16
+ UnionOptions,
17
+ IType,
18
+ IValidationContext,
19
+ IValidationResult,
20
+ IAnyType,
21
+ IAnyModelType,
22
+ Instance,
23
+ } from './types';
24
+ import { resolveIdentifier, getStateTreeNode, StateTreeNode, $treenode } from './tree';
25
+
26
+ // ============================================================================
27
+ // Optional Type
28
+ // ============================================================================
29
+
30
+ class OptionalType<T extends IAnyType, Default>
31
+ implements IOptionalType<T, Default>
32
+ {
33
+ readonly _kind = 'optional' as const;
34
+ readonly _subType: T;
35
+ readonly _defaultValue: Default | (() => Default);
36
+ readonly name: string;
37
+
38
+ readonly _C!: (T extends IType<infer C, unknown, unknown> ? C : never) | undefined;
39
+ readonly _S!: T extends IType<unknown, infer S, unknown> ? S : never;
40
+ readonly _T!: T extends IType<unknown, unknown, infer I> ? I : never;
41
+
42
+ constructor(subType: T, defaultValue: Default | (() => Default)) {
43
+ this._subType = subType;
44
+ this._defaultValue = defaultValue;
45
+ this.name = `optional<${subType.name}>`;
46
+ }
47
+
48
+ create(
49
+ snapshot?: (T extends IType<infer C, unknown, unknown> ? C : never) | undefined,
50
+ env?: unknown
51
+ ): T extends IType<unknown, unknown, infer I> ? I : never {
52
+ if (snapshot === undefined) {
53
+ const defaultVal =
54
+ typeof this._defaultValue === 'function'
55
+ ? (this._defaultValue as () => Default)()
56
+ : this._defaultValue;
57
+ return this._subType.create(defaultVal as unknown, env) as T extends IType<unknown, unknown, infer I> ? I : never;
58
+ }
59
+ return this._subType.create(snapshot, env) as T extends IType<unknown, unknown, infer I> ? I : never;
60
+ }
61
+
62
+ is(value: unknown): value is T extends IType<unknown, unknown, infer I> ? I : never {
63
+ return value === undefined || this._subType.is(value);
64
+ }
65
+
66
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
67
+ if (value === undefined) {
68
+ return { valid: true, errors: [] };
69
+ }
70
+ return this._subType.validate(value, context);
71
+ }
72
+ }
73
+
74
+ export function optional<T extends IAnyType, D extends T extends IType<infer C, unknown, unknown> ? C : never>(
75
+ type: T,
76
+ defaultValue: D | (() => D)
77
+ ): IOptionalType<T, D> {
78
+ return new OptionalType(type, defaultValue);
79
+ }
80
+
81
+ // ============================================================================
82
+ // Maybe Type (value | undefined)
83
+ // ============================================================================
84
+
85
+ class MaybeType<T extends IAnyType> implements IMaybeType<T> {
86
+ readonly _kind = 'maybe' as const;
87
+ readonly _subType: T;
88
+ readonly name: string;
89
+
90
+ readonly _C!: (T extends IType<infer C, unknown, unknown> ? C : never) | undefined;
91
+ readonly _S!: (T extends IType<unknown, infer S, unknown> ? S : never) | undefined;
92
+ readonly _T!: (T extends IType<unknown, unknown, infer I> ? I : never) | undefined;
93
+
94
+ constructor(subType: T) {
95
+ this._subType = subType;
96
+ this.name = `maybe<${subType.name}>`;
97
+ }
98
+
99
+ create(
100
+ snapshot?: (T extends IType<infer C, unknown, unknown> ? C : never) | undefined,
101
+ env?: unknown
102
+ ): (T extends IType<unknown, unknown, infer I> ? I : never) | undefined {
103
+ if (snapshot === undefined) {
104
+ return undefined;
105
+ }
106
+ return this._subType.create(snapshot, env) as (T extends IType<unknown, unknown, infer I> ? I : never) | undefined;
107
+ }
108
+
109
+ is(value: unknown): value is (T extends IType<unknown, unknown, infer I> ? I : never) | undefined {
110
+ return value === undefined || this._subType.is(value);
111
+ }
112
+
113
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
114
+ if (value === undefined) {
115
+ return { valid: true, errors: [] };
116
+ }
117
+ return this._subType.validate(value, context);
118
+ }
119
+ }
120
+
121
+ export function maybe<T extends IAnyType>(type: T): IMaybeType<T> {
122
+ return new MaybeType(type);
123
+ }
124
+
125
+ // ============================================================================
126
+ // MaybeNull Type (value | null)
127
+ // ============================================================================
128
+
129
+ class MaybeNullType<T extends IAnyType> implements IMaybeNullType<T> {
130
+ readonly _kind = 'maybeNull' as const;
131
+ readonly _subType: T;
132
+ readonly name: string;
133
+
134
+ readonly _C!: (T extends IType<infer C, unknown, unknown> ? C : never) | null;
135
+ readonly _S!: (T extends IType<unknown, infer S, unknown> ? S : never) | null;
136
+ readonly _T!: (T extends IType<unknown, unknown, infer I> ? I : never) | null;
137
+
138
+ constructor(subType: T) {
139
+ this._subType = subType;
140
+ this.name = `maybeNull<${subType.name}>`;
141
+ }
142
+
143
+ create(
144
+ snapshot?: (T extends IType<infer C, unknown, unknown> ? C : never) | null,
145
+ env?: unknown
146
+ ): (T extends IType<unknown, unknown, infer I> ? I : never) | null {
147
+ if (snapshot === null || snapshot === undefined) {
148
+ return null;
149
+ }
150
+ return this._subType.create(snapshot, env) as (T extends IType<unknown, unknown, infer I> ? I : never) | null;
151
+ }
152
+
153
+ is(value: unknown): value is (T extends IType<unknown, unknown, infer I> ? I : never) | null {
154
+ return value === null || this._subType.is(value);
155
+ }
156
+
157
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
158
+ if (value === null) {
159
+ return { valid: true, errors: [] };
160
+ }
161
+ return this._subType.validate(value, context);
162
+ }
163
+ }
164
+
165
+ export function maybeNull<T extends IAnyType>(type: T): IMaybeNullType<T> {
166
+ return new MaybeNullType(type);
167
+ }
168
+
169
+ // ============================================================================
170
+ // Union Type
171
+ // ============================================================================
172
+
173
+ class UnionType<Types extends IAnyType[]> implements IUnionType<Types> {
174
+ readonly _kind = 'union' as const;
175
+ readonly _types: Types;
176
+ readonly name: string;
177
+ private dispatcher?: (snapshot: unknown) => IAnyType;
178
+ private eager: boolean;
179
+
180
+ readonly _C!: Types[number] extends IType<infer C, unknown, unknown> ? C : never;
181
+ readonly _S!: Types[number] extends IType<unknown, infer S, unknown> ? S : never;
182
+ readonly _T!: Types[number] extends IType<unknown, unknown, infer T> ? T : never;
183
+
184
+ constructor(types: Types, options?: UnionOptions) {
185
+ this._types = types;
186
+ this.dispatcher = options?.dispatcher;
187
+ this.eager = options?.eager ?? true;
188
+ this.name = `union(${types.map((t) => t.name).join(' | ')})`;
189
+ }
190
+
191
+ create(
192
+ snapshot?: Types[number] extends IType<infer C, unknown, unknown> ? C : never,
193
+ env?: unknown
194
+ ): Types[number] extends IType<unknown, unknown, infer T> ? T : never {
195
+ type ResultType = Types[number] extends IType<unknown, unknown, infer T> ? T : never;
196
+
197
+ // Use dispatcher if available
198
+ if (this.dispatcher && snapshot !== undefined) {
199
+ const type = this.dispatcher(snapshot);
200
+ return type.create(snapshot, env) as ResultType;
201
+ }
202
+
203
+ // Try each type
204
+ for (const type of this._types) {
205
+ try {
206
+ const result = type.validate(snapshot, []);
207
+ if (result.valid) {
208
+ return type.create(snapshot, env) as ResultType;
209
+ }
210
+ } catch {
211
+ // Continue to next type
212
+ }
213
+ }
214
+
215
+ throw new Error(
216
+ `[jotai-state-tree] No type in union matched the value: ${JSON.stringify(snapshot)}`
217
+ );
218
+ }
219
+
220
+ is(value: unknown): value is Types[number] extends IType<unknown, unknown, infer T> ? T : never {
221
+ return this._types.some((type) => type.is(value));
222
+ }
223
+
224
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
225
+ for (const type of this._types) {
226
+ const result = type.validate(value, context);
227
+ if (result.valid) {
228
+ return result;
229
+ }
230
+ }
231
+
232
+ return {
233
+ valid: false,
234
+ errors: [
235
+ {
236
+ context,
237
+ value,
238
+ message: `Value does not match any type in union`,
239
+ },
240
+ ],
241
+ };
242
+ }
243
+ }
244
+
245
+ export function union<Types extends IAnyType[]>(
246
+ ...types: Types
247
+ ): IUnionType<Types>;
248
+ export function union<Types extends IAnyType[]>(
249
+ options: UnionOptions,
250
+ ...types: Types
251
+ ): IUnionType<Types>;
252
+ export function union<Types extends IAnyType[]>(
253
+ optionsOrType: UnionOptions | Types[number],
254
+ ...rest: Types
255
+ ): IUnionType<Types> {
256
+ if (optionsOrType && typeof optionsOrType === 'object' && 'dispatcher' in optionsOrType) {
257
+ return new UnionType(rest as Types, optionsOrType);
258
+ }
259
+ return new UnionType([optionsOrType as Types[number], ...rest] as unknown as Types);
260
+ }
261
+
262
+ // ============================================================================
263
+ // Late Type (for recursive/circular types)
264
+ // ============================================================================
265
+
266
+ class LateType<T extends IAnyType> implements ILateType<T> {
267
+ readonly _kind = 'late' as const;
268
+ readonly _definition: () => T;
269
+ readonly name: string;
270
+ private resolvedType?: T;
271
+
272
+ readonly _C!: T extends IType<infer C, unknown, unknown> ? C : never;
273
+ readonly _S!: T extends IType<unknown, infer S, unknown> ? S : never;
274
+ readonly _T!: T extends IType<unknown, unknown, infer I> ? I : never;
275
+
276
+ constructor(definition: () => T, name?: string) {
277
+ this._definition = definition;
278
+ this.name = name ?? 'late(...)';
279
+ }
280
+
281
+ private getType(): T {
282
+ if (!this.resolvedType) {
283
+ this.resolvedType = this._definition();
284
+ }
285
+ return this.resolvedType;
286
+ }
287
+
288
+ create(
289
+ snapshot?: T extends IType<infer C, unknown, unknown> ? C : never,
290
+ env?: unknown
291
+ ): T extends IType<unknown, unknown, infer I> ? I : never {
292
+ return this.getType().create(snapshot, env) as T extends IType<unknown, unknown, infer I> ? I : never;
293
+ }
294
+
295
+ is(value: unknown): value is T extends IType<unknown, unknown, infer I> ? I : never {
296
+ return this.getType().is(value);
297
+ }
298
+
299
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
300
+ return this.getType().validate(value, context);
301
+ }
302
+ }
303
+
304
+ export function late<T extends IAnyType>(definition: () => T): ILateType<T>;
305
+ export function late<T extends IAnyType>(name: string, definition: () => T): ILateType<T>;
306
+ export function late<T extends IAnyType>(
307
+ nameOrDefinition: string | (() => T),
308
+ maybeDefinition?: () => T
309
+ ): ILateType<T> {
310
+ if (typeof nameOrDefinition === 'string') {
311
+ return new LateType(maybeDefinition!, nameOrDefinition);
312
+ }
313
+ return new LateType(nameOrDefinition);
314
+ }
315
+
316
+ // ============================================================================
317
+ // Refinement Type
318
+ // ============================================================================
319
+
320
+ class RefinementType<T extends IAnyType> implements IRefinementType<T> {
321
+ readonly _kind = 'refinement' as const;
322
+ readonly _subType: T;
323
+ readonly _predicate: (value: unknown) => boolean;
324
+ readonly name: string;
325
+ private message: string | ((value: unknown) => string);
326
+
327
+ readonly _C!: T extends IType<infer C, unknown, unknown> ? C : never;
328
+ readonly _S!: T extends IType<unknown, infer S, unknown> ? S : never;
329
+ readonly _T!: T extends IType<unknown, unknown, infer I> ? I : never;
330
+
331
+ constructor(
332
+ subType: T,
333
+ predicate: (value: unknown) => boolean,
334
+ message?: string | ((value: unknown) => string)
335
+ ) {
336
+ this._subType = subType;
337
+ this._predicate = predicate;
338
+ this.message = message ?? 'Value failed refinement predicate';
339
+ this.name = `refinement<${subType.name}>`;
340
+ }
341
+
342
+ create(
343
+ snapshot?: T extends IType<infer C, unknown, unknown> ? C : never,
344
+ env?: unknown
345
+ ): T extends IType<unknown, unknown, infer I> ? I : never {
346
+ const instance = this._subType.create(snapshot, env);
347
+ if (!this._predicate(instance)) {
348
+ const msg = typeof this.message === 'function' ? this.message(instance) : this.message;
349
+ throw new Error(`[jotai-state-tree] ${msg}`);
350
+ }
351
+ return instance as T extends IType<unknown, unknown, infer I> ? I : never;
352
+ }
353
+
354
+ is(value: unknown): value is T extends IType<unknown, unknown, infer I> ? I : never {
355
+ return this._subType.is(value) && this._predicate(value);
356
+ }
357
+
358
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
359
+ const baseResult = this._subType.validate(value, context);
360
+ if (!baseResult.valid) {
361
+ return baseResult;
362
+ }
363
+
364
+ if (!this._predicate(value)) {
365
+ const msg = typeof this.message === 'function' ? this.message(value) : this.message;
366
+ return {
367
+ valid: false,
368
+ errors: [
369
+ {
370
+ context,
371
+ value,
372
+ message: msg,
373
+ },
374
+ ],
375
+ };
376
+ }
377
+
378
+ return { valid: true, errors: [] };
379
+ }
380
+ }
381
+
382
+ export function refinement<T extends IAnyType>(
383
+ type: T,
384
+ predicate: (value: T extends IType<unknown, unknown, infer I> ? I : never) => boolean,
385
+ message?: string | ((value: unknown) => string)
386
+ ): IRefinementType<T> {
387
+ return new RefinementType(type, predicate as (value: unknown) => boolean, message);
388
+ }
389
+
390
+ // ============================================================================
391
+ // Reference Type
392
+ // ============================================================================
393
+
394
+ class ReferenceType<T extends IAnyModelType> implements IReferenceType<T> {
395
+ readonly _kind = 'reference' as const;
396
+ readonly _targetType: T;
397
+ readonly name: string;
398
+ private options?: ReferenceOptions<T>;
399
+
400
+ readonly _C!: string | number;
401
+ readonly _S!: string | number;
402
+ readonly _T!: Instance<T>;
403
+
404
+ constructor(targetType: T, options?: ReferenceOptions<T>) {
405
+ this._targetType = targetType;
406
+ this.options = options;
407
+ this.name = `reference<${targetType.name}>`;
408
+ }
409
+
410
+ create(snapshot?: string | number, env?: unknown): Instance<T> {
411
+ if (snapshot === undefined) {
412
+ throw new Error('[jotai-state-tree] Reference requires an identifier');
413
+ }
414
+
415
+ // Create a proxy that resolves the reference lazily
416
+ const self = this;
417
+ let resolved: Instance<T> | null = null;
418
+
419
+ // Try custom getter first
420
+ if (this.options?.get) {
421
+ const result = this.options.get(snapshot, null);
422
+ if (result) return result;
423
+ }
424
+
425
+ // Create reference node that will resolve
426
+ const node = new StateTreeNode(this, snapshot, env);
427
+ node.identifierValue = snapshot;
428
+
429
+ const proxy = new Proxy({} as Instance<T>, {
430
+ get(target, prop) {
431
+ // Resolve the reference
432
+ if (!resolved) {
433
+ const targetNode = resolveIdentifier(self._targetType.name, snapshot);
434
+ if (!targetNode) {
435
+ throw new Error(
436
+ `[jotai-state-tree] Failed to resolve reference '${snapshot}' to type '${self._targetType.name}'`
437
+ );
438
+ }
439
+ resolved = targetNode.getInstance() as Instance<T>;
440
+ }
441
+
442
+ if (prop === $treenode) {
443
+ return node;
444
+ }
445
+
446
+ return (resolved as unknown as Record<string | symbol, unknown>)[prop];
447
+ },
448
+ set(target, prop, value) {
449
+ if (!resolved) {
450
+ const targetNode = resolveIdentifier(self._targetType.name, snapshot);
451
+ if (!targetNode) {
452
+ throw new Error(
453
+ `[jotai-state-tree] Failed to resolve reference '${snapshot}' to type '${self._targetType.name}'`
454
+ );
455
+ }
456
+ resolved = targetNode.getInstance() as Instance<T>;
457
+ }
458
+ (resolved as unknown as Record<string | symbol, unknown>)[prop] = value;
459
+ return true;
460
+ },
461
+ has(target, prop) {
462
+ if (!resolved) {
463
+ const targetNode = resolveIdentifier(self._targetType.name, snapshot);
464
+ if (targetNode) {
465
+ resolved = targetNode.getInstance() as Instance<T>;
466
+ }
467
+ }
468
+ return resolved ? prop in (resolved as object) : false;
469
+ },
470
+ });
471
+
472
+ node.setInstance(proxy);
473
+ return proxy;
474
+ }
475
+
476
+ is(value: unknown): value is Instance<T> {
477
+ return this._targetType.is(value);
478
+ }
479
+
480
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
481
+ if (typeof value === 'string' || typeof value === 'number') {
482
+ return { valid: true, errors: [] };
483
+ }
484
+ return {
485
+ valid: false,
486
+ errors: [
487
+ {
488
+ context,
489
+ value,
490
+ message: 'Reference must be a string or number identifier',
491
+ },
492
+ ],
493
+ };
494
+ }
495
+ }
496
+
497
+ export function reference<T extends IAnyModelType>(
498
+ targetType: T,
499
+ options?: ReferenceOptions<T>
500
+ ): IReferenceType<T> {
501
+ return new ReferenceType(targetType, options);
502
+ }
503
+
504
+ // ============================================================================
505
+ // Safe Reference Type (returns undefined instead of throwing)
506
+ // ============================================================================
507
+
508
+ class SafeReferenceType<T extends IAnyModelType> implements ISafeReferenceType<T> {
509
+ readonly _kind = 'safeReference' as const;
510
+ readonly _targetType: T;
511
+ readonly name: string;
512
+ private options?: ReferenceOptions<T> & { acceptsUndefined?: boolean };
513
+
514
+ readonly _C!: string | number | undefined;
515
+ readonly _S!: string | number | undefined;
516
+ readonly _T!: Instance<T> | undefined;
517
+
518
+ constructor(
519
+ targetType: T,
520
+ options?: ReferenceOptions<T> & { acceptsUndefined?: boolean }
521
+ ) {
522
+ this._targetType = targetType;
523
+ this.options = options;
524
+ this.name = `safeReference<${targetType.name}>`;
525
+ }
526
+
527
+ create(snapshot?: string | number | undefined, env?: unknown): Instance<T> | undefined {
528
+ if (snapshot === undefined) {
529
+ return undefined;
530
+ }
531
+
532
+ const targetNode = resolveIdentifier(this._targetType.name, snapshot);
533
+ if (!targetNode) {
534
+ if (this.options?.onInvalidated) {
535
+ // Let caller handle invalid reference
536
+ return undefined;
537
+ }
538
+ return undefined;
539
+ }
540
+
541
+ return targetNode.getInstance() as Instance<T>;
542
+ }
543
+
544
+ is(value: unknown): value is Instance<T> | undefined {
545
+ return value === undefined || this._targetType.is(value);
546
+ }
547
+
548
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
549
+ if (value === undefined) {
550
+ return { valid: true, errors: [] };
551
+ }
552
+ if (typeof value === 'string' || typeof value === 'number') {
553
+ return { valid: true, errors: [] };
554
+ }
555
+ return {
556
+ valid: false,
557
+ errors: [
558
+ {
559
+ context,
560
+ value,
561
+ message: 'Safe reference must be a string, number, or undefined',
562
+ },
563
+ ],
564
+ };
565
+ }
566
+ }
567
+
568
+ export function safeReference<T extends IAnyModelType>(
569
+ targetType: T,
570
+ options?: ReferenceOptions<T> & { acceptsUndefined?: boolean }
571
+ ): ISafeReferenceType<T> {
572
+ return new SafeReferenceType(targetType, options);
573
+ }
574
+
575
+ // ============================================================================
576
+ // SnapshotProcessor Type
577
+ // ============================================================================
578
+
579
+ export function snapshotProcessor<
580
+ IT extends IAnyType,
581
+ CustomC = IT extends IType<infer C, unknown, unknown> ? C : never,
582
+ CustomS = IT extends IType<unknown, infer S, unknown> ? S : never,
583
+ >(
584
+ type: IT,
585
+ processors: {
586
+ preProcessor?: (snapshot: CustomC) => IT extends IType<infer C, unknown, unknown> ? C : never;
587
+ postProcessor?: (
588
+ snapshot: IT extends IType<unknown, infer S, unknown> ? S : never
589
+ ) => CustomS;
590
+ }
591
+ ): IType<CustomC, CustomS, IT extends IType<unknown, unknown, infer T> ? T : never> {
592
+ type ResultType = IT extends IType<unknown, unknown, infer T> ? T : never;
593
+
594
+ return {
595
+ name: `snapshotProcessor<${type.name}>`,
596
+ _kind: 'simple' as const,
597
+ _C: undefined as unknown as CustomC,
598
+ _S: undefined as unknown as CustomS,
599
+ _T: undefined as unknown as ResultType,
600
+
601
+ create(snapshot?: CustomC, env?: unknown): ResultType {
602
+ const processed = processors.preProcessor
603
+ ? processors.preProcessor(snapshot as CustomC)
604
+ : snapshot;
605
+ return type.create(processed, env) as ResultType;
606
+ },
607
+
608
+ is(value: unknown): value is ResultType {
609
+ return type.is(value);
610
+ },
611
+
612
+ validate(value: unknown, context: IValidationContext[]): IValidationResult {
613
+ return type.validate(value, context);
614
+ },
615
+ };
616
+ }