@zeix/cause-effect 0.15.1 → 0.16.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/.ai-context.md +254 -0
- package/.cursorrules +54 -0
- package/.github/copilot-instructions.md +132 -0
- package/CLAUDE.md +319 -0
- package/README.md +167 -159
- package/eslint.config.js +1 -1
- package/index.dev.js +528 -407
- package/index.js +1 -1
- package/index.ts +36 -25
- package/package.json +1 -1
- package/src/computed.ts +41 -30
- package/src/diff.ts +57 -44
- package/src/effect.ts +15 -16
- package/src/errors.ts +64 -0
- package/src/match.ts +2 -2
- package/src/resolve.ts +2 -2
- package/src/signal.ts +27 -49
- package/src/state.ts +27 -19
- package/src/store.ts +410 -209
- package/src/system.ts +122 -0
- package/src/util.ts +45 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +508 -72
- package/test/diff.test.ts +321 -4
- package/test/effect.test.ts +61 -61
- package/test/match.test.ts +38 -28
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +19 -147
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +1370 -134
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +10 -9
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/diff.d.ts +5 -3
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +22 -0
- package/types/src/match.d.ts +1 -1
- package/types/src/resolve.d.ts +1 -1
- package/types/src/signal.d.ts +12 -19
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +40 -36
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +7 -5
- package/index.d.ts +0 -36
- package/src/scheduler.ts +0 -172
- package/types/test-new-effect.d.ts +0 -1
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
class W extends Error{constructor($){super(`Circular dependency detected in ${$}`);this.name="CircularDependencyError"}}class P extends TypeError{constructor($,x){super(`Invalid ${$} callback ${x}`);this.name="InvalidCallbackError"}}class n extends TypeError{constructor($,x){super(`Invalid signal value ${x} in ${$}`);this.name="InvalidSignalValueError"}}class L extends TypeError{constructor($){super(`Nullish signal values are not allowed in ${$}`);this.name="NullishSignalValueError"}}class u extends Error{constructor($,x){super(`Could not add store key "${$}" with value ${x} because it already exists`);this.name="StoreKeyExistsError"}}class c extends RangeError{constructor($){super(`Could not remove store index ${String($)} because it is out of range`);this.name="StoreKeyRangeError"}}class t extends Error{constructor($,x){super(`Could not set store key "${$}" to ${x} because it is readonly`);this.name="StoreKeyReadonlyError"}}var K=Symbol(),x$=($)=>typeof $==="string",H$=($)=>typeof $==="number",p=($)=>typeof $==="symbol",Y=($)=>typeof $==="function",d=($)=>Y($)&&$.constructor.name==="AsyncFunction",E=($,x)=>Object.prototype.toString.call($)===`[object ${x}]`,A=($)=>E($,"Object"),s=($)=>A($)||Array.isArray($),K$=($)=>{if(!$.length)return null;let x=$.map((G)=>x$(G)?parseInt(G,10):H$(G)?G:NaN);return x.every((G)=>Number.isFinite(G)&&G>=0)?x.sort((G,J)=>G-J):null};var g=($)=>$ instanceof DOMException&&$.name==="AbortError",_=($)=>$ instanceof Error?$:Error(String($));var i=($)=>{let x=K$(Object.keys($));if(x===null)return $;let G=[];for(let J of x)G.push($[String(J)]);return G},U=($)=>x$($)?`"${$}"`:!!$&&typeof $==="object"?JSON.stringify($):String($);var f=($,x,G)=>{if(Object.is($,x))return!0;if(typeof $!==typeof x)return!1;if(typeof $!=="object"||$===null||x===null)return!1;if(!G)G=new WeakSet;if(G.has($)||G.has(x))throw new W("isEqual");G.add($),G.add(x);try{if(Array.isArray($)&&Array.isArray(x)){if($.length!==x.length)return!1;for(let J=0;J<$.length;J++)if(!f($[J],x[J],G))return!1;return!0}if(Array.isArray($)!==Array.isArray(x))return!1;if(A($)&&A(x)){let J=Object.keys($),Q=Object.keys(x);if(J.length!==Q.length)return!1;for(let M of J){if(!(M in x))return!1;if(!f($[M],x[M],G))return!1}return!0}return!1}finally{G.delete($),G.delete(x)}},R$=($,x)=>{let G=s($),J=s(x);if(!G||!J){let q=!Object.is($,x);return{changed:q,add:q&&J?x:{},change:{},remove:q&&G?$:{}}}let Q=new WeakSet,M={},I={},C={},D=Object.keys($),j=Object.keys(x),O=new Set([...D,...j]);for(let q of O){let N=q in $,T=q in x;if(!N&&T){M[q]=x[q];continue}else if(N&&!T){C[q]=K;continue}let B=$[q],R=x[q];if(!f(B,R,Q))I[q]=R}return{changed:Object.keys(M).length>0||Object.keys(I).length>0||Object.keys(C).length>0,add:M,change:I,remove:C}};var w,r=new Set,B$=0,V=($)=>{let x=new Set,G=$;return G.unwatch=(J)=>{x.add(J)},G.cleanup=()=>{for(let J of x)J();x.clear()},G},m=($)=>{if(w&&!$.has(w)){let x=w;x.unwatch(()=>{$.delete(x)}),$.add(x)}},F=($)=>{for(let x of $)if(B$)r.add(x);else x()},l=()=>{while(r.size){let $=Array.from(r);r.clear();for(let x of $)x()}},J$=($)=>{B$++;try{$()}finally{l(),B$--}},y=($,x)=>{let G=w;w=x;try{$()}finally{w=G}};var G$="Computed",M$=($,x=K)=>{if(!a($))throw new P("computed",U($));if(x==null)throw new L("computed");let G=new Set,J=x,Q,M,I=!0,C=!1,D=!1,j=(B)=>{if(!f(B,J))J=B,C=!0;Q=void 0,I=!1},O=()=>{C=K!==J,J=K,Q=void 0},S=(B)=>{let R=_(B);C=!Q||R.name!==Q.name||R.message!==Q.message,J=K,Q=R},q=(B)=>(R)=>{if(D=!1,M=void 0,B(R),C)F(G)},N=V(()=>{if(I=!0,M?.abort(),G.size)F(G);else N.cleanup()});N.unwatch(()=>{M?.abort()});let T=()=>y(()=>{if(D)throw new W("computed");if(C=!1,d($)){if(M)return J;M=new AbortController,M.signal.addEventListener("abort",()=>{D=!1,M=void 0,T()},{once:!0})}let B;D=!0;try{B=M?$(J,M.signal):$(J)}catch(R){if(g(R))O();else S(R);D=!1;return}if(B instanceof Promise)B.then(q(j),q(S));else if(B==null||K===B)O();else j(B);D=!1},N);return{[Symbol.toStringTag]:G$,get:()=>{if(m(G),l(),I)T();if(Q)throw Q;return J}}},o=($)=>E($,G$),a=($)=>Y($)&&$.length<3;var q$=($)=>{if(!Y($)||$.length>1)throw new P("effect",U($));let x=d($),G=!1,J,Q=V(()=>y(()=>{if(G)throw new W("effect");G=!0,J?.abort(),J=void 0;let M;try{if(x){J=new AbortController;let I=J;$(J.signal).then((C)=>{if(Y(C)&&J===I)Q.unwatch(C)}).catch((C)=>{if(!g(C))console.error("Async effect error:",C)})}else if(M=$(),Y(M))Q.unwatch(M)}catch(I){if(!g(I))console.error("Effect callback error:",I)}G=!1},Q));return Q(),()=>{J?.abort(),Q.cleanup()}};function D$($,x){try{if($.pending)x.nil?.();else if($.errors)x.err?.($.errors);else if($.ok)x.ok($.values)}catch(G){if(x.err&&(!$.errors||!$.errors.includes(_(G))))x.err($.errors?[...$.errors,_(G)]:[_(G)]);else throw G}}function z$($){let x=[],G=!1,J={};for(let[Q,M]of Object.entries($))try{let I=M.get();if(I===K)G=!0;else J[Q]=I}catch(I){x.push(_(I))}if(G)return{ok:!1,pending:!0};if(x.length>0)return{ok:!1,errors:x};return{ok:!0,values:J}}var Q$="State",b=($)=>{if($==null)throw new L("state");let x=new Set,G=$,J={[Symbol.toStringTag]:Q$,get:()=>{return m(x),G},set:(Q)=>{if(Q==null)throw new L("state");if(f(G,Q))return;if(G=Q,F(x),K===G)x.clear()},update:(Q)=>{if(!Y(Q))throw new P("state update",U(Q));J.set(Q(G))}};return J},k=($)=>E($,Q$);var e="Store",$$=($)=>{if($==null)throw new L("store");let x=new Set,G={add:new Set,change:new Set,remove:new Set,sort:new Set},J=new Map,Q=new Map,M=Array.isArray($),I=b(0),C=()=>{let B={};for(let[R,X]of J)B[R]=X.get();return B},D=(B,R)=>{Object.freeze(R);for(let X of G[B])X(R)},j=()=>Array.from(J.keys()).map((B)=>Number(B)).filter((B)=>Number.isInteger(B)).sort((B,R)=>B-R),O=(B,R)=>{if(R==null)throw new L(`store for key "${B}"`);if(R===K)return!0;if(p(R)||Y(R)||o(R))throw new n(`store for key "${B}"`,U(R));return!0},S=(B,R,X=!1)=>{if(!O(B,R))return!1;let Z=k(R)||h(R)?R:A(R)||Array.isArray(R)?$$(R):b(R);J.set(B,Z);let H=V(()=>y(()=>{D("change",{[B]:Z.get()})},H));if(H(),Q.set(B,H),X)I.set(J.size),F(x),D("add",{[B]:R});return!0},q=(B,R=!1)=>{let X=J.delete(B);if(X){let Z=Q.get(B);if(Z)Z.cleanup();Q.delete(B)}if(R)I.set(J.size),F(x),D("remove",{[B]:K});return X},N=(B,R,X)=>{let Z=R$(B,R);return J$(()=>{if(Object.keys(Z.add).length){for(let H in Z.add){let z=Z.add[H]??K;S(H,z)}if(X)setTimeout(()=>{D("add",Z.add)},0);else D("add",Z.add)}if(Object.keys(Z.change).length){for(let H in Z.change){let z=Z.change[H];if(!O(H,z))continue;let v=J.get(H);if(X$(v))v.set(z);else throw new t(H,U(z))}D("change",Z.change)}if(Object.keys(Z.remove).length){for(let H in Z.remove)q(H);D("remove",Z.remove)}I.set(J.size)}),Z.changed};N({},$,!0);let T={add:M?(B)=>{let R=J.size,X=String(R);S(X,B,!0)}:(B,R)=>{if(!J.has(B))S(B,R,!0);else throw new u(B,U(R))},get:()=>{return m(x),i(C())},remove:M?(B)=>{let R=i(C()),X=J.size;if(!Array.isArray(R)||B<=-X||B>=X)throw new c(B);let Z=[...R];if(Z.splice(B,1),N(R,Z))F(x)}:(B)=>{if(J.has(B))q(B,!0)},set:(B)=>{if(N(C(),B)){if(F(x),K===B)x.clear()}},update:(B)=>{let R=C(),X=B(i(R));if(N(R,X)){if(F(x),K===X)x.clear()}},sort:(B)=>{let R=Array.from(J.entries()).map(([H,z])=>[H,z.get()]).sort(B?(H,z)=>B(H[1],z[1]):(H,z)=>String(H[1]).localeCompare(String(z[1]))),X=R.map(([H])=>String(H)),Z=new Map;R.forEach(([H],z)=>{let v=String(H),C$=M?String(z):String(H),Z$=J.get(v);if(Z$)Z.set(C$,Z$)}),J.clear(),Z.forEach((H,z)=>J.set(z,H)),F(x),D("sort",X)},on:(B,R)=>{return G[B].add(R),()=>G[B].delete(R)},size:I};return new Proxy({},{get(B,R){if(R===Symbol.toStringTag)return e;if(R===Symbol.isConcatSpreadable)return M;if(R===Symbol.iterator)return M?function*(){let X=j();for(let Z of X){let H=J.get(String(Z));if(H)yield H}}:function*(){for(let[X,Z]of J)yield[X,Z]};if(p(R))return;if(R in T)return T[R];if(R==="length"&&M)return m(x),I.get();return J.get(R)},has(B,R){let X=String(R);return X&&J.has(X)||Object.keys(T).includes(X)||R===Symbol.toStringTag||R===Symbol.iterator||R===Symbol.isConcatSpreadable||R==="length"&&M},ownKeys(){return M?j().map((B)=>String(B)).concat(["length"]):Array.from(J.keys()).map((B)=>String(B))},getOwnPropertyDescriptor(B,R){let X=(H)=>({enumerable:!1,configurable:!0,writable:!1,value:H});if(R==="length"&&M)return{enumerable:!0,configurable:!0,writable:!1,value:I.get()};if(R===Symbol.isConcatSpreadable)return X(M);if(R===Symbol.toStringTag)return X(e);if(p(R))return;if(Object.keys(T).includes(R))return X(T[R]);let Z=J.get(R);return Z?{enumerable:!0,configurable:!0,writable:!0,value:Z}:void 0}})},h=($)=>E($,e);var I$=($)=>k($)||o($)||h($),X$=($)=>k($)||h($);function F$($){if(I$($))return $;if(a($))return M$($);if(Array.isArray($)||A($))return $$($);return b($)}export{U as valueString,F$ as toSignal,_ as toError,m as subscribe,z$ as resolve,y as observe,F as notify,D$ as match,p as isSymbol,x$ as isString,h as isStore,k as isState,I$ as isSignal,s as isRecordOrArray,A as isRecord,H$ as isNumber,X$ as isMutableSignal,Y as isFunction,f as isEqual,a as isComputedCallback,o as isComputed,d as isAsyncFunction,g as isAbortError,l as flush,R$ as diff,V as createWatcher,$$ as createStore,b as createState,q$ as createEffect,M$ as createComputed,J$ as batch,K as UNSET,e as TYPE_STORE,Q$ as TYPE_STATE,G$ as TYPE_COMPUTED,t as StoreKeyReadonlyError,c as StoreKeyRangeError,u as StoreKeyExistsError,L as NullishSignalValueError,n as InvalidSignalValueError,P as InvalidCallbackError,W as CircularDependencyError};
|
package/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.
|
|
3
|
+
* @version 0.16.0
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export {
|
|
8
8
|
type Computed,
|
|
9
9
|
type ComputedCallback,
|
|
10
|
-
|
|
10
|
+
createComputed,
|
|
11
11
|
isComputed,
|
|
12
12
|
isComputedCallback,
|
|
13
13
|
TYPE_COMPUTED,
|
|
@@ -16,51 +16,62 @@ export {
|
|
|
16
16
|
type DiffResult,
|
|
17
17
|
diff,
|
|
18
18
|
isEqual,
|
|
19
|
+
type UnknownArray,
|
|
19
20
|
type UnknownRecord,
|
|
20
21
|
type UnknownRecordOrArray,
|
|
21
22
|
} from './src/diff'
|
|
22
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
createEffect,
|
|
25
|
+
type EffectCallback,
|
|
26
|
+
type MaybeCleanup,
|
|
27
|
+
} from './src/effect'
|
|
28
|
+
export {
|
|
29
|
+
CircularDependencyError,
|
|
30
|
+
InvalidCallbackError,
|
|
31
|
+
InvalidSignalValueError,
|
|
32
|
+
NullishSignalValueError,
|
|
33
|
+
StoreKeyExistsError,
|
|
34
|
+
StoreKeyRangeError,
|
|
35
|
+
StoreKeyReadonlyError,
|
|
36
|
+
} from './src/errors'
|
|
23
37
|
export { type MatchHandlers, match } from './src/match'
|
|
24
38
|
export { type ResolveResult, resolve } from './src/resolve'
|
|
25
39
|
export {
|
|
26
|
-
|
|
27
|
-
type Cleanup,
|
|
28
|
-
enqueue,
|
|
29
|
-
flush,
|
|
30
|
-
notify,
|
|
31
|
-
observe,
|
|
32
|
-
subscribe,
|
|
33
|
-
type Updater,
|
|
34
|
-
type Watcher,
|
|
35
|
-
watch,
|
|
36
|
-
} from './src/scheduler'
|
|
37
|
-
export {
|
|
40
|
+
isMutableSignal,
|
|
38
41
|
isSignal,
|
|
39
|
-
type MaybeSignal,
|
|
40
42
|
type Signal,
|
|
41
43
|
type SignalValues,
|
|
42
|
-
toMutableSignal,
|
|
43
44
|
toSignal,
|
|
44
|
-
UNSET,
|
|
45
45
|
type UnknownSignalRecord,
|
|
46
46
|
} from './src/signal'
|
|
47
|
-
export { isState, type State,
|
|
47
|
+
export { createState, isState, type State, TYPE_STATE } from './src/state'
|
|
48
48
|
export {
|
|
49
|
+
createStore,
|
|
49
50
|
isStore,
|
|
50
51
|
type Store,
|
|
51
|
-
type
|
|
52
|
-
type StoreChangeEvent,
|
|
53
|
-
type StoreEventMap,
|
|
54
|
-
type StoreRemoveEvent,
|
|
55
|
-
store,
|
|
52
|
+
type StoreChanges,
|
|
56
53
|
TYPE_STORE,
|
|
57
54
|
} from './src/store'
|
|
58
55
|
export {
|
|
59
|
-
|
|
56
|
+
batch,
|
|
57
|
+
type Cleanup,
|
|
58
|
+
createWatcher,
|
|
59
|
+
flush,
|
|
60
|
+
notify,
|
|
61
|
+
observe,
|
|
62
|
+
subscribe,
|
|
63
|
+
type Watcher,
|
|
64
|
+
} from './src/system'
|
|
65
|
+
export {
|
|
60
66
|
isAbortError,
|
|
61
67
|
isAsyncFunction,
|
|
62
68
|
isFunction,
|
|
63
69
|
isNumber,
|
|
70
|
+
isRecord,
|
|
71
|
+
isRecordOrArray,
|
|
64
72
|
isString,
|
|
73
|
+
isSymbol,
|
|
65
74
|
toError,
|
|
75
|
+
UNSET,
|
|
76
|
+
valueString,
|
|
66
77
|
} from './src/util'
|
package/package.json
CHANGED
package/src/computed.ts
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
import { isEqual } from './diff'
|
|
2
2
|
import {
|
|
3
|
+
CircularDependencyError,
|
|
4
|
+
InvalidCallbackError,
|
|
5
|
+
NullishSignalValueError,
|
|
6
|
+
} from './errors'
|
|
7
|
+
import {
|
|
8
|
+
createWatcher,
|
|
3
9
|
flush,
|
|
4
10
|
notify,
|
|
5
11
|
observe,
|
|
6
12
|
subscribe,
|
|
7
13
|
type Watcher,
|
|
8
|
-
|
|
9
|
-
} from './scheduler'
|
|
10
|
-
import { UNSET } from './signal'
|
|
14
|
+
} from './system'
|
|
11
15
|
import {
|
|
12
|
-
CircularDependencyError,
|
|
13
16
|
isAbortError,
|
|
14
17
|
isAsyncFunction,
|
|
15
18
|
isFunction,
|
|
16
19
|
isObjectOfType,
|
|
17
20
|
toError,
|
|
21
|
+
UNSET,
|
|
22
|
+
valueString,
|
|
18
23
|
} from './util'
|
|
19
24
|
|
|
20
25
|
/* === Types === */
|
|
21
26
|
|
|
22
27
|
type Computed<T extends {}> = {
|
|
23
|
-
[Symbol.toStringTag]: 'Computed'
|
|
28
|
+
readonly [Symbol.toStringTag]: 'Computed'
|
|
24
29
|
get(): T
|
|
25
30
|
}
|
|
26
31
|
type ComputedCallback<T extends {} & { then?: undefined }> =
|
|
27
|
-
| ((abort: AbortSignal) => Promise<T>)
|
|
28
|
-
| (() => T)
|
|
32
|
+
| ((oldValue: T, abort: AbortSignal) => Promise<T>)
|
|
33
|
+
| ((oldValue: T) => T)
|
|
29
34
|
|
|
30
35
|
/* === Constants === */
|
|
31
36
|
|
|
@@ -37,14 +42,21 @@ const TYPE_COMPUTED = 'Computed'
|
|
|
37
42
|
* Create a derived signal from existing signals
|
|
38
43
|
*
|
|
39
44
|
* @since 0.9.0
|
|
40
|
-
* @param {ComputedCallback<T>}
|
|
45
|
+
* @param {ComputedCallback<T>} callback - Computation callback function
|
|
41
46
|
* @returns {Computed<T>} - Computed signal
|
|
42
47
|
*/
|
|
43
|
-
const
|
|
48
|
+
const createComputed = <T extends {}>(
|
|
49
|
+
callback: ComputedCallback<T>,
|
|
50
|
+
initialValue: T = UNSET,
|
|
51
|
+
): Computed<T> => {
|
|
52
|
+
if (!isComputedCallback(callback))
|
|
53
|
+
throw new InvalidCallbackError('computed', valueString(callback))
|
|
54
|
+
if (initialValue == null) throw new NullishSignalValueError('computed')
|
|
55
|
+
|
|
44
56
|
const watchers: Set<Watcher> = new Set()
|
|
45
57
|
|
|
46
58
|
// Internal state
|
|
47
|
-
let value: T =
|
|
59
|
+
let value: T = initialValue
|
|
48
60
|
let error: Error | undefined
|
|
49
61
|
let controller: AbortController | undefined
|
|
50
62
|
let dirty = true
|
|
@@ -75,22 +87,22 @@ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
|
|
|
75
87
|
error = newError
|
|
76
88
|
}
|
|
77
89
|
const settle =
|
|
78
|
-
<T>(
|
|
90
|
+
<T>(fn: (arg: T) => void) =>
|
|
79
91
|
(arg: T) => {
|
|
80
92
|
computing = false
|
|
81
93
|
controller = undefined
|
|
82
|
-
|
|
94
|
+
fn(arg)
|
|
83
95
|
if (changed) notify(watchers)
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
// Own watcher: called when notified from sources (push)
|
|
87
|
-
const
|
|
99
|
+
const watcher = createWatcher(() => {
|
|
88
100
|
dirty = true
|
|
89
101
|
controller?.abort()
|
|
90
102
|
if (watchers.size) notify(watchers)
|
|
91
|
-
else
|
|
103
|
+
else watcher.cleanup()
|
|
92
104
|
})
|
|
93
|
-
|
|
105
|
+
watcher.unwatch(() => {
|
|
94
106
|
controller?.abort()
|
|
95
107
|
})
|
|
96
108
|
|
|
@@ -99,7 +111,7 @@ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
|
|
|
99
111
|
observe(() => {
|
|
100
112
|
if (computing) throw new CircularDependencyError('computed')
|
|
101
113
|
changed = false
|
|
102
|
-
if (isAsyncFunction(
|
|
114
|
+
if (isAsyncFunction(callback)) {
|
|
103
115
|
// Return current value until promise resolves
|
|
104
116
|
if (controller) return value
|
|
105
117
|
controller = new AbortController()
|
|
@@ -108,9 +120,7 @@ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
|
|
|
108
120
|
() => {
|
|
109
121
|
computing = false
|
|
110
122
|
controller = undefined
|
|
111
|
-
|
|
112
|
-
// Retry computation with updated state
|
|
113
|
-
compute()
|
|
123
|
+
compute() // Retry computation with updated state
|
|
114
124
|
},
|
|
115
125
|
{
|
|
116
126
|
once: true,
|
|
@@ -120,7 +130,9 @@ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
|
|
|
120
130
|
let result: T | Promise<T>
|
|
121
131
|
computing = true
|
|
122
132
|
try {
|
|
123
|
-
result = controller
|
|
133
|
+
result = controller
|
|
134
|
+
? callback(value, controller.signal)
|
|
135
|
+
: (callback as (oldValue: T) => T)(value)
|
|
124
136
|
} catch (e) {
|
|
125
137
|
if (isAbortError(e)) nil()
|
|
126
138
|
else err(e)
|
|
@@ -131,16 +143,16 @@ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
|
|
|
131
143
|
else if (null == result || UNSET === result) nil()
|
|
132
144
|
else ok(result)
|
|
133
145
|
computing = false
|
|
134
|
-
},
|
|
146
|
+
}, watcher)
|
|
135
147
|
|
|
136
|
-
|
|
148
|
+
return {
|
|
137
149
|
[Symbol.toStringTag]: TYPE_COMPUTED,
|
|
138
150
|
|
|
139
151
|
/**
|
|
140
152
|
* Get the current value of the computed
|
|
141
153
|
*
|
|
142
154
|
* @since 0.9.0
|
|
143
|
-
* @returns {T} -
|
|
155
|
+
* @returns {T} - Current value of the computed
|
|
144
156
|
*/
|
|
145
157
|
get: (): T => {
|
|
146
158
|
subscribe(watchers)
|
|
@@ -150,15 +162,14 @@ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> => {
|
|
|
150
162
|
return value
|
|
151
163
|
},
|
|
152
164
|
}
|
|
153
|
-
return c
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
/**
|
|
157
|
-
* Check if a value is a computed
|
|
168
|
+
* Check if a value is a computed signal
|
|
158
169
|
*
|
|
159
170
|
* @since 0.9.0
|
|
160
|
-
* @param {unknown} value -
|
|
161
|
-
* @returns {boolean} - true if value is a computed
|
|
171
|
+
* @param {unknown} value - Value to check
|
|
172
|
+
* @returns {boolean} - true if value is a computed signal, false otherwise
|
|
162
173
|
*/
|
|
163
174
|
const isComputed = /*#__PURE__*/ <T extends {}>(
|
|
164
175
|
value: unknown,
|
|
@@ -168,18 +179,18 @@ const isComputed = /*#__PURE__*/ <T extends {}>(
|
|
|
168
179
|
* Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
|
|
169
180
|
*
|
|
170
181
|
* @since 0.12.0
|
|
171
|
-
* @param {unknown} value -
|
|
182
|
+
* @param {unknown} value - Value to check
|
|
172
183
|
* @returns {boolean} - true if value is a callback or callbacks object, false otherwise
|
|
173
184
|
*/
|
|
174
185
|
const isComputedCallback = /*#__PURE__*/ <T extends {}>(
|
|
175
186
|
value: unknown,
|
|
176
|
-
): value is ComputedCallback<T> => isFunction(value) && value.length <
|
|
187
|
+
): value is ComputedCallback<T> => isFunction(value) && value.length < 3
|
|
177
188
|
|
|
178
189
|
/* === Exports === */
|
|
179
190
|
|
|
180
191
|
export {
|
|
181
192
|
TYPE_COMPUTED,
|
|
182
|
-
|
|
193
|
+
createComputed,
|
|
183
194
|
isComputed,
|
|
184
195
|
isComputedCallback,
|
|
185
196
|
type Computed,
|
package/src/diff.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { CircularDependencyError } from './errors'
|
|
2
|
+
import { isRecord, isRecordOrArray, UNSET } from './util'
|
|
3
3
|
|
|
4
4
|
/* === Types === */
|
|
5
5
|
|
|
6
6
|
type UnknownRecord = Record<string, unknown & {}>
|
|
7
|
-
type
|
|
8
|
-
|
|
7
|
+
type UnknownArray = ReadonlyArray<unknown & {}>
|
|
8
|
+
type ArrayToRecord<T extends UnknownArray> = {
|
|
9
|
+
[key: string]: T extends Array<infer U extends {}> ? U : never
|
|
9
10
|
}
|
|
11
|
+
type UnknownRecordOrArray = UnknownRecord | ArrayToRecord<UnknownArray>
|
|
10
12
|
|
|
11
13
|
type DiffResult<T extends UnknownRecordOrArray = UnknownRecord> = {
|
|
12
14
|
changed: boolean
|
|
@@ -69,6 +71,8 @@ const isEqual = <T>(a: T, b: T, visited?: WeakSet<object>): boolean => {
|
|
|
69
71
|
return true
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
// For non-records/non-arrays, they are only equal if they are the same reference
|
|
75
|
+
// (which would have been caught by Object.is at the beginning)
|
|
72
76
|
return false
|
|
73
77
|
} finally {
|
|
74
78
|
visited.delete(a as object)
|
|
@@ -88,61 +92,70 @@ const diff = <T extends UnknownRecordOrArray>(
|
|
|
88
92
|
oldObj: T,
|
|
89
93
|
newObj: T,
|
|
90
94
|
): DiffResult<T> => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (const key of allKeys) {
|
|
106
|
-
const oldHas = key in oldRecord
|
|
107
|
-
const newHas = key in newRecord
|
|
108
|
-
|
|
109
|
-
if (!oldHas && newHas) {
|
|
110
|
-
add[key as keyof T] = newRecord[key] as T[keyof T]
|
|
111
|
-
continue
|
|
112
|
-
} else if (oldHas && !newHas) {
|
|
113
|
-
remove[key as keyof T] = UNSET
|
|
114
|
-
continue
|
|
115
|
-
}
|
|
95
|
+
// Guard against non-objects that can't be diffed properly with Object.keys and 'in' operator
|
|
96
|
+
const oldValid = isRecordOrArray(oldObj)
|
|
97
|
+
const newValid = isRecordOrArray(newObj)
|
|
98
|
+
if (!oldValid || !newValid) {
|
|
99
|
+
// For non-objects or non-plain objects, treat as complete change if different
|
|
100
|
+
const changed = !Object.is(oldObj, newObj)
|
|
101
|
+
return {
|
|
102
|
+
changed,
|
|
103
|
+
add: changed && newValid ? newObj : {},
|
|
104
|
+
change: {},
|
|
105
|
+
remove: changed && oldValid ? oldObj : {},
|
|
106
|
+
}
|
|
107
|
+
}
|
|
116
108
|
|
|
117
|
-
|
|
118
|
-
const newValue = newRecord[key] as T[keyof T]
|
|
109
|
+
const visited = new WeakSet()
|
|
119
110
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
111
|
+
const add: Partial<T> = {}
|
|
112
|
+
const change: Partial<T> = {}
|
|
113
|
+
const remove: Partial<T> = {}
|
|
123
114
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
Object.keys(remove).length > 0
|
|
115
|
+
const oldKeys = Object.keys(oldObj)
|
|
116
|
+
const newKeys = Object.keys(newObj)
|
|
117
|
+
const allKeys = new Set([...oldKeys, ...newKeys])
|
|
128
118
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
119
|
+
for (const key of allKeys) {
|
|
120
|
+
const oldHas = key in oldObj
|
|
121
|
+
const newHas = key in newObj
|
|
122
|
+
|
|
123
|
+
if (!oldHas && newHas) {
|
|
124
|
+
add[key as keyof T] = newObj[key] as T[keyof T]
|
|
125
|
+
continue
|
|
126
|
+
} else if (oldHas && !newHas) {
|
|
127
|
+
remove[key as keyof T] = UNSET
|
|
128
|
+
continue
|
|
134
129
|
}
|
|
130
|
+
|
|
131
|
+
const oldValue = oldObj[key] as T[keyof T]
|
|
132
|
+
const newValue = newObj[key] as T[keyof T]
|
|
133
|
+
|
|
134
|
+
if (!isEqual(oldValue, newValue, visited))
|
|
135
|
+
change[key as keyof T] = newValue
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
const changed =
|
|
139
|
+
Object.keys(add).length > 0 ||
|
|
140
|
+
Object.keys(change).length > 0 ||
|
|
141
|
+
Object.keys(remove).length > 0
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
changed,
|
|
145
|
+
add,
|
|
146
|
+
change,
|
|
147
|
+
remove,
|
|
148
|
+
}
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
/* === Exports === */
|
|
141
152
|
|
|
142
153
|
export {
|
|
154
|
+
type ArrayToRecord,
|
|
143
155
|
type DiffResult,
|
|
144
156
|
diff,
|
|
145
157
|
isEqual,
|
|
146
158
|
type UnknownRecord,
|
|
159
|
+
type UnknownArray,
|
|
147
160
|
type UnknownRecordOrArray,
|
|
148
161
|
}
|
package/src/effect.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
isAbortError,
|
|
5
|
-
isAsyncFunction,
|
|
6
|
-
isFunction,
|
|
7
|
-
} from './util'
|
|
1
|
+
import { CircularDependencyError, InvalidCallbackError } from './errors'
|
|
2
|
+
import { type Cleanup, createWatcher, observe } from './system'
|
|
3
|
+
import { isAbortError, isAsyncFunction, isFunction, valueString } from './util'
|
|
8
4
|
|
|
9
5
|
/* === Types === */
|
|
10
6
|
|
|
@@ -28,12 +24,15 @@ type EffectCallback =
|
|
|
28
24
|
* @param {EffectCallback} callback - Synchronous or asynchronous effect callback
|
|
29
25
|
* @returns {Cleanup} - Cleanup function for the effect
|
|
30
26
|
*/
|
|
31
|
-
const
|
|
32
|
-
|
|
27
|
+
const createEffect = (callback: EffectCallback): Cleanup => {
|
|
28
|
+
if (!isFunction(callback) || callback.length > 1)
|
|
29
|
+
throw new InvalidCallbackError('effect', valueString(callback))
|
|
30
|
+
|
|
31
|
+
const isAsync = isAsyncFunction(callback)
|
|
33
32
|
let running = false
|
|
34
33
|
let controller: AbortController | undefined
|
|
35
34
|
|
|
36
|
-
const
|
|
35
|
+
const watcher = createWatcher(() =>
|
|
37
36
|
observe(() => {
|
|
38
37
|
if (running) throw new CircularDependencyError('effect')
|
|
39
38
|
running = true
|
|
@@ -56,7 +55,7 @@ const effect = (callback: EffectCallback): Cleanup => {
|
|
|
56
55
|
isFunction(cleanup) &&
|
|
57
56
|
controller === currentController
|
|
58
57
|
)
|
|
59
|
-
|
|
58
|
+
watcher.unwatch(cleanup)
|
|
60
59
|
})
|
|
61
60
|
.catch(error => {
|
|
62
61
|
if (!isAbortError(error))
|
|
@@ -64,7 +63,7 @@ const effect = (callback: EffectCallback): Cleanup => {
|
|
|
64
63
|
})
|
|
65
64
|
} else {
|
|
66
65
|
cleanup = (callback as () => MaybeCleanup)()
|
|
67
|
-
if (isFunction(cleanup))
|
|
66
|
+
if (isFunction(cleanup)) watcher.unwatch(cleanup)
|
|
68
67
|
}
|
|
69
68
|
} catch (error) {
|
|
70
69
|
if (!isAbortError(error))
|
|
@@ -72,16 +71,16 @@ const effect = (callback: EffectCallback): Cleanup => {
|
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
running = false
|
|
75
|
-
},
|
|
74
|
+
}, watcher),
|
|
76
75
|
)
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
watcher()
|
|
79
78
|
return () => {
|
|
80
79
|
controller?.abort()
|
|
81
|
-
|
|
80
|
+
watcher.cleanup()
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
/* === Exports === */
|
|
86
85
|
|
|
87
|
-
export { type MaybeCleanup, type EffectCallback,
|
|
86
|
+
export { type MaybeCleanup, type EffectCallback, createEffect }
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class CircularDependencyError extends Error {
|
|
2
|
+
constructor(where: string) {
|
|
3
|
+
super(`Circular dependency detected in ${where}`)
|
|
4
|
+
this.name = 'CircularDependencyError'
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class InvalidCallbackError extends TypeError {
|
|
9
|
+
constructor(where: string, value: string) {
|
|
10
|
+
super(`Invalid ${where} callback ${value}`)
|
|
11
|
+
this.name = 'InvalidCallbackError'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class InvalidSignalValueError extends TypeError {
|
|
16
|
+
constructor(where: string, value: string) {
|
|
17
|
+
super(`Invalid signal value ${value} in ${where}`)
|
|
18
|
+
this.name = 'InvalidSignalValueError'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class NullishSignalValueError extends TypeError {
|
|
23
|
+
constructor(where: string) {
|
|
24
|
+
super(`Nullish signal values are not allowed in ${where}`)
|
|
25
|
+
this.name = 'NullishSignalValueError'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class StoreKeyExistsError extends Error {
|
|
30
|
+
constructor(key: string, value: string) {
|
|
31
|
+
super(
|
|
32
|
+
`Could not add store key "${key}" with value ${value} because it already exists`,
|
|
33
|
+
)
|
|
34
|
+
this.name = 'StoreKeyExistsError'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class StoreKeyRangeError extends RangeError {
|
|
39
|
+
constructor(index: number) {
|
|
40
|
+
super(
|
|
41
|
+
`Could not remove store index ${String(index)} because it is out of range`,
|
|
42
|
+
)
|
|
43
|
+
this.name = 'StoreKeyRangeError'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class StoreKeyReadonlyError extends Error {
|
|
48
|
+
constructor(key: string, value: string) {
|
|
49
|
+
super(
|
|
50
|
+
`Could not set store key "${key}" to ${value} because it is readonly`,
|
|
51
|
+
)
|
|
52
|
+
this.name = 'StoreKeyReadonlyError'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
CircularDependencyError,
|
|
58
|
+
InvalidCallbackError,
|
|
59
|
+
InvalidSignalValueError,
|
|
60
|
+
NullishSignalValueError,
|
|
61
|
+
StoreKeyExistsError,
|
|
62
|
+
StoreKeyRangeError,
|
|
63
|
+
StoreKeyReadonlyError,
|
|
64
|
+
}
|
package/src/match.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { toError } from './util'
|
|
|
5
5
|
/* === Types === */
|
|
6
6
|
|
|
7
7
|
type MatchHandlers<S extends UnknownSignalRecord> = {
|
|
8
|
-
ok
|
|
8
|
+
ok: (values: SignalValues<S>) => void
|
|
9
9
|
err?: (errors: readonly Error[]) => void
|
|
10
10
|
nil?: () => void
|
|
11
11
|
}
|
|
@@ -31,7 +31,7 @@ function match<S extends UnknownSignalRecord>(
|
|
|
31
31
|
try {
|
|
32
32
|
if (result.pending) handlers.nil?.()
|
|
33
33
|
else if (result.errors) handlers.err?.(result.errors)
|
|
34
|
-
else handlers.ok
|
|
34
|
+
else if (result.ok) handlers.ok(result.values)
|
|
35
35
|
} catch (error) {
|
|
36
36
|
// If handler throws, try error handler, otherwise rethrow
|
|
37
37
|
if (
|
package/src/resolve.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { UnknownRecord } from './diff'
|
|
2
|
-
import {
|
|
3
|
-
import { toError } from './util'
|
|
2
|
+
import type { SignalValues, UnknownSignalRecord } from './signal'
|
|
3
|
+
import { toError, UNSET } from './util'
|
|
4
4
|
|
|
5
5
|
/* === Types === */
|
|
6
6
|
|