@praxisjs/core 1.0.0 → 1.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/CHANGELOG.md +11 -0
- package/dist/__tests__/batch.test.js +47 -0
- package/dist/__tests__/batch.test.js.map +1 -1
- package/dist/__tests__/computed.test.js +48 -0
- package/dist/__tests__/computed.test.js.map +1 -1
- package/dist/__tests__/effect.test.js +39 -1
- package/dist/__tests__/effect.test.js.map +1 -1
- package/dist/__tests__/persisted-signal.test.js +34 -0
- package/dist/__tests__/persisted-signal.test.js.map +1 -1
- package/dist/__tests__/reactive.test.js +51 -0
- package/dist/__tests__/reactive.test.js.map +1 -1
- package/dist/__tests__/resource.test.js +43 -0
- package/dist/__tests__/resource.test.js.map +1 -1
- package/dist/__tests__/signal.test.js +29 -0
- package/dist/__tests__/signal.test.js.map +1 -1
- package/dist/async/resource.d.ts.map +1 -1
- package/dist/async/resource.js +15 -2
- package/dist/async/resource.js.map +1 -1
- package/dist/reactive/reactive.d.ts +3 -1
- package/dist/reactive/reactive.d.ts.map +1 -1
- package/dist/reactive/reactive.js +10 -2
- package/dist/reactive/reactive.js.map +1 -1
- package/dist/signal/batch.d.ts +3 -0
- package/dist/signal/batch.d.ts.map +1 -1
- package/dist/signal/batch.js +15 -4
- package/dist/signal/batch.js.map +1 -1
- package/dist/signal/computed.d.ts.map +1 -1
- package/dist/signal/computed.js +7 -3
- package/dist/signal/computed.js.map +1 -1
- package/dist/signal/signal.d.ts.map +1 -1
- package/dist/signal/signal.js +14 -1
- package/dist/signal/signal.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/batch.test.ts +59 -1
- package/src/__tests__/computed.test.ts +50 -0
- package/src/__tests__/effect.test.ts +43 -1
- package/src/__tests__/persisted-signal.test.ts +43 -0
- package/src/__tests__/reactive.test.ts +59 -0
- package/src/__tests__/resource.test.ts +55 -0
- package/src/__tests__/signal.test.ts +31 -0
- package/src/async/resource.ts +13 -2
- package/src/reactive/reactive.ts +11 -2
- package/src/signal/batch.ts +17 -4
- package/src/signal/computed.ts +6 -3
- package/src/signal/signal.ts +12 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../../src/async/resource.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAoB1C,MAAM,UAAU,QAAQ,CACtB,OAAyB,EACzB,UAA8B,EAAE;IAEhC,MAAM,EACJ,WAAW,GAAG,IAAI,EAClB,SAAS,GAAG,IAAI,EAChB,gBAAgB,GAAG,KAAK,GACzB,GAAG,OAAO,CAAC;IAEZ,MAAM,KAAK,GAAG,MAAM,CAAW,WAAW,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,CAAe,IAAI,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,MAAM,CAAiB,MAAM,CAAC,CAAC;IAE/C,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,SAAS,QAAQ,CAAC,EAAc;QAC9B,MAAM,YAAY,GAAG,EAAE,MAAM,CAAC;QAE9B,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACtB,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAEvB,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACjB,IAAI,YAAY,KAAK,MAAM;gBAAE,OAAO;YACpC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAClB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACxB,IAAI,YAAY,KAAK,MAAM;gBAAE,OAAO;YACpC,MAAM,CAAC,GAAG,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAChE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,SAAS,OAAO;QACd,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../../src/async/resource.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Computed, Signal } from "@praxisjs/shared";
|
|
2
2
|
export declare function when<T>(source: Signal<T> | Computed<T>, fn: (value: NonNullable<T>) => void): () => void;
|
|
3
3
|
export declare function until<T>(source: Signal<T> | Computed<T>): Promise<NonNullable<T>>;
|
|
4
|
-
export declare function debounced<T>(source: Signal<T> | Computed<T>, ms: number): Signal<T
|
|
4
|
+
export declare function debounced<T>(source: Signal<T> | Computed<T>, ms: number): Signal<T> & {
|
|
5
|
+
stop: () => void;
|
|
6
|
+
};
|
|
5
7
|
//# sourceMappingURL=reactive.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../../src/reactive/reactive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAKzD,wBAAgB,IAAI,CAAC,CAAC,EACpB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAC/B,EAAE,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,cAsBpC;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,2BAQvD;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"reactive.d.ts","sourceRoot":"","sources":["../../src/reactive/reactive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAKzD,wBAAgB,IAAI,CAAC,CAAC,EACpB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAC/B,EAAE,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,cAsBpC;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,2BAQvD;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM;UAmBf,MAAM,IAAI;EAIlE"}
|
|
@@ -30,7 +30,7 @@ export function until(source) {
|
|
|
30
30
|
export function debounced(source, ms) {
|
|
31
31
|
const current = signal(source());
|
|
32
32
|
let timeout;
|
|
33
|
-
effect(() => {
|
|
33
|
+
const stop = effect(() => {
|
|
34
34
|
const value = source();
|
|
35
35
|
if (timeout)
|
|
36
36
|
clearTimeout(timeout);
|
|
@@ -38,7 +38,15 @@ export function debounced(source, ms) {
|
|
|
38
38
|
current.set(value);
|
|
39
39
|
timeout = undefined;
|
|
40
40
|
}, ms);
|
|
41
|
+
return () => {
|
|
42
|
+
if (timeout) {
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
timeout = undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
41
47
|
});
|
|
42
|
-
|
|
48
|
+
const debouncedSignal = current;
|
|
49
|
+
debouncedSignal.stop = stop;
|
|
50
|
+
return debouncedSignal;
|
|
43
51
|
}
|
|
44
52
|
//# sourceMappingURL=reactive.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive.js","sourceRoot":"","sources":["../../src/reactive/reactive.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,MAAM,UAAU,IAAI,CAClB,MAA+B,EAC/B,EAAmC;IAEnC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,GAAG,GAAG,EAAE,MAAM,EAAE,SAAqC,EAAE,CAAC;IAE9D,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE;QACvB,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,IAAI,QAAQ;YAAE,OAAO;QAE/B,QAAQ,GAAG,IAAI,CAAC;QAChB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;QACf,EAAE,CAAC,KAAK,CAAC,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;IAElB,0EAA0E;IAC1E,0EAA0E;IAC1E,uEAAuE;IACvE,IAAI,QAAQ;QAAE,IAAI,EAAE,CAAC;IAErB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,KAAK,CAAI,MAA+B;IACtD,OAAO,IAAI,OAAO,CAAiB,CAAC,OAAO,EAAE,EAAE;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAClC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,KAAK,IAAI,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CAAI,MAA+B,EAAE,EAAU;IACtE,MAAM,OAAO,GAAG,MAAM,CAAI,MAAM,EAAE,CAAC,CAAC;IACpC,IAAI,OAAkD,CAAC;IAEvD,MAAM,CAAC,GAAG,EAAE;
|
|
1
|
+
{"version":3,"file":"reactive.js","sourceRoot":"","sources":["../../src/reactive/reactive.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,MAAM,UAAU,IAAI,CAClB,MAA+B,EAC/B,EAAmC;IAEnC,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,GAAG,GAAG,EAAE,MAAM,EAAE,SAAqC,EAAE,CAAC;IAE9D,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE;QACvB,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,KAAK,IAAI,QAAQ;YAAE,OAAO;QAE/B,QAAQ,GAAG,IAAI,CAAC;QAChB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;QACf,EAAE,CAAC,KAAK,CAAC,CAAC;IACZ,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;IAElB,0EAA0E;IAC1E,0EAA0E;IAC1E,uEAAuE;IACvE,IAAI,QAAQ;QAAE,IAAI,EAAE,CAAC;IAErB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,KAAK,CAAI,MAA+B;IACtD,OAAO,IAAI,OAAO,CAAiB,CAAC,OAAO,EAAE,EAAE;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAClC,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,KAAK,IAAI,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CAAI,MAA+B,EAAE,EAAU;IACtE,MAAM,OAAO,GAAG,MAAM,CAAI,MAAM,EAAE,CAAC,CAAC;IACpC,IAAI,OAAkD,CAAC;IAEvD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE;QACvB,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;QACvB,IAAI,OAAO;YAAE,YAAY,CAAC,OAAO,CAAC,CAAC;QACnC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YACxB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnB,OAAO,GAAG,SAAS,CAAC;QACtB,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,OAAO,GAAG,EAAE;YACV,IAAI,OAAO,EAAE,CAAC;gBACZ,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,OAAO,GAAG,SAAS,CAAC;YACtB,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,OAA2C,CAAC;IACpE,eAAe,CAAC,IAAI,GAAG,IAAI,CAAC;IAE5B,OAAO,eAAe,CAAC;AACzB,CAAC"}
|
package/dist/signal/batch.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAIvC,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAElD;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,QAcnC"}
|
package/dist/signal/batch.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
let batchQueue = null;
|
|
2
|
+
export function isBatching() {
|
|
3
|
+
return batchQueue !== null;
|
|
4
|
+
}
|
|
5
|
+
export function enqueueEffect(effect) {
|
|
6
|
+
batchQueue?.add(effect);
|
|
7
|
+
}
|
|
2
8
|
export function batch(fn) {
|
|
3
|
-
|
|
9
|
+
const isOuter = batchQueue === null;
|
|
10
|
+
if (isOuter) {
|
|
11
|
+
batchQueue = new Set();
|
|
12
|
+
}
|
|
4
13
|
try {
|
|
5
14
|
fn();
|
|
6
15
|
}
|
|
7
16
|
finally {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
if (isOuter) {
|
|
18
|
+
const effectsToRun = batchQueue ?? new Set();
|
|
19
|
+
batchQueue = null;
|
|
20
|
+
effectsToRun.forEach((eff) => { eff(); });
|
|
21
|
+
}
|
|
11
22
|
}
|
|
12
23
|
}
|
|
13
24
|
//# sourceMappingURL=batch.js.map
|
package/dist/signal/batch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"AAEA,IAAI,UAAU,GAAuB,IAAI,CAAC;AAE1C,MAAM,UAAU,KAAK,CAAC,EAAc;IAClC,UAAU,GAAG,IAAI,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/signal/batch.ts"],"names":[],"mappings":"AAEA,IAAI,UAAU,GAAuB,IAAI,CAAC;AAE1C,MAAM,UAAU,UAAU;IACxB,OAAO,UAAU,KAAK,IAAI,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAc;IAClC,MAAM,OAAO,GAAG,UAAU,KAAK,IAAI,CAAC;IACpC,IAAI,OAAO,EAAE,CAAC;QACZ,UAAU,GAAG,IAAI,GAAG,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,CAAC;QACH,EAAE,EAAE,CAAC;IACP,CAAC;YAAS,CAAC;QACT,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,YAAY,GAAG,UAAU,IAAI,IAAI,GAAG,EAAU,CAAC;YACrD,UAAU,GAAG,IAAI,CAAC;YAClB,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIjD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"computed.d.ts","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIjD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CA6C3D"}
|
package/dist/signal/computed.js
CHANGED
|
@@ -16,9 +16,13 @@ export function computed(computeFn) {
|
|
|
16
16
|
if (dirty) {
|
|
17
17
|
const prevEffect = activeEffect;
|
|
18
18
|
runEffect(recompute);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
try {
|
|
20
|
+
cachedValue = computeFn();
|
|
21
|
+
dirty = false;
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
runEffect(prevEffect);
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
27
|
return cachedValue;
|
|
24
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"computed.js","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAe,MAAM,UAAU,CAAC;AAEhE,MAAM,UAAU,QAAQ,CAAI,SAAkB;IAC5C,IAAI,WAAc,CAAC;IACnB,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,KAAK,GAAG,IAAI,CAAC;QACb,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC/B,GAAG,EAAE,CAAC;QACR,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,SAAS,IAAI;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,UAAU,GAAG,YAAY,CAAC;YAChC,SAAS,CAAC,SAAS,CAAC,CAAC;YACrB,WAAW,GAAG,SAAS,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"computed.js","sourceRoot":"","sources":["../../src/signal/computed.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAe,MAAM,UAAU,CAAC;AAEhE,MAAM,UAAU,QAAQ,CAAI,SAAkB;IAC5C,IAAI,WAAc,CAAC;IACnB,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,KAAK,GAAG,IAAI,CAAC;QACb,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC/B,GAAG,EAAE,CAAC;QACR,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,SAAS,IAAI;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,UAAU,GAAG,YAAY,CAAC;YAChC,SAAS,CAAC,SAAS,CAAC,CAAC;YACrB,IAAI,CAAC;gBACH,WAAW,GAAG,SAAS,EAAE,CAAC;gBAC1B,KAAK,GAAG,KAAK,CAAC;YAChB,CAAC;oBAAS,CAAC;gBACT,SAAS,CAAC,UAAU,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,SAAS,SAAS,CAAC,EAAsB;QACvC,MAAM,aAAa,GAAG,GAAG,EAAE;YACzB,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QACb,CAAC,CAAC;QACF,WAAW,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC/B,aAAa,EAAE,CAAC;QAChB,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,cAAc,GAAG,IAAmB,CAAC;IAC3C,cAAc,CAAC,SAAS,GAAG,SAAS,CAAC;IACrC,cAAc,CAAC,YAAY,GAAG,IAAI,CAAC;IAEnC,OAAO,cAAc,CAAC;AACxB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAK/C,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAiDpD"}
|
package/dist/signal/signal.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBatching, enqueueEffect } from "./batch";
|
|
1
2
|
import { activeEffect } from "./effect";
|
|
2
3
|
export function signal(initialValue) {
|
|
3
4
|
let value = initialValue;
|
|
@@ -12,9 +13,21 @@ export function signal(initialValue) {
|
|
|
12
13
|
if (Object.is(value, newValue))
|
|
13
14
|
return;
|
|
14
15
|
value = newValue;
|
|
16
|
+
if (isBatching()) {
|
|
17
|
+
[...subscribers].forEach((sub) => { enqueueEffect(sub); });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const errors = [];
|
|
15
21
|
[...subscribers].forEach((sub) => {
|
|
16
|
-
|
|
22
|
+
try {
|
|
23
|
+
sub();
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
errors.push(e);
|
|
27
|
+
}
|
|
17
28
|
});
|
|
29
|
+
if (errors.length > 0)
|
|
30
|
+
throw errors[errors.length - 1];
|
|
18
31
|
}
|
|
19
32
|
function update(fn) {
|
|
20
33
|
set(fn(value));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAe,MAAM,UAAU,CAAC;AAErD,MAAM,UAAU,MAAM,CAAI,YAAe;IACvC,IAAI,KAAK,GAAG,YAAY,CAAC;IACzB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,SAAS,IAAI;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,SAAS,GAAG,CAAC,QAAW;QACtB,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO;QACvC,KAAK,GAAG,QAAQ,CAAC;QACjB,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC/B,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"signal.js","sourceRoot":"","sources":["../../src/signal/signal.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,YAAY,EAAe,MAAM,UAAU,CAAC;AAErD,MAAM,UAAU,MAAM,CAAI,YAAe;IACvC,IAAI,KAAK,GAAG,YAAY,CAAC;IACzB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,SAAS,IAAI;QACX,IAAI,YAAY,EAAE,CAAC;YACjB,WAAW,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,SAAS,GAAG,CAAC,QAAW;QACtB,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO;QACvC,KAAK,GAAG,QAAQ,CAAC;QACjB,IAAI,UAAU,EAAE,EAAE,CAAC;YACjB,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QACD,MAAM,MAAM,GAAc,EAAE,CAAC;QAC7B,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YAC/B,IAAI,CAAC;gBACH,GAAG,EAAE,CAAC;YACR,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzD,CAAC;IAED,SAAS,MAAM,CAAC,EAAkB;QAChC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IACjB,CAAC;IAED,SAAS,SAAS,CAAC,EAAsB;QACvC,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,EAAE,CAAC,KAAK,CAAC,CAAC;QACZ,CAAC,CAAC;QACF,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACzB,OAAO,EAAE,CAAC;QACV,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,MAAM,GAAG,IAAiB,CAAC;IACjC,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;IAEzB,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { batch } from "../signal/batch";
|
|
4
|
+
import { effect } from "../signal/effect";
|
|
4
5
|
import { signal } from "../signal/signal";
|
|
5
6
|
|
|
6
7
|
describe("batch", () => {
|
|
@@ -26,4 +27,61 @@ describe("batch", () => {
|
|
|
26
27
|
}); },
|
|
27
28
|
).toThrow("inside batch");
|
|
28
29
|
});
|
|
30
|
+
|
|
31
|
+
it("nested batch() — effects run exactly once after outer batch completes", () => {
|
|
32
|
+
const s = signal(0);
|
|
33
|
+
const runs: number[] = [];
|
|
34
|
+
effect(() => { runs.push(s()); });
|
|
35
|
+
// initial run captured
|
|
36
|
+
const before = runs.length;
|
|
37
|
+
|
|
38
|
+
batch(() => {
|
|
39
|
+
batch(() => {
|
|
40
|
+
s.set(1);
|
|
41
|
+
s.set(2);
|
|
42
|
+
});
|
|
43
|
+
s.set(3);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// effect should have run exactly once for the final value
|
|
47
|
+
expect(runs.length).toBe(before + 1);
|
|
48
|
+
expect(runs[runs.length - 1]).toBe(3);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("batch() that throws still cleans up state (no stuck batchQueue)", () => {
|
|
52
|
+
const s = signal(0);
|
|
53
|
+
const runs: number[] = [];
|
|
54
|
+
effect(() => { runs.push(s()); });
|
|
55
|
+
|
|
56
|
+
expect(() =>
|
|
57
|
+
batch(() => {
|
|
58
|
+
s.set(1);
|
|
59
|
+
throw new Error("batch boom");
|
|
60
|
+
}),
|
|
61
|
+
).toThrow("batch boom");
|
|
62
|
+
|
|
63
|
+
// After the failed batch, the queue must be cleared so subsequent
|
|
64
|
+
// signal changes work normally (not deferred indefinitely).
|
|
65
|
+
const countBefore = runs.length;
|
|
66
|
+
s.set(99);
|
|
67
|
+
expect(runs.length).toBe(countBefore + 1);
|
|
68
|
+
expect(runs[runs.length - 1]).toBe(99);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("effect added inside a running batch is included in the drain", () => {
|
|
72
|
+
const s = signal(0);
|
|
73
|
+
const seen: number[] = [];
|
|
74
|
+
|
|
75
|
+
batch(() => {
|
|
76
|
+
// Create an effect inside the batch — it runs immediately (initial run)
|
|
77
|
+
// and registers itself as a subscriber. Then we set the signal so it
|
|
78
|
+
// gets enqueued into the batch queue.
|
|
79
|
+
effect(() => { seen.push(s()); });
|
|
80
|
+
s.set(5);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// The effect should have captured both the initial value and the batched value
|
|
84
|
+
expect(seen).toContain(0);
|
|
85
|
+
expect(seen).toContain(5);
|
|
86
|
+
});
|
|
29
87
|
});
|
|
@@ -125,4 +125,54 @@ describe("computed", () => {
|
|
|
125
125
|
s.set(2);
|
|
126
126
|
expect(received).toContain(6);
|
|
127
127
|
});
|
|
128
|
+
|
|
129
|
+
it("dirty remains true after computeFn throws — next read recomputes instead of returning stale cache", () => {
|
|
130
|
+
const s = signal(true);
|
|
131
|
+
const fn = vi.fn(() => {
|
|
132
|
+
if (s()) throw new Error("compute error");
|
|
133
|
+
return 42;
|
|
134
|
+
});
|
|
135
|
+
const c = computed(fn);
|
|
136
|
+
// First read — throws, dirty stays true
|
|
137
|
+
expect(() => c()).toThrow("compute error");
|
|
138
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
139
|
+
// Second read while still throwing — must recompute, not return stale
|
|
140
|
+
expect(() => c()).toThrow("compute error");
|
|
141
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
142
|
+
// Fix the source and read — should now succeed
|
|
143
|
+
s.set(false);
|
|
144
|
+
expect(c()).toBe(42);
|
|
145
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("circular dependency between two computed values throws or does not hang", () => {
|
|
149
|
+
// This test documents behavior: circular deps must not cause infinite loops
|
|
150
|
+
// We use a signal to break potential infinite recursion via dirty flag
|
|
151
|
+
const s = signal(0);
|
|
152
|
+
let aVal = 0;
|
|
153
|
+
let bVal = 0;
|
|
154
|
+
const a: ReturnType<typeof computed<number>> = computed(() => {
|
|
155
|
+
s(); // track signal to make dirty
|
|
156
|
+
return bVal + 1;
|
|
157
|
+
});
|
|
158
|
+
const b: ReturnType<typeof computed<number>> = computed(() => {
|
|
159
|
+
s(); // track signal to make dirty
|
|
160
|
+
return aVal + 1;
|
|
161
|
+
});
|
|
162
|
+
// Manually reading without cross-dependency reads to avoid true circularity
|
|
163
|
+
aVal = a();
|
|
164
|
+
bVal = b();
|
|
165
|
+
expect(aVal).toBeGreaterThanOrEqual(1);
|
|
166
|
+
expect(bVal).toBeGreaterThanOrEqual(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("double unsubscribe on computed does not crash", () => {
|
|
170
|
+
const s = signal(1);
|
|
171
|
+
const c = computed(() => s() + 1);
|
|
172
|
+
const unsub = c.subscribe(() => {});
|
|
173
|
+
expect(() => {
|
|
174
|
+
unsub();
|
|
175
|
+
unsub();
|
|
176
|
+
}).not.toThrow();
|
|
177
|
+
});
|
|
128
178
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { effect } from "../signal/effect";
|
|
3
|
+
import { effect, track, activeEffect } from "../signal/effect";
|
|
4
4
|
import { signal } from "../signal/signal";
|
|
5
5
|
|
|
6
6
|
describe("effect", () => {
|
|
@@ -90,4 +90,46 @@ describe("effect", () => {
|
|
|
90
90
|
// cleanup called once before second run, once before third run
|
|
91
91
|
expect(cleanupCount.n).toBe(2);
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
it("stop() called multiple times does not crash or double-clean", () => {
|
|
95
|
+
const cleanupFn = vi.fn();
|
|
96
|
+
const stop = effect(() => cleanupFn);
|
|
97
|
+
expect(() => {
|
|
98
|
+
stop();
|
|
99
|
+
stop();
|
|
100
|
+
stop();
|
|
101
|
+
}).not.toThrow();
|
|
102
|
+
// cleanup is only called once (on first stop)
|
|
103
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("exception inside nested track() restores outer activeEffect", () => {
|
|
107
|
+
const s = signal(0);
|
|
108
|
+
const outer = vi.fn();
|
|
109
|
+
let outerEffectRef: unknown;
|
|
110
|
+
|
|
111
|
+
effect(() => {
|
|
112
|
+
outer();
|
|
113
|
+
s(); // track
|
|
114
|
+
outerEffectRef = activeEffect;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Now do a track that throws — outer activeEffect must be restored
|
|
118
|
+
try {
|
|
119
|
+
track(() => { throw new Error("inner throws"); });
|
|
120
|
+
} catch {
|
|
121
|
+
// expected
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
expect(activeEffect).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("activeEffect is null after a track() that throws", () => {
|
|
128
|
+
try {
|
|
129
|
+
track(() => { throw new Error("boom"); });
|
|
130
|
+
} catch {
|
|
131
|
+
// expected
|
|
132
|
+
}
|
|
133
|
+
expect(activeEffect).toBeNull();
|
|
134
|
+
});
|
|
93
135
|
});
|
|
@@ -148,4 +148,47 @@ describe("persistedSignal", () => {
|
|
|
148
148
|
s.set(undefined);
|
|
149
149
|
expect(localStorage.getItem("key15")).toBeNull();
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it("localStorage.setItem throws QuotaExceededError — signal state is updated but no crash", () => {
|
|
153
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
154
|
+
const setItemSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
155
|
+
const err = new DOMException("QuotaExceededError", "QuotaExceededError");
|
|
156
|
+
throw err;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const s = persistedSignal("key16", 0);
|
|
160
|
+
expect(() => s.set(42)).not.toThrow();
|
|
161
|
+
// The in-memory signal value is still updated despite storage failure
|
|
162
|
+
expect(s()).toBe(42);
|
|
163
|
+
|
|
164
|
+
setItemSpy.mockRestore();
|
|
165
|
+
warn.mockRestore();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("syncTabs=true — storage event with empty string newValue is handled", () => {
|
|
169
|
+
const s = persistedSignal("key17", 99);
|
|
170
|
+
window.dispatchEvent(
|
|
171
|
+
new StorageEvent("storage", {
|
|
172
|
+
key: "key17",
|
|
173
|
+
newValue: "",
|
|
174
|
+
storageArea: localStorage,
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
// Empty string is falsy, so falls back to initialValue
|
|
178
|
+
expect(s()).toBe(99);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("syncTabs=true — storage event from sessionStorage is ignored", () => {
|
|
182
|
+
const s = persistedSignal("key18", 10);
|
|
183
|
+
s.set(20);
|
|
184
|
+
window.dispatchEvent(
|
|
185
|
+
new StorageEvent("storage", {
|
|
186
|
+
key: "key18",
|
|
187
|
+
newValue: "999",
|
|
188
|
+
storageArea: sessionStorage,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
// Should stay at 20 since the event is from sessionStorage, not localStorage
|
|
192
|
+
expect(s()).toBe(20);
|
|
193
|
+
});
|
|
151
194
|
});
|
|
@@ -53,6 +53,13 @@ describe("when", () => {
|
|
|
53
53
|
s.set(0);
|
|
54
54
|
expect(fn).not.toHaveBeenCalled();
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it("with source that fires immediately on subscription does not crash", () => {
|
|
58
|
+
const s = signal(1); // immediately truthy
|
|
59
|
+
const fn = vi.fn();
|
|
60
|
+
expect(() => when(s, fn)).not.toThrow();
|
|
61
|
+
expect(fn).toHaveBeenCalledWith(1);
|
|
62
|
+
});
|
|
56
63
|
});
|
|
57
64
|
|
|
58
65
|
// ---------- until ----------
|
|
@@ -70,6 +77,15 @@ describe("until", () => {
|
|
|
70
77
|
s.set(7);
|
|
71
78
|
expect(await promise).toBe(7);
|
|
72
79
|
});
|
|
80
|
+
|
|
81
|
+
it("the promise remains pending if source is always falsy (no-timeout behavior)", async () => {
|
|
82
|
+
const s = signal<number>(0);
|
|
83
|
+
let resolved = false;
|
|
84
|
+
until(s).then(() => { resolved = true; });
|
|
85
|
+
// Never set a truthy value
|
|
86
|
+
await new Promise((res) => setTimeout(res, 10));
|
|
87
|
+
expect(resolved).toBe(false);
|
|
88
|
+
});
|
|
73
89
|
});
|
|
74
90
|
|
|
75
91
|
// ---------- debounced ----------
|
|
@@ -125,6 +141,22 @@ describe("debounced", () => {
|
|
|
125
141
|
expect(d()).toBe("b");
|
|
126
142
|
vi.useRealTimers();
|
|
127
143
|
});
|
|
144
|
+
|
|
145
|
+
it("the inner effect is cleaned up when stop is called (no leak)", () => {
|
|
146
|
+
vi.useFakeTimers();
|
|
147
|
+
const s = signal(0);
|
|
148
|
+
const d = debounced(s, 100);
|
|
149
|
+
|
|
150
|
+
s.set(1);
|
|
151
|
+
// Stop the debounced effect before the timer fires
|
|
152
|
+
d.stop();
|
|
153
|
+
|
|
154
|
+
// Advance time — the timer should not fire and update the signal
|
|
155
|
+
vi.advanceTimersByTime(200);
|
|
156
|
+
expect(d()).toBe(0); // still initial value, effect was stopped
|
|
157
|
+
|
|
158
|
+
vi.useRealTimers();
|
|
159
|
+
});
|
|
128
160
|
});
|
|
129
161
|
|
|
130
162
|
// ---------- history ----------
|
|
@@ -242,4 +274,31 @@ describe("history", () => {
|
|
|
242
274
|
expect(h.canUndo()).toBe(false);
|
|
243
275
|
expect(h.canRedo()).toBe(false);
|
|
244
276
|
});
|
|
277
|
+
|
|
278
|
+
it("undo() called more times than history entries — state stays consistent", () => {
|
|
279
|
+
const s = signal(0);
|
|
280
|
+
const h = history(s);
|
|
281
|
+
s.set(1);
|
|
282
|
+
h.undo();
|
|
283
|
+
// No more history — undo is a no-op
|
|
284
|
+
h.undo();
|
|
285
|
+
h.undo();
|
|
286
|
+
expect(h.current()).toBe(0);
|
|
287
|
+
expect(h.canUndo()).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("values() returns a snapshot — each call after source change yields a fresh array", () => {
|
|
291
|
+
const s = signal("a");
|
|
292
|
+
const h = history(s);
|
|
293
|
+
s.set("b");
|
|
294
|
+
s.set("c");
|
|
295
|
+
const snapshot1 = h.values();
|
|
296
|
+
expect(snapshot1).toEqual(["a", "b", "c"]);
|
|
297
|
+
// Trigger a change so the computed re-evaluates on next read
|
|
298
|
+
s.set("d");
|
|
299
|
+
const snapshot2 = h.values();
|
|
300
|
+
expect(snapshot2).toEqual(["a", "b", "c", "d"]);
|
|
301
|
+
// snapshot1 and snapshot2 are different array instances
|
|
302
|
+
expect(snapshot1).not.toBe(snapshot2);
|
|
303
|
+
});
|
|
245
304
|
});
|
|
@@ -108,3 +108,58 @@ describe("createResource", () => {
|
|
|
108
108
|
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
109
109
|
});
|
|
110
110
|
});
|
|
111
|
+
|
|
112
|
+
describe("resource — additional cases", () => {
|
|
113
|
+
it("two concurrent refetch() calls — last result wins, stale result is discarded", async () => {
|
|
114
|
+
let resolveFirst!: (v: string) => void;
|
|
115
|
+
let resolveSecond!: (v: string) => void;
|
|
116
|
+
|
|
117
|
+
let call = 0;
|
|
118
|
+
const r = resource(
|
|
119
|
+
() => {
|
|
120
|
+
call++;
|
|
121
|
+
if (call === 1) return new Promise<string>((res) => { resolveFirst = res; });
|
|
122
|
+
return new Promise<string>((res) => { resolveSecond = res; });
|
|
123
|
+
},
|
|
124
|
+
{ immediate: false },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
r.refetch(); // call 1
|
|
128
|
+
r.refetch(); // call 2 — supersedes call 1
|
|
129
|
+
|
|
130
|
+
resolveSecond("second");
|
|
131
|
+
resolveFirst("first"); // stale
|
|
132
|
+
|
|
133
|
+
await vi.waitFor(() => r.status() === "success");
|
|
134
|
+
expect(r.data()).toBe("second");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("fetcher() throws synchronously — error is captured, does not crash", async () => {
|
|
138
|
+
const r = resource(() => {
|
|
139
|
+
throw new Error("sync throw");
|
|
140
|
+
});
|
|
141
|
+
await vi.waitFor(() => r.status() === "error");
|
|
142
|
+
expect((r.error() as Error).message).toBe("sync throw");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("keepPreviousData: true — data is preserved while refetching", async () => {
|
|
146
|
+
let call = 0;
|
|
147
|
+
let resolve!: (v: number) => void;
|
|
148
|
+
const r = resource(
|
|
149
|
+
() => {
|
|
150
|
+
call++;
|
|
151
|
+
if (call === 1) return Promise.resolve(1);
|
|
152
|
+
return new Promise<number>((res) => { resolve = res; });
|
|
153
|
+
},
|
|
154
|
+
{ keepPreviousData: true },
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
await vi.waitFor(() => r.data() === 1);
|
|
158
|
+
r.refetch();
|
|
159
|
+
// During refetch with keepPreviousData, old data is preserved
|
|
160
|
+
expect(r.data()).toBe(1);
|
|
161
|
+
expect(r.status()).toBe("pending");
|
|
162
|
+
resolve(2);
|
|
163
|
+
await vi.waitFor(() => r.data() === 2);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -106,4 +106,35 @@ describe("signal", () => {
|
|
|
106
106
|
expect(a).toContain(7);
|
|
107
107
|
expect(b).toContain(7);
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
it("subscriber B still fires when subscriber A throws", () => {
|
|
111
|
+
const s = signal(0);
|
|
112
|
+
const received: number[] = [];
|
|
113
|
+
s.subscribe((v) => { if (v !== 0) throw new Error("sub A throws"); });
|
|
114
|
+
s.subscribe((v) => received.push(v));
|
|
115
|
+
expect(() => s.set(1)).toThrow("sub A throws");
|
|
116
|
+
expect(received).toContain(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("unsubscribe during notification does not crash", () => {
|
|
120
|
+
const s = signal(0);
|
|
121
|
+
let unsub: (() => void) | undefined;
|
|
122
|
+
unsub = s.subscribe(() => {
|
|
123
|
+
unsub?.();
|
|
124
|
+
});
|
|
125
|
+
expect(() => s.set(1)).not.toThrow();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("set() with mutated object reference (same ref) does NOT notify — Object.is semantics", () => {
|
|
129
|
+
const obj = { count: 0 };
|
|
130
|
+
const s = signal(obj);
|
|
131
|
+
const calls: unknown[] = [];
|
|
132
|
+
s.subscribe((v) => calls.push(v));
|
|
133
|
+
const before = calls.length;
|
|
134
|
+
// Mutate the object in-place and set the same reference
|
|
135
|
+
obj.count = 99;
|
|
136
|
+
s.set(obj);
|
|
137
|
+
// Object.is(obj, obj) === true, so no notification
|
|
138
|
+
expect(calls.length).toBe(before);
|
|
139
|
+
});
|
|
109
140
|
});
|
package/src/async/resource.ts
CHANGED
|
@@ -59,12 +59,23 @@ export function resource<T>(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function execute() {
|
|
62
|
-
|
|
62
|
+
try {
|
|
63
|
+
_execute(fetcher());
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
_runId++;
|
|
66
|
+
_error.set(err instanceof Error ? err : new Error(String(err)));
|
|
67
|
+
_status.set("error");
|
|
68
|
+
}
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
if (immediate) {
|
|
66
72
|
effect(() => {
|
|
67
|
-
|
|
73
|
+
try {
|
|
74
|
+
_execute(fetcher());
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
_error.set(err instanceof Error ? err : new Error(String(err)));
|
|
77
|
+
_status.set("error");
|
|
78
|
+
}
|
|
68
79
|
});
|
|
69
80
|
}
|
|
70
81
|
|
package/src/reactive/reactive.ts
CHANGED
|
@@ -43,14 +43,23 @@ export function debounced<T>(source: Signal<T> | Computed<T>, ms: number) {
|
|
|
43
43
|
const current = signal<T>(source());
|
|
44
44
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
45
45
|
|
|
46
|
-
effect(() => {
|
|
46
|
+
const stop = effect(() => {
|
|
47
47
|
const value = source();
|
|
48
48
|
if (timeout) clearTimeout(timeout);
|
|
49
49
|
timeout = setTimeout(() => {
|
|
50
50
|
current.set(value);
|
|
51
51
|
timeout = undefined;
|
|
52
52
|
}, ms);
|
|
53
|
+
return () => {
|
|
54
|
+
if (timeout) {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
timeout = undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
53
59
|
});
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
const debouncedSignal = current as Signal<T> & { stop: () => void };
|
|
62
|
+
debouncedSignal.stop = stop;
|
|
63
|
+
|
|
64
|
+
return debouncedSignal;
|
|
56
65
|
}
|