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
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Primitive types implementation
|
|
3
|
+
* string, number, boolean, integer, Date, identifier, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ISimpleType,
|
|
8
|
+
IType,
|
|
9
|
+
IValidationContext,
|
|
10
|
+
IValidationResult,
|
|
11
|
+
IIdentifierType,
|
|
12
|
+
IIdentifierNumberType,
|
|
13
|
+
ILiteralType,
|
|
14
|
+
IEnumerationType,
|
|
15
|
+
IFrozenType,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Base Simple Type
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
function createSimpleType<T>(
|
|
23
|
+
name: string,
|
|
24
|
+
validator: (value: unknown) => boolean,
|
|
25
|
+
defaultValue?: T
|
|
26
|
+
): ISimpleType<T> {
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
_kind: 'simple',
|
|
30
|
+
_C: undefined as unknown as T,
|
|
31
|
+
_S: undefined as unknown as T,
|
|
32
|
+
_T: undefined as unknown as T,
|
|
33
|
+
|
|
34
|
+
create(snapshot?: T): T {
|
|
35
|
+
if (snapshot === undefined) {
|
|
36
|
+
if (defaultValue !== undefined) {
|
|
37
|
+
return defaultValue;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`[jotai-state-tree] A value of type '${name}' is required`);
|
|
40
|
+
}
|
|
41
|
+
if (!validator(snapshot)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[jotai-state-tree] Value '${String(snapshot)}' is not a valid '${name}'`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return snapshot;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
is(value: unknown): value is T {
|
|
50
|
+
return validator(value);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
validate(value: unknown, context: IValidationContext[]): IValidationResult {
|
|
54
|
+
if (validator(value)) {
|
|
55
|
+
return { valid: true, errors: [] };
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
errors: [
|
|
60
|
+
{
|
|
61
|
+
context,
|
|
62
|
+
value,
|
|
63
|
+
message: `Value '${String(value)}' is not a valid '${name}'`,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Primitive Types
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/** String type */
|
|
76
|
+
export const string: ISimpleType<string> = createSimpleType<string>(
|
|
77
|
+
'string',
|
|
78
|
+
(value): value is string => typeof value === 'string'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/** Number type (includes floats) */
|
|
82
|
+
export const number: ISimpleType<number> = createSimpleType<number>(
|
|
83
|
+
'number',
|
|
84
|
+
(value): value is number => typeof value === 'number' && !isNaN(value)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/** Integer type */
|
|
88
|
+
export const integer: ISimpleType<number> = createSimpleType<number>(
|
|
89
|
+
'integer',
|
|
90
|
+
(value): value is number =>
|
|
91
|
+
typeof value === 'number' && !isNaN(value) && Number.isInteger(value)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
/** Boolean type */
|
|
95
|
+
export const boolean: ISimpleType<boolean> = createSimpleType<boolean>(
|
|
96
|
+
'boolean',
|
|
97
|
+
(value): value is boolean => typeof value === 'boolean'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
/** Date type - stores as number, exposes as Date */
|
|
101
|
+
export const DatePrimitive: IType<number | Date, number, Date> = {
|
|
102
|
+
name: 'Date',
|
|
103
|
+
_kind: 'simple' as const,
|
|
104
|
+
_C: undefined as unknown as number | Date,
|
|
105
|
+
_S: undefined as unknown as number,
|
|
106
|
+
_T: undefined as unknown as Date,
|
|
107
|
+
|
|
108
|
+
create(snapshot?: number | Date): Date {
|
|
109
|
+
if (snapshot === undefined) {
|
|
110
|
+
return new Date();
|
|
111
|
+
}
|
|
112
|
+
if (snapshot instanceof Date) {
|
|
113
|
+
return snapshot;
|
|
114
|
+
}
|
|
115
|
+
if (typeof snapshot === 'number') {
|
|
116
|
+
return new Date(snapshot);
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`[jotai-state-tree] Value is not a valid Date`);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
is(value: unknown): value is Date {
|
|
122
|
+
return value instanceof Date;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
validate(value: unknown, context: IValidationContext[]): IValidationResult {
|
|
126
|
+
if (value instanceof Date || typeof value === 'number') {
|
|
127
|
+
return { valid: true, errors: [] };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
valid: false,
|
|
131
|
+
errors: [
|
|
132
|
+
{
|
|
133
|
+
context,
|
|
134
|
+
value,
|
|
135
|
+
message: 'Value is not a valid Date',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** Null type */
|
|
143
|
+
export const nullType: ISimpleType<null> = createSimpleType<null>(
|
|
144
|
+
'null',
|
|
145
|
+
(value): value is null => value === null
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
/** Undefined type */
|
|
149
|
+
export const undefinedType: ISimpleType<undefined> = createSimpleType<undefined>(
|
|
150
|
+
'undefined',
|
|
151
|
+
(value): value is undefined => value === undefined
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Identifier Types
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
/** String identifier type */
|
|
159
|
+
export const identifier: IIdentifierType = {
|
|
160
|
+
...createSimpleType<string>(
|
|
161
|
+
'identifier',
|
|
162
|
+
(value): value is string => typeof value === 'string'
|
|
163
|
+
),
|
|
164
|
+
_kind: 'identifier' as const,
|
|
165
|
+
identifierAttribute: 'id',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Number identifier type */
|
|
169
|
+
export const identifierNumber: IIdentifierNumberType = {
|
|
170
|
+
...createSimpleType<number>(
|
|
171
|
+
'identifierNumber',
|
|
172
|
+
(value): value is number => typeof value === 'number' && !isNaN(value)
|
|
173
|
+
),
|
|
174
|
+
_kind: 'identifierNumber' as const,
|
|
175
|
+
identifierAttribute: 'id',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Literal Type
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
export function literal<T extends string | number | boolean>(value: T): ILiteralType<T> {
|
|
183
|
+
return {
|
|
184
|
+
name: `literal(${JSON.stringify(value)})`,
|
|
185
|
+
_kind: 'literal',
|
|
186
|
+
_value: value,
|
|
187
|
+
_C: undefined as unknown as T,
|
|
188
|
+
_S: undefined as unknown as T,
|
|
189
|
+
_T: undefined as unknown as T,
|
|
190
|
+
|
|
191
|
+
create(snapshot?: T): T {
|
|
192
|
+
if (snapshot === undefined) {
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
if (snapshot !== value) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`[jotai-state-tree] Value '${String(snapshot)}' is not the literal '${String(value)}'`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return snapshot;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
is(v: unknown): v is T {
|
|
204
|
+
return v === value;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
validate(v: unknown, context: IValidationContext[]): IValidationResult {
|
|
208
|
+
if (v === value) {
|
|
209
|
+
return { valid: true, errors: [] };
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
valid: false,
|
|
213
|
+
errors: [
|
|
214
|
+
{
|
|
215
|
+
context,
|
|
216
|
+
value: v,
|
|
217
|
+
message: `Value '${String(v)}' is not the literal '${String(value)}'`,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// Enumeration Type
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
export function enumeration<E extends string>(
|
|
230
|
+
name: string,
|
|
231
|
+
options: readonly E[]
|
|
232
|
+
): IEnumerationType<E>;
|
|
233
|
+
export function enumeration<E extends string>(options: readonly E[]): IEnumerationType<E>;
|
|
234
|
+
export function enumeration<E extends string>(
|
|
235
|
+
nameOrOptions: string | readonly E[],
|
|
236
|
+
maybeOptions?: readonly E[]
|
|
237
|
+
): IEnumerationType<E> {
|
|
238
|
+
const name = typeof nameOrOptions === 'string' ? nameOrOptions : 'enumeration';
|
|
239
|
+
const options = typeof nameOrOptions === 'string' ? maybeOptions! : nameOrOptions;
|
|
240
|
+
|
|
241
|
+
const optionSet = new Set(options);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
name,
|
|
245
|
+
_kind: 'enumeration',
|
|
246
|
+
_options: options,
|
|
247
|
+
_C: undefined as unknown as E,
|
|
248
|
+
_S: undefined as unknown as E,
|
|
249
|
+
_T: undefined as unknown as E,
|
|
250
|
+
|
|
251
|
+
create(snapshot?: E): E {
|
|
252
|
+
if (snapshot === undefined) {
|
|
253
|
+
throw new Error(`[jotai-state-tree] A value for enumeration '${name}' is required`);
|
|
254
|
+
}
|
|
255
|
+
if (!optionSet.has(snapshot)) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`[jotai-state-tree] Value '${snapshot}' is not a valid option for enumeration '${name}'. ` +
|
|
258
|
+
`Expected one of: ${options.join(', ')}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return snapshot;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
is(value: unknown): value is E {
|
|
265
|
+
return typeof value === 'string' && optionSet.has(value as E);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
validate(value: unknown, context: IValidationContext[]): IValidationResult {
|
|
269
|
+
if (typeof value === 'string' && optionSet.has(value as E)) {
|
|
270
|
+
return { valid: true, errors: [] };
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
valid: false,
|
|
274
|
+
errors: [
|
|
275
|
+
{
|
|
276
|
+
context,
|
|
277
|
+
value,
|
|
278
|
+
message: `Value '${String(value)}' is not a valid option. Expected one of: ${options.join(', ')}`,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Frozen Type
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
export function frozen<T = unknown>(): IFrozenType<T>;
|
|
291
|
+
export function frozen<T>(defaultValue: T): IFrozenType<T>;
|
|
292
|
+
export function frozen<T>(defaultValue?: T): IFrozenType<T> {
|
|
293
|
+
return {
|
|
294
|
+
name: 'frozen',
|
|
295
|
+
_kind: 'frozen',
|
|
296
|
+
_C: undefined as unknown as T,
|
|
297
|
+
_S: undefined as unknown as T,
|
|
298
|
+
_T: undefined as unknown as T,
|
|
299
|
+
|
|
300
|
+
create(snapshot?: T): T {
|
|
301
|
+
if (snapshot === undefined) {
|
|
302
|
+
if (defaultValue !== undefined) {
|
|
303
|
+
// Deep freeze if object
|
|
304
|
+
return deepFreeze(structuredClone(defaultValue));
|
|
305
|
+
}
|
|
306
|
+
return undefined as T;
|
|
307
|
+
}
|
|
308
|
+
// Deep freeze the snapshot
|
|
309
|
+
return deepFreeze(structuredClone(snapshot));
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
is(value: unknown): value is T {
|
|
313
|
+
// Frozen accepts any value
|
|
314
|
+
return true;
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
validate(): IValidationResult {
|
|
318
|
+
// Frozen accepts any value
|
|
319
|
+
return { valid: true, errors: [] };
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function deepFreeze<T>(obj: T): T {
|
|
325
|
+
if (obj === null || typeof obj !== 'object') {
|
|
326
|
+
return obj;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
Object.freeze(obj);
|
|
330
|
+
|
|
331
|
+
if (Array.isArray(obj)) {
|
|
332
|
+
obj.forEach(deepFreeze);
|
|
333
|
+
} else {
|
|
334
|
+
Object.values(obj).forEach(deepFreeze);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return obj;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ============================================================================
|
|
341
|
+
// Custom Type
|
|
342
|
+
// ============================================================================
|
|
343
|
+
|
|
344
|
+
export interface CustomTypeOptions<C, S, T> {
|
|
345
|
+
name: string;
|
|
346
|
+
fromSnapshot(snapshot: S): T;
|
|
347
|
+
toSnapshot(value: T): S;
|
|
348
|
+
isTargetType(value: unknown): value is T;
|
|
349
|
+
getValidationMessage(value: unknown): string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function custom<C, S, T>(options: CustomTypeOptions<C, S, T>): IType<C, S, T> {
|
|
353
|
+
return {
|
|
354
|
+
name: options.name,
|
|
355
|
+
_kind: 'simple' as const,
|
|
356
|
+
_C: undefined as unknown as C,
|
|
357
|
+
_S: undefined as unknown as S,
|
|
358
|
+
_T: undefined as unknown as T,
|
|
359
|
+
|
|
360
|
+
create(snapshot?: C): T {
|
|
361
|
+
if (snapshot === undefined) {
|
|
362
|
+
throw new Error(`[jotai-state-tree] A value for custom type '${options.name}' is required`);
|
|
363
|
+
}
|
|
364
|
+
return options.fromSnapshot(snapshot as unknown as S);
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
is(value: unknown): value is T {
|
|
368
|
+
return options.isTargetType(value);
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
validate(value: unknown, context: IValidationContext[]): IValidationResult {
|
|
372
|
+
if (options.isTargetType(value)) {
|
|
373
|
+
return { valid: true, errors: [] };
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
valid: false,
|
|
377
|
+
errors: [
|
|
378
|
+
{
|
|
379
|
+
context,
|
|
380
|
+
value,
|
|
381
|
+
message: options.getValidationMessage(value),
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Finite Number Type
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
/** Finite number type (excludes Infinity and -Infinity) */
|
|
394
|
+
export const finite: ISimpleType<number> = createSimpleType<number>(
|
|
395
|
+
'finite',
|
|
396
|
+
(value): value is number => typeof value === 'number' && isFinite(value)
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
/** Float type (alias for number) */
|
|
400
|
+
export const float = number;
|