@zeix/cause-effect 0.12.2 → 0.12.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Cause & Effect
2
2
 
3
- Version 0.12.2
3
+ Version 0.12.3
4
4
 
5
5
  **Cause & Effect** is a lightweight, reactive state management library for JavaScript applications. It uses the concept of signals to create a predictable and efficient data flow in your app.
6
6
 
@@ -54,7 +54,7 @@ npm install @zeix/cause-effect
54
54
  bun add @zeix/cause-effect
55
55
  ```
56
56
 
57
- ## Basic Usage
57
+ ## Usage
58
58
 
59
59
  ### Single State Signal
60
60
 
@@ -214,3 +214,45 @@ This example showcases several powerful features of Cause & Effect:
214
214
  5. **Flexibility and Integration**: Seamlessly integrates with DOM manipulation and event listeners, fitting into any JavaScript application or framework.
215
215
 
216
216
  These principles enable developers to create complex, reactive applications with clear data flow, efficient updates, and robust error handling, while promoting code reuse and modularity.
217
+
218
+ ### Effects and DOM Updates
219
+
220
+ The `enqueue()` function allows you to schedule DOM updates to be executed on the next animation frame. This function returns a `Promise`, which makes it easy to detect when updates are applied or if they fail.
221
+
222
+ ```js
223
+ import { enqueue } from '@zeix/cause-effect'
224
+
225
+ // Schedule a DOM update
226
+ enqueue(() => {
227
+ document.getElementById('myElement').textContent = 'Updated content'
228
+ })
229
+ .then(() => console.log('Update applied successfully'))
230
+ .catch(error => console.error('Update failed:', error))
231
+ ```
232
+
233
+ You can also use the deduplication feature to ensure that only the latest update for a specific element and operation is applied:
234
+
235
+ ```js
236
+ import { enqueue } from '@zeix/cause-effect'
237
+
238
+ // Define a signal and update it in an event handler
239
+ const name = state('')
240
+ document.querySelector('input[name="name"]').addEventListener('input', e => {
241
+ state.set(e.target.value) // Triggers an update on every keystroke
242
+ })
243
+
244
+ // Define an effect to react to signal changes
245
+ effect(text => {
246
+ const nameSpan = document.querySelector('.greeting .name')
247
+ enqueue(() => {
248
+ nameSpan.textContent = text
249
+ return text
250
+ }, [nameSpan, 'setName']) // For deduplication
251
+ .then(result => console.log(`Name was updated to ${result}`))
252
+ .catch(error => console.error('Failed to update name:', error))
253
+ }, name)
254
+ ```
255
+
256
+ In this example, as the user types in the input field only 'Jane' will be applied to the DOM. 'J', 'Ja', 'Jan' were superseded by more recent updates and deduplicated (if typing was fast enough).
257
+
258
+ When multiple `enqueue` calls are made with the same deduplication key before the next animation frame, only the last call will be executed. Previous calls are superseded and their Promises will not be resolved or rejected. This "last-write-wins" behavior ensures that only the most recent update is applied, which is typically desirable for UI updates and state changes.
package/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.12.2
3
+ * @version 0.12.3
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export { type Signal, type MaybeSignal, type InferMaybeSignalType, type ComputedCallbacks, type EffectCallbacks, UNSET, isSignal, toSignal } from './lib/signal';
package/index.js CHANGED
@@ -1 +1 @@
1
- var V=(x)=>typeof x==="function";var R=(x,B)=>Object.prototype.toString.call(x)===`[object ${B}]`,m=(x)=>(B)=>B instanceof x,j=m(Error),g=m(Promise),y=(x)=>j(x)?x:new Error(String(x));var F,D=new Set,w=0,N=new Map,M,p=()=>{M=void 0;for(let x of N.values()){for(let B of x.values())B();x.clear()}},d=()=>{if(M)cancelAnimationFrame(M);M=requestAnimationFrame(p)};queueMicrotask(p);var T=(x)=>{if(F&&!x.includes(F))x.push(F)},C=(x)=>{for(let B of x)w?D.add(B):B()},U=()=>{while(D.size){let x=Array.from(D);D.clear();for(let B of x)B()}},n=(x)=>{w++,x(),U(),w--},O=(x,B)=>{let z=F;F=B,x(),F=z},v=(x,B)=>new Promise((z,$)=>{let L=()=>{try{z(x())}catch(H){$(H)}};if(B){let[H,J]=B;if(!N.has(H))N.set(H,new Map);N.get(H).set(J,L)}d()});function Y(x,...B){let z=!1,$=()=>O(()=>{if(z)throw new Error("Circular dependency in effect detected");z=!0;let L=_(B,x);if(j(L))console.error("Unhandled error in effect:",L);z=!1},$);$()}var o="Computed",i=(x,B)=>{if(!B)return!1;return x.name===B.name&&x.message===B.message},I=(x,...B)=>{let z=[],$=Q,L,H=!0,J=!1,X=!1,K=(G)=>{if(!Object.is(G,$))$=G,H=!1,L=void 0,J=!1},W=()=>{J=Q===$,$=Q,L=void 0},Z=(G)=>{let P=y(G);J=i(P,L),$=Q,L=P},A=()=>{if(H=!0,!J)C(z)},h=()=>O(()=>{if(X)throw new Error("Circular dependency in computed detected");J=!0,X=!0;let G=_(B,x);if(g(G))W(),G.then((P)=>{K(P),C(z)}).catch(Z);else if(G==null||Q===G)W();else if(j(G))Z(G);else K(G);X=!1},A),q={[Symbol.toStringTag]:o,get:()=>{if(T(z),U(),H)h();if(L)throw L;return $},map:(G)=>I(G,q),match:(G)=>{return Y(G,q),q}};return q},S=(x)=>R(x,o);var b="State",E=(x)=>{let B=[],z=x,$={[Symbol.toStringTag]:b,get:()=>{return T(B),z},set:(L)=>{if(Object.is(z,L))return;if(z=L,C(B),Q===z)B.length=0},update:(L)=>{$.set(L(z))},map:(L)=>I(L,$),match:(L)=>{return Y(L,$),$}};return $},k=(x)=>R(x,b);var Q=Symbol(),c=(x)=>V(x)&&!x.length||typeof x==="object"&&x!==null&&("ok"in x)&&V(x.ok),f=(x)=>k(x)||S(x),t=(x)=>f(x)?x:c(x)?I(x):E(x),_=(x,B)=>{let{ok:z,nil:$,err:L}=V(B)?{ok:B}:B,H=[],J=[],X=!1;for(let W=0;W<x.length;W++){let Z=x[W];try{let A=f(Z)?Z.get():V(Z)?Z():Z;if(A===Q)X=!0;H[W]=A}catch(A){J.push(y(A))}}let K=void 0;try{if(X&&$)K=$();else if(J.length)K=L?L(...J):J[0];else if(!X)K=z(...H)}catch(W){if(K=y(W),L)K=L(K)}return K};export{O as watch,t as toSignal,E as state,k as isState,f as isSignal,S as isComputed,v as enqueue,Y as effect,I as computed,n as batch,Q as UNSET};
1
+ var V=(x)=>typeof x==="function";var q=(x,B)=>Object.prototype.toString.call(x)===`[object ${B}]`,m=(x)=>(B)=>B instanceof x,j=m(Error),g=m(Promise),y=(x)=>j(x)?x:new Error(String(x));var F,R=new Set,T=0,w=new Map,N,p=()=>{N=void 0;let x=Array.from(w.values());w.clear();for(let B of x)B()},o=()=>{if(N)cancelAnimationFrame(N);N=requestAnimationFrame(p)};queueMicrotask(p);var D=(x)=>{if(F&&!x.includes(F))x.push(F)},C=(x)=>{for(let B of x)T?R.add(B):B()},S=()=>{while(R.size){let x=Array.from(R);R.clear();for(let B of x)B()}},v=(x)=>{T++;try{x()}finally{S(),T--}},O=(x,B)=>{let z=F;F=B;try{x()}finally{F=z}},n=(x,B)=>new Promise((z,$)=>{let L=()=>{try{z(x())}catch(W){$(W)}};if(B)w.set(B,L);o()});function Y(x,...B){let z=!1,$=()=>O(()=>{if(z)throw new Error("Circular dependency in effect detected");z=!0;let L=_(B,x);if(j(L))console.error("Unhandled error in effect:",L);z=!1},$);$()}var b="Computed",i=(x,B)=>{if(!B)return!1;return x.name===B.name&&x.message===B.message},I=(x,...B)=>{let z=[],$=K,L,W=!0,H=!1,Z=!1,J=(G)=>{if(!Object.is(G,$))$=G,W=!1,L=void 0,H=!1},Q=()=>{H=K===$,$=K,L=void 0},X=(G)=>{let P=y(G);H=i(P,L),$=K,L=P},A=()=>{if(W=!0,!H)C(z)},d=()=>O(()=>{if(Z)throw new Error("Circular dependency in computed detected");H=!0,Z=!0;let G=_(B,x);if(g(G))Q(),G.then((P)=>{J(P),C(z)}).catch(X);else if(G==null||K===G)Q();else if(j(G))X(G);else J(G);Z=!1},A),M={[Symbol.toStringTag]:b,get:()=>{if(D(z),S(),W)d();if(L)throw L;return $},map:(G)=>I(G,M),match:(G)=>{return Y(G,M),M}};return M},U=(x)=>q(x,b);var h="State",k=(x)=>{let B=[],z=x,$={[Symbol.toStringTag]:h,get:()=>{return D(B),z},set:(L)=>{if(Object.is(z,L))return;if(z=L,C(B),K===z)B.length=0},update:(L)=>{$.set(L(z))},map:(L)=>I(L,$),match:(L)=>{return Y(L,$),$}};return $},E=(x)=>q(x,h);var K=Symbol(),c=(x)=>V(x)&&!x.length||typeof x==="object"&&x!==null&&("ok"in x)&&V(x.ok),f=(x)=>E(x)||U(x),t=(x)=>f(x)?x:c(x)?I(x):k(x),_=(x,B)=>{let{ok:z,nil:$,err:L}=V(B)?{ok:B}:B,W=[],H=[],Z=!1;for(let Q=0;Q<x.length;Q++){let X=x[Q];try{let A=f(X)?X.get():V(X)?X():X;if(A===K)Z=!0;W[Q]=A}catch(A){H.push(y(A))}}let J=void 0;try{if(Z&&$)J=$();else if(H.length)J=L?L(...H):H[0];else if(!Z)J=z(...W)}catch(Q){if(J=y(Q),L)J=L(J)}return J};export{O as watch,t as toSignal,k as state,E as isState,f as isSignal,U as isComputed,n as enqueue,Y as effect,I as computed,v as batch,K as UNSET};
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @name Cause & Effect
3
- * @version 0.12.2
3
+ * @version 0.12.3
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export {
@@ -1,6 +1,6 @@
1
1
  export type EnqueueDedupe = [Element, string];
2
2
  export type Watcher = () => void;
3
- export type Updater = <T>() => T;
3
+ export type Updater = <T>() => T | boolean | void;
4
4
  /**
5
5
  * Add active watcher to the array of watchers
6
6
  *
@@ -27,14 +27,13 @@ export declare const batch: (fn: () => void) => void;
27
27
  * Run a function in a reactive context
28
28
  *
29
29
  * @param {() => void} run - function to run the computation or effect
30
- * @param {Watcher} mark - function to be called when the state changes
30
+ * @param {Watcher} mark - function to be called when the state changes or undefined for temporary unwatching while inserting auto-hydrating DOM nodes that might read signals (e.g., web components)
31
31
  */
32
- export declare const watch: (run: () => void, mark: Watcher) => void;
32
+ export declare const watch: (run: () => void, mark?: Watcher) => void;
33
33
  /**
34
34
  * Enqueue a function to be executed on the next animation frame
35
35
  *
36
- * @param callback
37
- * @param dedupe
38
- * @returns
36
+ * @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void
37
+ * @param {EnqueueDedupe} dedupe - [element, operation] pair for deduplication
39
38
  */
40
- export declare const enqueue: <T>(update: Updater, dedupe?: EnqueueDedupe) => Promise<T>;
39
+ export declare const enqueue: <T>(fn: Updater, dedupe?: EnqueueDedupe) => Promise<boolean | void | T>;
package/lib/scheduler.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  export type EnqueueDedupe = [Element, string]
4
4
 
5
5
  export type Watcher = () => void
6
- export type Updater = <T>() => T
6
+ export type Updater = <T>() => T | boolean | void
7
7
 
8
8
  /* === Internal === */
9
9
 
@@ -15,16 +15,15 @@ const pending = new Set<Watcher>()
15
15
  let batchDepth = 0
16
16
 
17
17
  // Map of DOM elements to update functions
18
- const updateMap = new Map<Element, Map<string, () => void>>()
18
+ const updateMap = new Map<EnqueueDedupe, () => void>()
19
19
  let requestId: number | undefined
20
20
 
21
21
  const updateDOM = () => {
22
22
  requestId = undefined
23
- for (const elementMap of updateMap.values()) {
24
- for (const fn of elementMap.values()) {
25
- fn()
26
- }
27
- elementMap.clear()
23
+ const updates = Array.from(updateMap.values())
24
+ updateMap.clear()
25
+ for (const fn of updates) {
26
+ fn()
28
27
  }
29
28
  }
30
29
 
@@ -81,47 +80,49 @@ export const flush = () => {
81
80
  */
82
81
  export const batch = (fn: () => void) => {
83
82
  batchDepth++
84
- fn()
85
- flush()
86
- batchDepth--
83
+ try {
84
+ fn()
85
+ } finally {
86
+ flush()
87
+ batchDepth--
88
+ }
87
89
  }
88
90
 
89
91
  /**
90
92
  * Run a function in a reactive context
91
93
  *
92
94
  * @param {() => void} run - function to run the computation or effect
93
- * @param {Watcher} mark - function to be called when the state changes
95
+ * @param {Watcher} mark - function to be called when the state changes or undefined for temporary unwatching while inserting auto-hydrating DOM nodes that might read signals (e.g., web components)
94
96
  */
95
- export const watch = (run: () => void, mark: Watcher): void => {
97
+ export const watch = (run: () => void, mark?: Watcher): void => {
96
98
  const prev = active
97
99
  active = mark
98
- run()
99
- active = prev
100
+ try {
101
+ run()
102
+ } finally {
103
+ active = prev
104
+ }
100
105
  }
101
106
 
102
107
  /**
103
108
  * Enqueue a function to be executed on the next animation frame
104
109
  *
105
- * @param callback
106
- * @param dedupe
107
- * @returns
110
+ * @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void
111
+ * @param {EnqueueDedupe} dedupe - [element, operation] pair for deduplication
108
112
  */
109
113
  export const enqueue = <T>(
110
- update: Updater,
114
+ fn: Updater,
111
115
  dedupe?: EnqueueDedupe
112
- ) => new Promise<T>((resolve, reject) => {
116
+ ) => new Promise<T | boolean | void>((resolve, reject) => {
113
117
  const wrappedCallback = () => {
114
118
  try {
115
- resolve(update())
119
+ resolve(fn())
116
120
  } catch (error) {
117
121
  reject(error)
118
122
  }
119
123
  }
120
124
  if (dedupe) {
121
- const [el, op] = dedupe
122
- if (!updateMap.has(el)) updateMap.set(el, new Map())
123
- const elementMap = updateMap.get(el)!
124
- elementMap.set(op, wrappedCallback)
125
+ updateMap.set(dedupe, wrappedCallback)
125
126
  }
126
127
  requestTick()
127
128
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeix/cause-effect",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "author": "Esther Brunner",
5
5
  "main": "index.js",
6
6
  "module": "index.ts",