@zeix/cause-effect 0.12.1 → 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.1
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.1
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=()=>O(()=>{let $=_(B,x);if(j($))console.error("Unhandled error in effect:",$)},z);z()}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 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.1
3
+ * @version 0.12.3
4
4
  * @author Esther Brunner
5
5
  */
6
6
  export {
package/lib/computed.ts CHANGED
@@ -79,7 +79,7 @@ export const computed = <T extends {}, U extends MaybeSignal<{}>[]>(
79
79
 
80
80
  // Called when requested by dependencies (pull)
81
81
  const compute = () => watch(() => {
82
- if (computing) throw new Error('Circular dependency detected')
82
+ if (computing) throw new Error('Circular dependency in computed detected')
83
83
  unchanged = true
84
84
  computing = true
85
85
  const result = resolve(maybeSignals, cb)
package/lib/effect.ts CHANGED
@@ -16,10 +16,14 @@ export function effect<U extends MaybeSignal<{}>[]>(
16
16
  cb: EffectCallbacks<U>,
17
17
  ...maybeSignals: U
18
18
  ): void {
19
+ let running = false
19
20
  const run = () => watch(() => {
21
+ if (running) throw new Error('Circular dependency in effect detected')
22
+ running = true
20
23
  const result = resolve(maybeSignals, cb)
21
24
  if (isError(result))
22
25
  console.error('Unhandled error in effect:', result)
26
+ running = false
23
27
  }, run)
24
28
  run()
25
29
  }
@@ -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.1",
3
+ "version": "0.12.3",
4
4
  "author": "Esther Brunner",
5
5
  "main": "index.js",
6
6
  "module": "index.ts",
@@ -216,36 +216,45 @@ describe('Computed', function () {
216
216
  const c = computed(() => b.get() + a.get());
217
217
  expect(() => {
218
218
  b.get(); // This should trigger the circular dependency
219
- }).toThrow('Circular dependency detected');
219
+ }).toThrow('Circular dependency in computed detected');
220
220
  expect(a.get()).toBe(1);
221
221
  });
222
222
 
223
- /* test('should propagate error if an error occurred', function() {
224
- let count = 0;
223
+ test('should propagate error if an error occurred', function() {
224
+ let okCount = 0;
225
+ let errCount = 0;
225
226
  const x = state(0);
226
- const a = computed(() => {
227
- if (x.get() === 1) throw new Error('Calculation error');
227
+ const a = x.map(v => {
228
+ if (v === 1) throw new Error('Calculation error');
228
229
  return 1;
229
230
  });
230
- const b = a.map(v => v ? 'success' : 'pending');
231
- const c = computed(() => {
232
- count++;
233
- return `c: ${b.get()}`;
231
+ const b = a.map({
232
+ ok: v => v ? 'success' : 'failure',
233
+ err: _e => {
234
+ errCount++;
235
+ // console.error(e);
236
+ return `recovered`;
237
+ }
238
+ });
239
+ const c = b.map(v => {
240
+ okCount++;
241
+ return `c: ${v}`;
234
242
  });
235
243
  expect(a.get()).toBe(1);
236
244
  expect(c.get()).toBe('c: success');
237
- expect(count).toBe(1);
238
- x.set(1)
245
+ expect(okCount).toBe(1);
239
246
  try {
247
+ x.set(1)
240
248
  expect(a.get()).toBe(1);
241
249
  expect(true).toBe(false); // This line should not be reached
242
250
  } catch (error) {
243
251
  expect(error.message).toBe('Calculation error');
244
252
  } finally {
245
- expect(c.get()).toBe('c: success');
246
- expect(count).toBe(2);
253
+ expect(c.get()).toBe('c: recovered');
254
+ expect(okCount).toBe(2);
255
+ expect(errCount).toBe(1);
247
256
  }
248
- }); */
257
+ });
249
258
 
250
259
  test('should return a computed signal with .map()', function() {
251
260
  const cause = state(42);
@@ -154,4 +154,28 @@ describe('Effect', function () {
154
154
  console.error = originalConsoleError;
155
155
  }
156
156
  });
157
+
158
+ test('should detect and throw error for circular dependencies in effects', () => {
159
+ let okCount = 0
160
+ let errCount = 0
161
+ const count = state(0)
162
+
163
+ effect({
164
+ ok: () => {
165
+ okCount++
166
+ // This effect updates the signal it depends on, creating a circular dependency
167
+ count.update(v => ++v)
168
+ },
169
+ err: e => {
170
+ errCount++
171
+ expect(e).toBeInstanceOf(Error)
172
+ expect(e.message).toBe('Circular dependency in effect detected')
173
+ }
174
+ }, count)
175
+
176
+ // Verify that the count was changed only once due to the circular dependency error
177
+ expect(count.get()).toBe(1)
178
+ expect(okCount).toBe(1)
179
+ expect(errCount).toBe(1)
180
+ })
157
181
  });