@zeix/cause-effect 0.16.0 → 0.17.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.
Files changed (62) hide show
  1. package/.ai-context.md +71 -21
  2. package/.cursorrules +3 -2
  3. package/.github/copilot-instructions.md +59 -13
  4. package/CLAUDE.md +170 -24
  5. package/LICENSE +1 -1
  6. package/README.md +156 -52
  7. package/archive/benchmark.ts +688 -0
  8. package/archive/collection.ts +312 -0
  9. package/{src → archive}/computed.ts +33 -34
  10. package/archive/list.ts +551 -0
  11. package/archive/memo.ts +138 -0
  12. package/archive/state.ts +89 -0
  13. package/archive/store.ts +368 -0
  14. package/archive/task.ts +194 -0
  15. package/eslint.config.js +1 -0
  16. package/index.dev.js +902 -501
  17. package/index.js +1 -1
  18. package/index.ts +42 -22
  19. package/package.json +1 -1
  20. package/src/classes/collection.ts +272 -0
  21. package/src/classes/composite.ts +176 -0
  22. package/src/classes/computed.ts +333 -0
  23. package/src/classes/list.ts +304 -0
  24. package/src/classes/state.ts +98 -0
  25. package/src/classes/store.ts +210 -0
  26. package/src/diff.ts +28 -52
  27. package/src/effect.ts +9 -9
  28. package/src/errors.ts +50 -25
  29. package/src/signal.ts +58 -41
  30. package/src/system.ts +79 -42
  31. package/src/util.ts +16 -34
  32. package/test/batch.test.ts +15 -17
  33. package/test/benchmark.test.ts +4 -4
  34. package/test/collection.test.ts +796 -0
  35. package/test/computed.test.ts +138 -130
  36. package/test/diff.test.ts +2 -2
  37. package/test/effect.test.ts +36 -35
  38. package/test/list.test.ts +754 -0
  39. package/test/match.test.ts +25 -25
  40. package/test/resolve.test.ts +17 -19
  41. package/test/signal.test.ts +72 -121
  42. package/test/state.test.ts +44 -44
  43. package/test/store.test.ts +344 -1663
  44. package/types/index.d.ts +11 -9
  45. package/types/src/classes/collection.d.ts +32 -0
  46. package/types/src/classes/composite.d.ts +15 -0
  47. package/types/src/classes/computed.d.ts +97 -0
  48. package/types/src/classes/list.d.ts +41 -0
  49. package/types/src/classes/state.d.ts +52 -0
  50. package/types/src/classes/store.d.ts +51 -0
  51. package/types/src/diff.d.ts +8 -12
  52. package/types/src/errors.d.ts +12 -11
  53. package/types/src/signal.d.ts +27 -14
  54. package/types/src/system.d.ts +41 -20
  55. package/types/src/util.d.ts +6 -3
  56. package/src/state.ts +0 -98
  57. package/src/store.ts +0 -525
  58. package/types/src/collection.d.ts +0 -26
  59. package/types/src/computed.d.ts +0 -33
  60. package/types/src/scheduler.d.ts +0 -55
  61. package/types/src/state.d.ts +0 -24
  62. package/types/src/store.d.ts +0 -66
package/index.js CHANGED
@@ -1 +1 @@
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};
1
+ var B=Symbol(),o=($)=>typeof $==="string",s=($)=>typeof $==="number",W=($)=>typeof $==="symbol",q=($)=>typeof $==="function",j=($)=>q($)&&$.constructor.name==="AsyncFunction",Y$=($)=>q($)&&$.constructor.name!=="AsyncFunction",J$=($)=>$!=null&&typeof $==="object",F=($,J)=>Object.prototype.toString.call($)===`[object ${J}]`,Y=($)=>F($,"Object"),c=($)=>Y($)||Array.isArray($),z$=($,J=(z)=>z!=null)=>Array.isArray($)&&$.every(J);var E=($)=>$ instanceof DOMException&&$.name==="AbortError",I=($)=>$ instanceof Error?$:Error(String($)),f=($)=>o($)?`"${$}"`:!!$&&typeof $==="object"?JSON.stringify($):String($);var L=($,J,z)=>{if(Object.is($,J))return!0;if(typeof $!==typeof J)return!1;if(!J$($)||!J$(J))return!1;if(!z)z=new WeakSet;if(z.has($)||z.has(J))throw new P("isEqual");z.add($),z.add(J);try{if(Array.isArray($)&&Array.isArray(J)){if($.length!==J.length)return!1;for(let G=0;G<$.length;G++)if(!L($[G],J[G],z))return!1;return!0}if(Array.isArray($)!==Array.isArray(J))return!1;if(Y($)&&Y(J)){let G=Object.keys($),X=Object.keys(J);if(G.length!==X.length)return!1;for(let Z of G){if(!(Z in J))return!1;if(!L($[Z],J[Z],z))return!1}return!0}return!1}finally{z.delete($),z.delete(J)}},w=($,J)=>{let z=c($),G=c(J);if(!z||!G){let M=!Object.is($,J);return{changed:M,add:M&&G?J:{},change:{},remove:M&&z?$:{}}}let X=new WeakSet,Z={},Q={},A={},V=Object.keys($),u=Object.keys(J),U=new Set([...V,...u]);for(let M of U){let C=M in $,m=M in J;if(!C&&m){Z[M]=J[M];continue}else if(C&&!m){A[M]=B;continue}let P$=$[M],K$=J[M];if(!L(P$,K$,X))Q[M]=K$}return{add:Z,change:Q,remove:A,changed:!!(Object.keys(Z).length||Object.keys(Q).length||Object.keys(A).length)}};var S,g=new Set,i=0,R=($)=>{let J=new Set,z=$;return z.onCleanup=(G)=>{J.add(G)},z.stop=()=>{for(let G of J)G();J.clear()},z},D=($)=>{if(S&&!$.has(S)){let J=S;J.onCleanup(()=>$.delete(J)),$.add(J)}},H=($)=>{for(let J of $)if(i)g.add(J);else J()},p=()=>{while(g.size){let $=Array.from(g);g.clear();for(let J of $)J()}},G$=($)=>{i++;try{$()}finally{p(),i--}},_=($,J)=>{let z=S;S=$||void 0;try{J()}finally{S=z}},x=($,J)=>{for(let z of $)if(i)g.add(()=>z(J));else z(J)};var t="Computed";class b{#J=new Set;#z;#G;#$;#X=!0;#Q=!1;#Z;constructor($,J=B){N("memo",$,l),K("memo",J),this.#z=$,this.#G=J,this.#Z=R(()=>{if(this.#X=!0,this.#J.size)H(this.#J);else this.#Z.stop()})}get[Symbol.toStringTag](){return t}get(){if(D(this.#J),p(),this.#X)_(this.#Z,()=>{if(this.#Q)throw new P("memo");let $;this.#Q=!0;try{$=this.#z(this.#G)}catch(J){this.#G=B,this.#$=I(J),this.#Q=!1;return}if($==null||B===$)this.#G=B,this.#$=void 0;else this.#G=$,this.#$=void 0,this.#X=!1;this.#Q=!1});if(this.#$)throw this.#$;return this.#G}}class y{#J=new Set;#z;#G;#$;#X=!0;#Q=!1;#Z=!1;#H;#B;constructor($,J=B){N("task",$,r),K("task",J),this.#z=$,this.#G=J,this.#H=R(()=>{if(this.#X=!0,this.#B?.abort(),this.#J.size)H(this.#J);else this.#H.stop()}),this.#H.onCleanup(()=>{this.#B?.abort()})}get[Symbol.toStringTag](){return t}get(){D(this.#J),p();let $=(Z)=>{if(!L(Z,this.#G))this.#G=Z,this.#Z=!0;this.#$=void 0,this.#X=!1},J=()=>{this.#Z=B!==this.#G,this.#G=B,this.#$=void 0},z=(Z)=>{let Q=I(Z);this.#Z=!this.#$||Q.name!==this.#$.name||Q.message!==this.#$.message,this.#G=B,this.#$=Q},G=(Z)=>(Q)=>{if(this.#Q=!1,this.#B=void 0,Z(Q),this.#Z)H(this.#J)},X=()=>_(this.#H,()=>{if(this.#Q)throw new P("task");if(this.#Z=!1,this.#B)return this.#G;this.#B=new AbortController,this.#B.signal.addEventListener("abort",()=>{this.#Q=!1,this.#B=void 0,X()},{once:!0});let Z;this.#Q=!0;try{Z=this.#z(this.#G,this.#B.signal)}catch(Q){if(E(Q))J();else z(Q);this.#Q=!1;return}if(Z instanceof Promise)Z.then(G($),G(z));else if(Z==null||B===Z)J();else $(Z);this.#Q=!1});if(this.#X)X();if(this.#$)throw this.#$;return this.#G}}var X$=($,J=B)=>j($)?new y($,J):new b($,J),Z$=($)=>F($,t),l=($)=>Y$($)&&$.length<2,r=($)=>j($)&&$.length<3;class k{signals=new Map;#J;#z;#G=new Map;#$={add:new Set,change:new Set,remove:new Set};#X=!1;constructor($,J,z){this.#J=J,this.#z=z,this.change({add:$,change:{},remove:{},changed:!0},!0)}#Q($){let J=R(()=>{_(J,()=>{if(this.signals.get($)?.get(),!this.#X)x(this.#$.change,[$])})});this.#G.set($,J),J()}add($,J){if(!this.#J($,J))return!1;if(this.signals.set($,this.#z(J)),this.#$.change.size)this.#Q($);if(!this.#X)x(this.#$.add,[$]);return!0}remove($){if(!this.signals.delete($))return!1;let z=this.#G.get($);if(z)z.stop(),this.#G.delete($);if(!this.#X)x(this.#$.remove,[$]);return!0}change($,J){if(this.#X=!0,Object.keys($.add).length){for(let G in $.add)this.add(G,$.add[G]);let z=()=>x(this.#$.add,Object.keys($.add));if(J)setTimeout(z,0);else z()}if(Object.keys($.change).length)G$(()=>{for(let z in $.change){let G=$.change[z];if(!this.#J(z,G))continue;let X=this.signals.get(z);if(I$(`list item "${z}"`,G,X))X.set(G)}}),x(this.#$.change,Object.keys($.change));if(Object.keys($.remove).length){for(let z in $.remove)this.remove(z);x(this.#$.remove,Object.keys($.remove))}return this.#X=!1,$.changed}clear(){let $=Array.from(this.signals.keys());return this.signals.clear(),this.#G.clear(),x(this.#$.remove,$),!0}on($,J){if(this.#$[$].add(J),$==="change"&&!this.#G.size){this.#X=!0;for(let z of this.signals.keys())this.#Q(z);this.#X=!1}return()=>{if(this.#$[$].delete(J),$==="change"&&!this.#$.change.size){if(this.#G.size){for(let z of this.#G.values())z.stop();this.#G.clear()}}}}}var Q$="State";class T{#J=new Set;#z;constructor($){K("state",$),this.#z=$}get[Symbol.toStringTag](){return Q$}get(){return D(this.#J),this.#z}set($){if(K("state",$),L(this.#z,$))return;if(this.#z=$,H(this.#J),B===this.#z)this.#J.clear()}update($){N("state update",$),this.set($(this.#z))}}var a=($)=>F($,Q$);var B$="List";class v{#J;#z=new Set;#G={sort:new Set};#$=[];#X;constructor($,J){K("list",$,Array.isArray);let z=0;this.#X=o(J)?()=>`${J}${z++}`:q(J)?(G)=>J(G):()=>String(z++),this.#J=new k(this.#Q($),(G,X)=>{return K(`list for key "${G}"`,X),!0},(G)=>new T(G))}#Q($){let J={};for(let z=0;z<$.length;z++){let G=$[z];if(G===void 0)continue;let X=this.#$[z];if(!X)X=this.#X(G),this.#$[z]=X;J[X]=G}return J}get#Z(){return this.#$.map(($)=>this.#J.signals.get($)?.get()).filter(($)=>$!==void 0)}get[Symbol.toStringTag](){return B$}get[Symbol.isConcatSpreadable](){return!0}*[Symbol.iterator](){for(let $ of this.#$){let J=this.#J.signals.get($);if(J)yield J}}get length(){return D(this.#z),this.#$.length}get(){return D(this.#z),this.#Z}set($){if(B===$){this.#J.clear(),H(this.#z),this.#z.clear();return}let J=this.#Z,z=w(this.#Q(J),this.#Q($)),G=Object.keys(z.remove);if(this.#J.change(z)){for(let Z of G){let Q=this.#$.indexOf(Z);if(Q!==-1)this.#$.splice(Q,1)}this.#$=this.#$.filter(()=>!0),H(this.#z)}}update($){this.set($(this.get()))}at($){return this.#J.signals.get(this.#$[$])}keys(){return this.#$.values()}byKey($){return this.#J.signals.get($)}keyAt($){return this.#$[$]}indexOfKey($){return this.#$.indexOf($)}add($){let J=this.#X($);if(this.#J.signals.has(J))throw new O("store",J,$);if(!this.#$.includes(J))this.#$.push(J);if(this.#J.add(J,$))H(this.#z);return J}remove($){let J=s($)?this.#$[$]:$;if(this.#J.remove(J)){let G=s($)?$:this.#$.indexOf(J);if(G>=0)this.#$.splice(G,1);this.#$=this.#$.filter(()=>!0),H(this.#z)}}sort($){let z=this.#$.map((G)=>[G,this.#J.signals.get(G)?.get()]).sort(q($)?(G,X)=>$(G[1],X[1]):(G,X)=>String(G[1]).localeCompare(String(X[1]))).map(([G])=>G);if(!L(this.#$,z))this.#$=z,H(this.#z),x(this.#G.sort,this.#$)}splice($,J,...z){let G=this.#$.length,X=$<0?Math.max(0,G+$):Math.min($,G),Z=Math.max(0,Math.min(J??Math.max(0,G-Math.max(0,X)),G-X)),Q={},A={};for(let U=0;U<Z;U++){let M=X+U,C=this.#$[M];if(C){let m=this.#J.signals.get(C);if(m)A[C]=m.get()}}let V=this.#$.slice(0,X);for(let U of z){let M=this.#X(U);V.push(M),Q[M]=U}V.push(...this.#$.slice(X+Z));let u=!!(Object.keys(Q).length||Object.keys(A).length);if(u)this.#J.change({add:Q,change:{},remove:A,changed:u}),this.#$=V.filter(()=>!0),H(this.#z);return Object.values(A)}on($,J){if($==="sort")return this.#G.sort.add(J),()=>this.#G.sort.delete(J);return this.#J.on($,J)}deriveCollection($){return new d(this,$)}}var h=($)=>F($,B$);var H$="Store";class M${#J;#z=new Set;constructor($){K("store",$,Y),this.#J=new k($,(J,z)=>{return K(`store for key "${J}"`,z),!0},(J)=>L$(J))}get#G(){let $={};for(let[J,z]of this.#J.signals.entries())$[J]=z.get();return $}get[Symbol.toStringTag](){return H$}get[Symbol.isConcatSpreadable](){return!1}*[Symbol.iterator](){for(let[$,J]of this.#J.signals.entries())yield[$,J]}get(){return D(this.#z),this.#G}set($){if(B===$){this.#J.clear(),H(this.#z),this.#z.clear();return}let J=this.#G;if(this.#J.change(w(J,$)))H(this.#z)}keys(){return this.#J.signals.keys()}byKey($){return this.#J.signals.get($)}update($){this.set($(this.get()))}add($,J){if(this.#J.signals.has($))throw new O("store",$,J);if(this.#J.add($,J))H(this.#z);return $}remove($){if(this.#J.remove($))H(this.#z)}on($,J){return this.#J.on($,J)}}var e=($)=>{let J=new M$($);return new Proxy(J,{get(z,G){if(G in z){let X=Reflect.get(z,G);return q(X)?X.bind(z):X}if(!W(G))return z.byKey(G)},has(z,G){if(G in z)return!0;return z.byKey(String(G))!==void 0},ownKeys(z){return Array.from(z.keys())},getOwnPropertyDescriptor(z,G){if(G in z)return Reflect.getOwnPropertyDescriptor(z,G);if(W(G))return;let X=z.byKey(String(G));return X?{enumerable:!0,configurable:!0,writable:!0,value:X}:void 0}})},$$=($)=>F($,H$);var U$=($)=>a($)||Z$($)||$$($),q$=($)=>a($)||$$($)||h($);function j$($){if(l($))return new b($);if(r($))return new y($);if(z$($))return new v($);if(Y($))return e($);return new T($)}function L$($){if(z$($))return new v($);if(Y($))return e($);return new T($)}class P extends Error{constructor($){super(`Circular dependency detected in ${$}`);this.name="CircularDependencyError"}}class O extends Error{constructor($,J,z){super(`Could not add ${$} key "${J}"${z?` with value ${f(z)}`:""} because it already exists`);this.name="DuplicateKeyError"}}class n extends TypeError{constructor($,J){super(`Invalid ${$} callback ${f(J)}`);this.name="InvalidCallbackError"}}class x$ extends TypeError{constructor($,J){super(`Invalid signal value ${f(J)} in ${$}`);this.name="InvalidSignalValueError"}}class A$ extends TypeError{constructor($){super(`Nullish signal values are not allowed in ${$}`);this.name="NullishSignalValueError"}}class D$ extends Error{constructor($,J){super(`Could not set ${$} to ${f(J)} because signal is read-only`);this.name="ReadonlySignalError"}}var N=($,J,z=q)=>{if(!z(J))throw new n($,J)},K=($,J,z=()=>!(W(J)&&J!==B)||q(J))=>{if(J==null)throw new A$($);if(!z(J))throw new x$($,J)},I$=($,J,z)=>{if(!q$(z))throw new D$($,J);return!0};var F$="Collection";class d{#J=new Set;#z;#G;#$=new Map;#X=new Map;#Q={add:new Set,change:new Set,remove:new Set,sort:new Set};#Z=[];constructor($,J){if(N("collection",J),q($))$=$();if(!N$($))throw Error("Invalid collection source");this.#z=$,this.#G=J;for(let z=0;z<this.#z.length;z++){let G=this.#z.keyAt(z);if(!G)continue;this.#B(G)}this.#z.on("add",(z)=>{for(let G of z)if(!this.#$.has(G)){this.#B(G);let X=this.#$.get(G);if(X&&R$(this.#G))X.get()}H(this.#J),x(this.#Q.add,z)}),this.#z.on("remove",(z)=>{for(let G of z){if(!this.#$.has(G))continue;this.#$.delete(G);let X=this.#Z.indexOf(G);if(X>=0)this.#Z.splice(X,1);this.#q(G)}this.#Z=this.#Z.filter(()=>!0),H(this.#J),x(this.#Q.remove,z)}),this.#z.on("sort",(z)=>{this.#Z=[...z],H(this.#J),x(this.#Q.sort,z)})}get#H(){return this.#Z.map(($)=>this.#$.get($)?.get()).filter(($)=>$!=null&&$!==B)}#B($){let J=R$(this.#G)?async(G,X)=>{let Z=this.#z.byKey($);if(!Z)return B;let Q=Z.get();if(Q===B)return B;return this.#G(Q,X)}:()=>{let G=this.#z.byKey($);if(!G)return B;let X=G.get();if(X===B)return B;return this.#G(X)},z=X$(J);if(this.#$.set($,z),!this.#Z.includes($))this.#Z.push($);if(this.#Q.change.size)this.#M($);return!0}#M($){let J=R(()=>{_(J,()=>{this.#$.get($)?.get()})});this.#X.set($,J),J()}#q($){let J=this.#X.get($);if(J)J.stop(),this.#X.delete($)}get[Symbol.toStringTag](){return F$}get[Symbol.isConcatSpreadable](){return!0}*[Symbol.iterator](){for(let $ of this.#Z){let J=this.#$.get($);if(J)yield J}}get length(){return D(this.#J),this.#Z.length}get(){return D(this.#J),this.#H}at($){return this.#$.get(this.#Z[$])}keys(){return this.#Z.values()}byKey($){return this.#$.get($)}keyAt($){return this.#Z[$]}indexOfKey($){return this.#Z.indexOf($)}on($,J){if(this.#Q[$].add(J),$==="change"&&!this.#X.size)for(let z of this.#$.keys())this.#M(z);return()=>{if(this.#Q[$].delete(J),$==="change"&&!this.#Q.change.size){if(this.#X.size){for(let z of this.#X.values())z.stop();this.#X.clear()}}}}deriveCollection($){return new d(this,$)}}var _$=($)=>F($,F$),N$=($)=>h($)||_$($),R$=($)=>j($);var T$=($)=>{if(!q($)||$.length>1)throw new n("effect",$);let J=j($),z=!1,G,X=R(()=>_(X,()=>{if(z)throw new P("effect");z=!0,G?.abort(),G=void 0;let Z;try{if(J){G=new AbortController;let Q=G;$(G.signal).then((A)=>{if(q(A)&&G===Q)X.onCleanup(A)}).catch((A)=>{if(!E(A))console.error("Async effect error:",A)})}else if(Z=$(),q(Z))X.onCleanup(Z)}catch(Q){if(!E(Q))console.error("Effect callback error:",Q)}z=!1}));return X(),()=>{G?.abort(),X.stop()}};function C$($,J){try{if($.pending)J.nil?.();else if($.errors)J.err?.($.errors);else if($.ok)J.ok($.values)}catch(z){if(J.err&&(!$.errors||!$.errors.includes(I(z))))J.err($.errors?[...$.errors,I(z)]:[I(z)]);else throw z}}function W$($){let J=[],z=!1,G={};for(let[X,Z]of Object.entries($))try{let Q=Z.get();if(Q===B)z=!0;else G[X]=Q}catch(Q){J.push(I(Q))}if(z)return{ok:!1,pending:!0};if(J.length>0)return{ok:!1,errors:J};return{ok:!0,values:G}}export{f as valueString,_ as trackSignalReads,I as toError,D as subscribeActiveWatcher,W$ as resolve,H as notifyWatchers,C$ as match,r as isTaskCallback,W as isSymbol,o as isString,$$ as isStore,a as isState,U$ as isSignal,c as isRecordOrArray,Y as isRecord,F as isObjectOfType,s as isNumber,q$ as isMutableSignal,l as isMemoCallback,h as isList,q as isFunction,L as isEqual,Z$ as isComputed,_$ as isCollection,j as isAsyncFunction,E as isAbortError,p as flushPendingReactions,x as emitNotification,w as diff,R as createWatcher,e as createStore,j$ as createSignal,T$ as createEffect,X$ as createComputed,G$ as batchSignalWrites,B as UNSET,y as Task,H$ as TYPE_STORE,Q$ as TYPE_STATE,B$ as TYPE_LIST,t as TYPE_COMPUTED,F$ as TYPE_COLLECTION,T as State,D$ as ReadonlySignalError,A$ as NullishSignalValueError,b as Memo,v as List,x$ as InvalidSignalValueError,n as InvalidCallbackError,O as DuplicateKeyError,d as Collection,P as CircularDependencyError,M$ as BaseStore};
package/index.ts CHANGED
@@ -1,24 +1,48 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.16.0
3
+ * @version 0.17.0
4
4
  * @author Esther Brunner
5
5
  */
6
6
 
7
+ export {
8
+ Collection,
9
+ type CollectionCallback,
10
+ isCollection,
11
+ TYPE_COLLECTION,
12
+ } from './src/classes/collection'
7
13
  export {
8
14
  type Computed,
9
- type ComputedCallback,
10
15
  createComputed,
11
16
  isComputed,
12
- isComputedCallback,
17
+ isMemoCallback,
18
+ isTaskCallback,
19
+ Memo,
20
+ type MemoCallback,
21
+ Task,
22
+ type TaskCallback,
13
23
  TYPE_COMPUTED,
14
- } from './src/computed'
24
+ } from './src/classes/computed'
25
+ export {
26
+ type ArrayToRecord,
27
+ isList,
28
+ type KeyConfig,
29
+ List,
30
+ TYPE_LIST,
31
+ } from './src/classes/list'
32
+ export { isState, State, TYPE_STATE } from './src/classes/state'
33
+ export {
34
+ BaseStore,
35
+ createStore,
36
+ isStore,
37
+ type Store,
38
+ TYPE_STORE,
39
+ } from './src/classes/store'
15
40
  export {
16
41
  type DiffResult,
17
42
  diff,
18
43
  isEqual,
19
44
  type UnknownArray,
20
45
  type UnknownRecord,
21
- type UnknownRecordOrArray,
22
46
  } from './src/diff'
23
47
  export {
24
48
  createEffect,
@@ -27,39 +51,34 @@ export {
27
51
  } from './src/effect'
28
52
  export {
29
53
  CircularDependencyError,
54
+ DuplicateKeyError,
30
55
  InvalidCallbackError,
31
56
  InvalidSignalValueError,
32
57
  NullishSignalValueError,
33
- StoreKeyExistsError,
34
- StoreKeyRangeError,
35
- StoreKeyReadonlyError,
58
+ ReadonlySignalError,
36
59
  } from './src/errors'
37
60
  export { type MatchHandlers, match } from './src/match'
38
61
  export { type ResolveResult, resolve } from './src/resolve'
39
62
  export {
63
+ createSignal,
40
64
  isMutableSignal,
41
65
  isSignal,
42
66
  type Signal,
43
67
  type SignalValues,
44
- toSignal,
45
68
  type UnknownSignalRecord,
46
69
  } from './src/signal'
47
- export { createState, isState, type State, TYPE_STATE } from './src/state'
48
- export {
49
- createStore,
50
- isStore,
51
- type Store,
52
- type StoreChanges,
53
- TYPE_STORE,
54
- } from './src/store'
55
70
  export {
56
- batch,
71
+ batchSignalWrites,
57
72
  type Cleanup,
58
73
  createWatcher,
59
- flush,
60
- notify,
61
- observe,
62
- subscribe,
74
+ emitNotification,
75
+ flushPendingReactions,
76
+ type Listener,
77
+ type Listeners,
78
+ type Notifications,
79
+ notifyWatchers,
80
+ subscribeActiveWatcher,
81
+ trackSignalReads,
63
82
  type Watcher,
64
83
  } from './src/system'
65
84
  export {
@@ -67,6 +86,7 @@ export {
67
86
  isAsyncFunction,
68
87
  isFunction,
69
88
  isNumber,
89
+ isObjectOfType,
70
90
  isRecord,
71
91
  isRecordOrArray,
72
92
  isString,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeix/cause-effect",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "author": "Esther Brunner",
5
5
  "main": "index.js",
6
6
  "module": "index.ts",
@@ -0,0 +1,272 @@
1
+ import { validateCallback } from '../errors'
2
+ import {
3
+ type Cleanup,
4
+ createWatcher,
5
+ emitNotification,
6
+ type Listener,
7
+ type Listeners,
8
+ notifyWatchers,
9
+ subscribeActiveWatcher,
10
+ trackSignalReads,
11
+ type Watcher,
12
+ } from '../system'
13
+ import { isAsyncFunction, isFunction, isObjectOfType, UNSET } from '../util'
14
+ import { type Computed, createComputed } from './computed'
15
+ import { isList, type List } from './list'
16
+
17
+ /* === Types === */
18
+
19
+ // biome-ignore lint/suspicious/noExplicitAny: source type of current collection doesn't matter
20
+ type CollectionSource<T extends {}> = List<T> | Collection<T, any>
21
+
22
+ type CollectionCallback<T extends {}, U extends {}> =
23
+ | ((sourceValue: U) => T)
24
+ | ((sourceValue: U, abort: AbortSignal) => Promise<T>)
25
+
26
+ /* === Constants === */
27
+
28
+ const TYPE_COLLECTION = 'Collection' as const
29
+
30
+ /* === Class === */
31
+
32
+ class Collection<T extends {}, U extends {}> {
33
+ #watchers = new Set<Watcher>()
34
+ #source: CollectionSource<U>
35
+ #callback: CollectionCallback<T, U>
36
+ #signals = new Map<string, Computed<T>>()
37
+ #ownWatchers = new Map<string, Watcher>()
38
+ #listeners: Listeners = {
39
+ add: new Set<Listener<'add'>>(),
40
+ change: new Set<Listener<'change'>>(),
41
+ remove: new Set<Listener<'remove'>>(),
42
+ sort: new Set<Listener<'sort'>>(),
43
+ }
44
+ #order: string[] = []
45
+
46
+ constructor(
47
+ source: CollectionSource<U> | (() => CollectionSource<U>),
48
+ callback: CollectionCallback<T, U>,
49
+ ) {
50
+ validateCallback('collection', callback)
51
+
52
+ if (isFunction(source)) source = source()
53
+ if (!isCollectionSource(source))
54
+ throw new Error('Invalid collection source')
55
+ this.#source = source
56
+
57
+ this.#callback = callback
58
+
59
+ for (let i = 0; i < this.#source.length; i++) {
60
+ const key = this.#source.keyAt(i)
61
+ if (!key) continue
62
+
63
+ this.#add(key)
64
+ }
65
+
66
+ this.#source.on('add', additions => {
67
+ for (const key of additions) {
68
+ if (!this.#signals.has(key)) {
69
+ this.#add(key)
70
+ // For async computations, trigger initial computation
71
+ const signal = this.#signals.get(key)
72
+ if (signal && isAsyncCollectionCallback(this.#callback))
73
+ signal.get()
74
+ }
75
+ }
76
+ notifyWatchers(this.#watchers)
77
+ emitNotification(this.#listeners.add, additions)
78
+ })
79
+
80
+ this.#source.on('remove', removals => {
81
+ for (const key of removals) {
82
+ if (!this.#signals.has(key)) continue
83
+
84
+ this.#signals.delete(key)
85
+ const index = this.#order.indexOf(key)
86
+ if (index >= 0) this.#order.splice(index, 1)
87
+ this.#removeWatcher(key)
88
+ }
89
+ this.#order = this.#order.filter(() => true) // Compact array
90
+ notifyWatchers(this.#watchers)
91
+ emitNotification(this.#listeners.remove, removals)
92
+ })
93
+
94
+ this.#source.on('sort', newOrder => {
95
+ this.#order = [...newOrder]
96
+ notifyWatchers(this.#watchers)
97
+ emitNotification(this.#listeners.sort, newOrder)
98
+ })
99
+ }
100
+
101
+ get #value(): T[] {
102
+ return this.#order
103
+ .map(key => this.#signals.get(key)?.get())
104
+ .filter(v => v != null && v !== UNSET) as T[]
105
+ }
106
+
107
+ #add(key: string): boolean {
108
+ const computedCallback = isAsyncCollectionCallback<T>(this.#callback)
109
+ ? async (_: T, abort: AbortSignal) => {
110
+ const sourceSignal = this.#source.byKey(key)
111
+ if (!sourceSignal) return UNSET
112
+
113
+ const sourceValue = sourceSignal.get() as U
114
+ if (sourceValue === UNSET) return UNSET
115
+ return this.#callback(sourceValue, abort)
116
+ }
117
+ : () => {
118
+ const sourceSignal = this.#source.byKey(key)
119
+ if (!sourceSignal) return UNSET
120
+
121
+ const sourceValue = sourceSignal.get() as U
122
+ if (sourceValue === UNSET) return UNSET
123
+ return (this.#callback as (sourceValue: U) => T)(
124
+ sourceValue,
125
+ )
126
+ }
127
+
128
+ const signal = createComputed(computedCallback)
129
+
130
+ this.#signals.set(key, signal)
131
+ if (!this.#order.includes(key)) this.#order.push(key)
132
+ if (this.#listeners.change.size) this.#addWatcher(key)
133
+ return true
134
+ }
135
+
136
+ #addWatcher(key: string): void {
137
+ const watcher = createWatcher(() => {
138
+ trackSignalReads(watcher, () => {
139
+ this.#signals.get(key)?.get() // Subscribe to the signal
140
+ })
141
+ })
142
+ this.#ownWatchers.set(key, watcher)
143
+ watcher()
144
+ }
145
+
146
+ #removeWatcher(key: string): void {
147
+ const watcher = this.#ownWatchers.get(key)
148
+ if (watcher) {
149
+ watcher.stop()
150
+ this.#ownWatchers.delete(key)
151
+ }
152
+ }
153
+
154
+ get [Symbol.toStringTag](): 'Collection' {
155
+ return TYPE_COLLECTION
156
+ }
157
+
158
+ get [Symbol.isConcatSpreadable](): boolean {
159
+ return true
160
+ }
161
+
162
+ *[Symbol.iterator](): IterableIterator<Computed<T>> {
163
+ for (const key of this.#order) {
164
+ const signal = this.#signals.get(key)
165
+ if (signal) yield signal as Computed<T>
166
+ }
167
+ }
168
+
169
+ get length(): number {
170
+ subscribeActiveWatcher(this.#watchers)
171
+ return this.#order.length
172
+ }
173
+
174
+ get(): T[] {
175
+ subscribeActiveWatcher(this.#watchers)
176
+ return this.#value
177
+ }
178
+
179
+ at(index: number): Computed<T> | undefined {
180
+ return this.#signals.get(this.#order[index])
181
+ }
182
+
183
+ keys(): IterableIterator<string> {
184
+ return this.#order.values()
185
+ }
186
+
187
+ byKey(key: string): Computed<T> | undefined {
188
+ return this.#signals.get(key)
189
+ }
190
+
191
+ keyAt(index: number): string | undefined {
192
+ return this.#order[index]
193
+ }
194
+
195
+ indexOfKey(key: string): number {
196
+ return this.#order.indexOf(key)
197
+ }
198
+
199
+ on<K extends keyof Listeners>(type: K, listener: Listener<K>): Cleanup {
200
+ this.#listeners[type].add(listener)
201
+ if (type === 'change' && !this.#ownWatchers.size) {
202
+ for (const key of this.#signals.keys()) this.#addWatcher(key)
203
+ }
204
+
205
+ return () => {
206
+ this.#listeners[type].delete(listener)
207
+ if (type === 'change' && !this.#listeners.change.size) {
208
+ if (this.#ownWatchers.size) {
209
+ for (const watcher of this.#ownWatchers.values())
210
+ watcher.stop()
211
+ this.#ownWatchers.clear()
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ deriveCollection<R extends {}>(
218
+ callback: (sourceValue: T) => R,
219
+ ): Collection<R, T>
220
+ deriveCollection<R extends {}>(
221
+ callback: (sourceValue: T, abort: AbortSignal) => Promise<R>,
222
+ ): Collection<R, T>
223
+ deriveCollection<R extends {}>(
224
+ callback: CollectionCallback<R, T>,
225
+ ): Collection<R, T> {
226
+ return new Collection(this, callback)
227
+ }
228
+ }
229
+
230
+ /* === Functions === */
231
+
232
+ /**
233
+ * Check if a value is a collection signal
234
+ *
235
+ * @since 0.17.0
236
+ * @param {unknown} value - Value to check
237
+ * @returns {boolean} - True if value is a collection signal, false otherwise
238
+ */
239
+ const isCollection = /*#__PURE__*/ <T extends {}, U extends {}>(
240
+ value: unknown,
241
+ ): value is Collection<T, U> => isObjectOfType(value, TYPE_COLLECTION)
242
+
243
+ /**
244
+ * Check if a value is a collection source
245
+ *
246
+ * @since 0.17.0
247
+ * @param {unknown} value - Value to check
248
+ * @returns {boolean} - True if value is a collection source, false otherwise
249
+ */
250
+ const isCollectionSource = /*#__PURE__*/ <T extends {}>(
251
+ value: unknown,
252
+ ): value is CollectionSource<T> => isList(value) || isCollection(value)
253
+
254
+ /**
255
+ * Check if the provided callback is an async function
256
+ *
257
+ * @since 0.17.0
258
+ * @param {unknown} callback - Value to check
259
+ * @returns {boolean} - True if value is an async collection callback, false otherwise
260
+ */
261
+ const isAsyncCollectionCallback = <T extends {}>(
262
+ callback: unknown,
263
+ ): callback is (sourceValue: unknown, abort: AbortSignal) => Promise<T> =>
264
+ isAsyncFunction(callback)
265
+
266
+ export {
267
+ Collection,
268
+ type CollectionSource,
269
+ type CollectionCallback,
270
+ isCollection,
271
+ TYPE_COLLECTION,
272
+ }
@@ -0,0 +1,176 @@
1
+ import type { DiffResult, UnknownRecord } from '../diff'
2
+ import { guardMutableSignal } from '../errors'
3
+ import type { Signal } from '../signal'
4
+ import {
5
+ batchSignalWrites,
6
+ type Cleanup,
7
+ createWatcher,
8
+ emitNotification,
9
+ type Listener,
10
+ type Listeners,
11
+ trackSignalReads,
12
+ type Watcher,
13
+ } from '../system'
14
+
15
+ /* === Types === */
16
+
17
+ type CompositeListeners = Pick<Listeners, 'add' | 'change' | 'remove'>
18
+
19
+ /* === Class Definitions === */
20
+
21
+ class Composite<T extends UnknownRecord, S extends Signal<T[keyof T] & {}>> {
22
+ signals = new Map<string, S>()
23
+ #validate: <K extends keyof T & string>(
24
+ key: K,
25
+ value: unknown,
26
+ ) => value is T[K] & {}
27
+ #create: <V extends T[keyof T] & {}>(value: V) => S
28
+ #watchers = new Map<string, Watcher>()
29
+ #listeners: CompositeListeners = {
30
+ add: new Set<Listener<'add'>>(),
31
+ change: new Set<Listener<'change'>>(),
32
+ remove: new Set<Listener<'remove'>>(),
33
+ }
34
+ #batching = false
35
+
36
+ constructor(
37
+ values: T,
38
+ validate: <K extends keyof T & string>(
39
+ key: K,
40
+ value: unknown,
41
+ ) => value is T[K] & {},
42
+ create: <V extends T[keyof T] & {}>(value: V) => S,
43
+ ) {
44
+ this.#validate = validate
45
+ this.#create = create
46
+ this.change(
47
+ {
48
+ add: values,
49
+ change: {},
50
+ remove: {},
51
+ changed: true,
52
+ },
53
+ true,
54
+ )
55
+ }
56
+
57
+ #addWatcher(key: string): void {
58
+ const watcher = createWatcher(() => {
59
+ trackSignalReads(watcher, () => {
60
+ this.signals.get(key)?.get() // Subscribe to the signal
61
+ if (!this.#batching)
62
+ emitNotification(this.#listeners.change, [key])
63
+ })
64
+ })
65
+ this.#watchers.set(key, watcher)
66
+ watcher()
67
+ }
68
+
69
+ add<K extends keyof T & string>(key: K, value: T[K]): boolean {
70
+ if (!this.#validate(key, value)) return false
71
+
72
+ this.signals.set(key, this.#create(value))
73
+ if (this.#listeners.change.size) this.#addWatcher(key)
74
+
75
+ if (!this.#batching) emitNotification(this.#listeners.add, [key])
76
+ return true
77
+ }
78
+
79
+ remove<K extends keyof T & string>(key: K): boolean {
80
+ const ok = this.signals.delete(key)
81
+ if (!ok) return false
82
+
83
+ const watcher = this.#watchers.get(key)
84
+ if (watcher) {
85
+ watcher.stop()
86
+ this.#watchers.delete(key)
87
+ }
88
+
89
+ if (!this.#batching) emitNotification(this.#listeners.remove, [key])
90
+ return true
91
+ }
92
+
93
+ change(changes: DiffResult, initialRun?: boolean): boolean {
94
+ this.#batching = true
95
+
96
+ // Additions
97
+ if (Object.keys(changes.add).length) {
98
+ for (const key in changes.add)
99
+ this.add(
100
+ key as Extract<keyof T, string>,
101
+ changes.add[key] as T[Extract<keyof T, string>] & {},
102
+ )
103
+
104
+ // Queue initial additions event to allow listeners to be added first
105
+ const notify = () =>
106
+ emitNotification(this.#listeners.add, Object.keys(changes.add))
107
+ if (initialRun) setTimeout(notify, 0)
108
+ else notify()
109
+ }
110
+
111
+ // Changes
112
+ if (Object.keys(changes.change).length) {
113
+ batchSignalWrites(() => {
114
+ for (const key in changes.change) {
115
+ const value = changes.change[key]
116
+ if (!this.#validate(key as keyof T & string, value))
117
+ continue
118
+
119
+ const signal = this.signals.get(key)
120
+ if (guardMutableSignal(`list item "${key}"`, value, signal))
121
+ signal.set(value)
122
+ }
123
+ })
124
+ emitNotification(
125
+ this.#listeners.change,
126
+ Object.keys(changes.change),
127
+ )
128
+ }
129
+
130
+ // Removals
131
+ if (Object.keys(changes.remove).length) {
132
+ for (const key in changes.remove)
133
+ this.remove(key as keyof T & string)
134
+ emitNotification(
135
+ this.#listeners.remove,
136
+ Object.keys(changes.remove),
137
+ )
138
+ }
139
+
140
+ this.#batching = false
141
+ return changes.changed
142
+ }
143
+
144
+ clear(): boolean {
145
+ const keys = Array.from(this.signals.keys())
146
+ this.signals.clear()
147
+ this.#watchers.clear()
148
+ emitNotification(this.#listeners.remove, keys)
149
+ return true
150
+ }
151
+
152
+ on<K extends keyof CompositeListeners>(
153
+ type: K,
154
+ listener: Listener<K>,
155
+ ): Cleanup {
156
+ this.#listeners[type].add(listener)
157
+ if (type === 'change' && !this.#watchers.size) {
158
+ this.#batching = true
159
+ for (const key of this.signals.keys()) this.#addWatcher(key)
160
+ this.#batching = false
161
+ }
162
+
163
+ return () => {
164
+ this.#listeners[type].delete(listener)
165
+ if (type === 'change' && !this.#listeners.change.size) {
166
+ if (this.#watchers.size) {
167
+ for (const watcher of this.#watchers.values())
168
+ watcher.stop()
169
+ this.#watchers.clear()
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ export { Composite, type CompositeListeners }