@fumari/stf 1.0.0 → 1.0.1
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/dist/index.d.ts +13 -4
- package/dist/index.js +49 -15
- package/dist/lib/utils.d.ts +2 -1
- package/dist/lib/utils.js +11 -5
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -75,6 +75,10 @@ interface DataEngineListener {
|
|
|
75
75
|
* when `field` is specified, this is also fired when parent is deleted.
|
|
76
76
|
*/
|
|
77
77
|
onDelete?: (key: FieldKey, ctx: OnDeleteContext) => void;
|
|
78
|
+
/**
|
|
79
|
+
* For listener attached to a namespace's engine, this is fired when the namespace is removed.
|
|
80
|
+
*/
|
|
81
|
+
onUnmount?: () => void;
|
|
78
82
|
}
|
|
79
83
|
interface OnUpdateContext {
|
|
80
84
|
/**
|
|
@@ -94,8 +98,7 @@ interface OnInitContext {
|
|
|
94
98
|
}
|
|
95
99
|
declare class DataEngine {
|
|
96
100
|
private data;
|
|
97
|
-
|
|
98
|
-
private namespaces;
|
|
101
|
+
private readonly namespaces;
|
|
99
102
|
private readonly listeners;
|
|
100
103
|
constructor(defaultValues?: DefaultValue<NonNullable<object>>);
|
|
101
104
|
listen(listener: DataEngineListener): void;
|
|
@@ -119,7 +122,8 @@ declare class DataEngine {
|
|
|
119
122
|
* create an isolated data engine
|
|
120
123
|
*/
|
|
121
124
|
namespace(namespace: string, initialValue?: DefaultValue<NonNullable<object>>): DataEngine;
|
|
122
|
-
reset(data:
|
|
125
|
+
reset(data: NonNullable<object>): void;
|
|
126
|
+
clearNamespaces(): void;
|
|
123
127
|
}
|
|
124
128
|
declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
|
|
125
129
|
stf?: Stf | DataEngine;
|
|
@@ -135,5 +139,10 @@ declare function useFieldValue<V = unknown>(key: FieldKey, options?: {
|
|
|
135
139
|
declare function useListener(listener: DataEngineListener & {
|
|
136
140
|
stf?: Stf | DataEngine;
|
|
137
141
|
}): void;
|
|
142
|
+
declare function useNamespace(options: {
|
|
143
|
+
namespace: string;
|
|
144
|
+
initial?: DefaultValue<NonNullable<object>>;
|
|
145
|
+
stf?: Stf | DataEngine;
|
|
146
|
+
}): DataEngine;
|
|
138
147
|
//#endregion
|
|
139
|
-
export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, OnDeleteContext, OnInitContext, OnUpdateContext, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
|
|
148
|
+
export { ArrayItemInfo, DataEngine, DataEngineListener, DefaultValue, FieldKey, OnDeleteContext, OnInitContext, OnUpdateContext, PropertyItemInfo, Stf, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useNamespace, useObject, useStf };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey } from "./lib/utils.js";
|
|
3
|
+
import { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey } from "./lib/utils.js";
|
|
4
4
|
import { createContext, use, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
|
|
@@ -160,6 +160,10 @@ var ListenerManager = class {
|
|
|
160
160
|
for (const v of listeners) v.onDelete?.(field, ctx);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
onUnmount() {
|
|
164
|
+
for (const v of this.unindexed) v.onUnmount?.();
|
|
165
|
+
for (const listeners of this.indexed.values()) for (const v of listeners) v.onUnmount?.();
|
|
166
|
+
}
|
|
163
167
|
};
|
|
164
168
|
var DataEngine = class DataEngine {
|
|
165
169
|
constructor(defaultValues = {}) {
|
|
@@ -184,28 +188,33 @@ var DataEngine = class DataEngine {
|
|
|
184
188
|
*/
|
|
185
189
|
init(key, defaultValue, ctx = {}) {
|
|
186
190
|
if (key.length === 0) return this.data;
|
|
187
|
-
|
|
188
|
-
const currentKey = [];
|
|
191
|
+
const parentKey = [];
|
|
189
192
|
const parentUpdateCtx = {
|
|
190
193
|
swallow: true,
|
|
191
194
|
...ctx
|
|
192
195
|
};
|
|
196
|
+
let cur = this.data;
|
|
193
197
|
for (let i = 0; i < key.length; i++) {
|
|
194
198
|
const propKey = key[i];
|
|
195
199
|
const propValue = cur[propKey];
|
|
196
200
|
if (i === key.length - 1) {
|
|
197
201
|
if (propValue !== void 0) return propValue;
|
|
198
202
|
cur[propKey] = getDefaultValue(defaultValue);
|
|
199
|
-
this.listeners.onUpdate(
|
|
203
|
+
this.listeners.onUpdate(parentKey, parentUpdateCtx);
|
|
200
204
|
this.listeners.onInit(key, ctx);
|
|
201
205
|
return cur[propKey];
|
|
202
|
-
} else if (
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
} else if (isPlainObject(propValue)) {
|
|
207
|
+
cur = propValue;
|
|
208
|
+
parentKey.push(propKey);
|
|
209
|
+
} else {
|
|
205
210
|
cur = cur[propKey] = {};
|
|
206
|
-
this.listeners.onUpdate(
|
|
211
|
+
this.listeners.onUpdate(parentKey, parentUpdateCtx);
|
|
212
|
+
parentKey.push(propKey);
|
|
213
|
+
if (propValue !== void 0) {
|
|
214
|
+
console.warn(`the original value of field ${parentKey.join(".")} is overidden, this might be unexpected.`);
|
|
215
|
+
this.listeners.onUpdate(parentKey, parentUpdateCtx);
|
|
216
|
+
}
|
|
207
217
|
}
|
|
208
|
-
currentKey.push(propKey);
|
|
209
218
|
}
|
|
210
219
|
}
|
|
211
220
|
delete(key, ctx = {}) {
|
|
@@ -222,7 +231,7 @@ var DataEngine = class DataEngine {
|
|
|
222
231
|
...ctx
|
|
223
232
|
});
|
|
224
233
|
return deleted[0];
|
|
225
|
-
} else if (
|
|
234
|
+
} else if (isPlainObject(parent)) {
|
|
226
235
|
const temp = parent[prop];
|
|
227
236
|
delete parent[prop];
|
|
228
237
|
this.listeners.onDelete(key, ctx);
|
|
@@ -264,17 +273,27 @@ var DataEngine = class DataEngine {
|
|
|
264
273
|
return child;
|
|
265
274
|
}
|
|
266
275
|
reset(data) {
|
|
267
|
-
this.namespaces.clear();
|
|
268
276
|
this.update([], data);
|
|
269
277
|
}
|
|
278
|
+
clearNamespaces() {
|
|
279
|
+
for (const [name, engine] of this.namespaces) {
|
|
280
|
+
this.namespaces.delete(name);
|
|
281
|
+
engine.listeners.onUnmount();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
270
284
|
};
|
|
271
285
|
function useFieldValue(key, options = {}) {
|
|
272
|
-
const
|
|
273
|
-
const
|
|
286
|
+
const { stf, compute = (v) => v, defaultValue, isChanged = (a, b) => a !== b } = options;
|
|
287
|
+
const engine = useDataEngine(stf);
|
|
274
288
|
const [value, setValue] = useState(() => compute(engine.init(key, defaultValue)));
|
|
289
|
+
const prevEngineRef = useRef(engine);
|
|
290
|
+
if (prevEngineRef.current !== engine) {
|
|
291
|
+
setValue(compute(engine.init(key, defaultValue)));
|
|
292
|
+
prevEngineRef.current = engine;
|
|
293
|
+
}
|
|
275
294
|
useListener({
|
|
276
295
|
field: key,
|
|
277
|
-
stf
|
|
296
|
+
stf,
|
|
278
297
|
onInit() {
|
|
279
298
|
const computed = compute(engine.get(key));
|
|
280
299
|
setValue((prev) => isChanged(prev, computed) ? computed : prev);
|
|
@@ -297,6 +316,9 @@ function useListener(listener) {
|
|
|
297
316
|
useEffect(() => {
|
|
298
317
|
const internal = {
|
|
299
318
|
field: listener.field,
|
|
319
|
+
onUnmount(...args) {
|
|
320
|
+
return listenerRef.current.onUnmount?.(...args);
|
|
321
|
+
},
|
|
300
322
|
onDelete(...args) {
|
|
301
323
|
return listenerRef.current.onDelete?.(...args);
|
|
302
324
|
},
|
|
@@ -313,6 +335,18 @@ function useListener(listener) {
|
|
|
313
335
|
};
|
|
314
336
|
}, [engine, listener.field]);
|
|
315
337
|
}
|
|
338
|
+
function useNamespace(options) {
|
|
339
|
+
const { namespace, stf, initial } = options;
|
|
340
|
+
const engine = useDataEngine(stf);
|
|
341
|
+
const [value, setValue] = useState(() => engine.namespace(namespace, initial));
|
|
342
|
+
useListener({
|
|
343
|
+
stf: value,
|
|
344
|
+
onUnmount() {
|
|
345
|
+
setValue(engine.namespace(namespace, initial));
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
316
350
|
|
|
317
351
|
//#endregion
|
|
318
|
-
export { DataEngine, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useObject, useStf };
|
|
352
|
+
export { DataEngine, StfProvider, useArray, useDataEngine, useFieldValue, useListener, useNamespace, useObject, useStf };
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -17,5 +17,6 @@ declare function stringifyFieldKey(fieldKey: FieldKey): string;
|
|
|
17
17
|
* @returns if `a` starts with `b`.
|
|
18
18
|
*/
|
|
19
19
|
declare function fieldKeyStartsWith(a: string, b: string): boolean;
|
|
20
|
+
declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
20
21
|
//#endregion
|
|
21
|
-
export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
|
|
22
|
+
export { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey };
|
package/dist/lib/utils.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
function objectGet(obj, key) {
|
|
3
3
|
let cur = obj;
|
|
4
4
|
for (const prop of key) {
|
|
5
|
-
if (
|
|
5
|
+
if (!isPlainObject(cur) || !(prop in cur)) return;
|
|
6
6
|
cur = cur[prop];
|
|
7
7
|
}
|
|
8
8
|
return cur;
|
|
@@ -15,7 +15,7 @@ function objectGet(obj, key) {
|
|
|
15
15
|
function objectSet(obj, key, value) {
|
|
16
16
|
if (key.length === 0) return value;
|
|
17
17
|
const parent = objectGet(obj, key.slice(0, -1));
|
|
18
|
-
if (
|
|
18
|
+
if (!isPlainObject(parent)) throw new Error("missing parent object");
|
|
19
19
|
parent[key[key.length - 1]] = value;
|
|
20
20
|
return obj;
|
|
21
21
|
}
|
|
@@ -31,13 +31,14 @@ function deepEqual(a, b) {
|
|
|
31
31
|
return a.every((item, index) => deepEqual(item, b[index]));
|
|
32
32
|
}
|
|
33
33
|
if (Array.isArray(a) || Array.isArray(b)) return false;
|
|
34
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
34
35
|
const keysA = Object.keys(a);
|
|
35
36
|
const keysB = Object.keys(b);
|
|
36
37
|
if (keysA.length !== keysB.length) return false;
|
|
37
|
-
return keysA.every((key) =>
|
|
38
|
+
return keysA.every((key) => key in b && deepEqual(a[key], b[key]));
|
|
38
39
|
}
|
|
39
40
|
function stringifyFieldKey(fieldKey) {
|
|
40
|
-
return fieldKey.map((v) =>
|
|
41
|
+
return fieldKey.map((v) => typeof v === "string" ? `_${v}` : `n${v}`).join(".");
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
44
|
* @returns if `a` starts with `b`.
|
|
@@ -45,6 +46,11 @@ function stringifyFieldKey(fieldKey) {
|
|
|
45
46
|
function fieldKeyStartsWith(a, b) {
|
|
46
47
|
return b.length === 0 || a === b || a.startsWith(b + ".");
|
|
47
48
|
}
|
|
49
|
+
function isPlainObject(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null) return false;
|
|
51
|
+
const prototype = Object.getPrototypeOf(value);
|
|
52
|
+
return prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null;
|
|
53
|
+
}
|
|
48
54
|
|
|
49
55
|
//#endregion
|
|
50
|
-
export { deepEqual, fieldKeyStartsWith, objectGet, objectSet, stringifyFieldKey };
|
|
56
|
+
export { deepEqual, fieldKeyStartsWith, isPlainObject, objectGet, objectSet, stringifyFieldKey };
|