@zeix/cause-effect 0.14.0 → 0.14.1

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/src/effect.ts CHANGED
@@ -1,21 +1,12 @@
1
- import { type Signal, UNSET } from './signal'
2
- import {
3
- CircularDependencyError,
4
- isFunction,
5
- toError,
6
- isAbortError,
7
- } from './util'
8
- import { watch, type Cleanup, type Watcher } from './scheduler'
1
+ import { type Signal, type SignalValues, UNSET } from './signal'
2
+ import { CircularDependencyError, isFunction, toError } from './util'
3
+ import { type Cleanup, watch, observe } from './scheduler'
9
4
 
10
5
  /* === Types === */
11
6
 
12
7
  type EffectMatcher<S extends Signal<{}>[]> = {
13
8
  signals: S
14
- ok: (
15
- ...values: {
16
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never
17
- }
18
- ) => void | Cleanup
9
+ ok: (...values: SignalValues<S>) => void | Cleanup
19
10
  err?: (...errors: Error[]) => void | Cleanup
20
11
  nil?: () => void | Cleanup
21
12
  }
@@ -40,54 +31,46 @@ function effect<S extends Signal<{}>[]>(
40
31
  } = isFunction(matcher)
41
32
  ? { signals: [] as unknown as S, ok: matcher }
42
33
  : matcher
34
+
43
35
  let running = false
44
- const run = (() =>
45
- watch(() => {
36
+ const run = watch(() =>
37
+ observe(() => {
46
38
  if (running) throw new CircularDependencyError('effect')
47
39
  running = true
48
- let cleanup: void | Cleanup = undefined
49
- try {
50
- const errors: Error[] = []
51
- let suspense = false
52
- const values = signals.map(signal => {
53
- try {
54
- const value = signal.get()
55
- if (value === UNSET) suspense = true
56
- return value
57
- } catch (e) {
58
- if (isAbortError(e)) throw e
59
- errors.push(toError(e))
60
- return UNSET
61
- }
62
- }) as {
63
- [K in keyof S]: S[K] extends Signal<infer T extends {}>
64
- ? T
65
- : never
66
- }
67
40
 
41
+ // Pure part
42
+ const errors: Error[] = []
43
+ let pending = false
44
+ const values = signals.map(signal => {
68
45
  try {
69
- cleanup = suspense
70
- ? nil()
71
- : errors.length
72
- ? err(...errors)
73
- : ok(...values)
46
+ const value = signal.get()
47
+ if (value === UNSET) pending = true
48
+ return value
74
49
  } catch (e) {
75
- if (isAbortError(e)) throw e
76
- const error = toError(e)
77
- cleanup = err(error)
50
+ errors.push(toError(e))
51
+ return UNSET
78
52
  }
53
+ }) as SignalValues<S>
54
+
55
+ // Effectful part
56
+ let cleanup: void | Cleanup = undefined
57
+ try {
58
+ cleanup = pending
59
+ ? nil()
60
+ : errors.length
61
+ ? err(...errors)
62
+ : ok(...values)
79
63
  } catch (e) {
80
- err(toError(e))
64
+ cleanup = err(toError(e))
65
+ } finally {
66
+ if (isFunction(cleanup)) run.off(cleanup)
81
67
  }
82
- if (isFunction(cleanup)) run.cleanups.add(cleanup)
68
+
83
69
  running = false
84
- }, run)) as Watcher
85
- run.cleanups = new Set<Cleanup>()
70
+ }, run),
71
+ )
86
72
  run()
87
- return () => {
88
- run.cleanups.forEach(fn => fn())
89
- run.cleanups.clear()
90
- }
73
+ return () => run.cleanup()
91
74
  }
92
75
 
93
76
  /* === Exports === */
@@ -1,9 +1,18 @@
1
1
  type Cleanup = () => void;
2
2
  type Watcher = {
3
3
  (): void;
4
- cleanups: Set<Cleanup>;
4
+ off(cleanup: Cleanup): void;
5
+ cleanup(): void;
5
6
  };
6
7
  type Updater = <T>() => T | boolean | void;
8
+ /**
9
+ * Create a watcher that can be used to observe changes to a signal
10
+ *
11
+ * @since 0.14.1
12
+ * @param {() => void} notice - function to be called when the state changes
13
+ * @returns {Watcher} - watcher object with off and cleanup methods
14
+ */
15
+ declare const watch: (notice: () => void) => Watcher;
7
16
  /**
8
17
  * Add active watcher to the Set of watchers
9
18
  *
@@ -30,9 +39,9 @@ declare const batch: (fn: () => void) => void;
30
39
  * Run a function in a reactive context
31
40
  *
32
41
  * @param {() => void} run - function to run the computation or effect
33
- * @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)
42
+ * @param {Watcher} watcher - 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)
34
43
  */
35
- declare const watch: (run: () => void, mark?: Watcher) => void;
44
+ declare const observe: (run: () => void, watcher?: Watcher) => void;
36
45
  /**
37
46
  * Enqueue a function to be executed on the next animation frame
38
47
  *
@@ -43,4 +52,4 @@ declare const watch: (run: () => void, mark?: Watcher) => void;
43
52
  * @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed
44
53
  */
45
54
  declare const enqueue: <T>(fn: Updater, dedupe?: symbol) => Promise<boolean | void | T>;
46
- export { type Cleanup, type Watcher, type Updater, subscribe, notify, flush, batch, watch, enqueue, };
55
+ export { type Cleanup, type Watcher, type Updater, subscribe, notify, flush, batch, watch, observe, enqueue, };
package/src/scheduler.ts CHANGED
@@ -4,7 +4,8 @@ type Cleanup = () => void
4
4
 
5
5
  type Watcher = {
6
6
  (): void
7
- cleanups: Set<Cleanup>
7
+ off(cleanup: Cleanup): void
8
+ cleanup(): void
8
9
  }
9
10
 
10
11
  type Updater = <T>() => T | boolean | void
@@ -26,8 +27,8 @@ const updateDOM = () => {
26
27
  requestId = undefined
27
28
  const updates = Array.from(updateMap.values())
28
29
  updateMap.clear()
29
- for (const fn of updates) {
30
- fn()
30
+ for (const update of updates) {
31
+ update()
31
32
  }
32
33
  }
33
34
 
@@ -41,17 +42,38 @@ queueMicrotask(updateDOM)
41
42
 
42
43
  /* === Functions === */
43
44
 
45
+ /**
46
+ * Create a watcher that can be used to observe changes to a signal
47
+ *
48
+ * @since 0.14.1
49
+ * @param {() => void} notice - function to be called when the state changes
50
+ * @returns {Watcher} - watcher object with off and cleanup methods
51
+ */
52
+ const watch = (notice: () => void): Watcher => {
53
+ const cleanups = new Set<Cleanup>()
54
+ const w = notice as Partial<Watcher>
55
+ w.off = (on: Cleanup) => {
56
+ cleanups.add(on)
57
+ }
58
+ w.cleanup = () => {
59
+ for (const cleanup of cleanups) {
60
+ cleanup()
61
+ }
62
+ cleanups.clear()
63
+ }
64
+ return w as Watcher
65
+ }
66
+
44
67
  /**
45
68
  * Add active watcher to the Set of watchers
46
69
  *
47
70
  * @param {Set<Watcher>} watchers - watchers of the signal
48
71
  */
49
72
  const subscribe = (watchers: Set<Watcher>) => {
50
- // if (!active) console.warn('Calling .get() outside of a reactive context')
51
73
  if (active && !watchers.has(active)) {
52
74
  const watcher = active
53
75
  watchers.add(watcher)
54
- active.cleanups.add(() => {
76
+ active.off(() => {
55
77
  watchers.delete(watcher)
56
78
  })
57
79
  }
@@ -63,9 +85,9 @@ const subscribe = (watchers: Set<Watcher>) => {
63
85
  * @param {Set<Watcher>} watchers - watchers of the signal
64
86
  */
65
87
  const notify = (watchers: Set<Watcher>) => {
66
- for (const mark of watchers) {
67
- if (batchDepth) pending.add(mark)
68
- else mark()
88
+ for (const watcher of watchers) {
89
+ if (batchDepth) pending.add(watcher)
90
+ else watcher()
69
91
  }
70
92
  }
71
93
 
@@ -76,8 +98,8 @@ const flush = () => {
76
98
  while (pending.size) {
77
99
  const watchers = Array.from(pending)
78
100
  pending.clear()
79
- for (const mark of watchers) {
80
- mark()
101
+ for (const watcher of watchers) {
102
+ watcher()
81
103
  }
82
104
  }
83
105
  }
@@ -101,11 +123,11 @@ const batch = (fn: () => void) => {
101
123
  * Run a function in a reactive context
102
124
  *
103
125
  * @param {() => void} run - function to run the computation or effect
104
- * @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)
126
+ * @param {Watcher} watcher - 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)
105
127
  */
106
- const watch = (run: () => void, mark?: Watcher): void => {
128
+ const observe = (run: () => void, watcher?: Watcher): void => {
107
129
  const prev = active
108
- active = mark
130
+ active = watcher
109
131
  try {
110
132
  run()
111
133
  } finally {
@@ -145,5 +167,6 @@ export {
145
167
  flush,
146
168
  batch,
147
169
  watch,
170
+ observe,
148
171
  enqueue,
149
172
  }
package/src/signal.d.ts CHANGED
@@ -1,8 +1,11 @@
1
- import { type ComputedCallback } from './computed';
1
+ import { type ComputedCallback, isComputedCallback } from './computed';
2
2
  type Signal<T extends {}> = {
3
3
  get(): T;
4
4
  };
5
5
  type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>;
6
+ type SignalValues<S extends Signal<{}>[]> = {
7
+ [K in keyof S]: S[K] extends Signal<infer T> ? T : never;
8
+ };
6
9
  declare const UNSET: any;
7
10
  /**
8
11
  * Check whether a value is a Signal or not
@@ -12,14 +15,6 @@ declare const UNSET: any;
12
15
  * @returns {boolean} - true if value is a Signal, false otherwise
13
16
  */
14
17
  declare const isSignal: <T extends {}>(value: unknown) => value is Signal<T>;
15
- /**
16
- * Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
17
- *
18
- * @since 0.12.0
19
- * @param {unknown} value - value to check
20
- * @returns {boolean} - true if value is a callback or callbacks object, false otherwise
21
- */
22
- declare const isComputedCallback: <T extends {}>(value: unknown) => value is ComputedCallback<T>;
23
18
  /**
24
19
  * Convert a value to a Signal if it's not already a Signal
25
20
  *
@@ -28,4 +23,4 @@ declare const isComputedCallback: <T extends {}>(value: unknown) => value is Com
28
23
  * @returns {Signal<T>} - converted Signal
29
24
  */
30
25
  declare const toSignal: <T extends {}>(value: MaybeSignal<T>) => Signal<T>;
31
- export { type Signal, type MaybeSignal, UNSET, isSignal, isComputedCallback, toSignal, };
26
+ export { type Signal, type MaybeSignal, type SignalValues, UNSET, isSignal, isComputedCallback, toSignal, };
package/src/signal.ts CHANGED
@@ -2,9 +2,9 @@ import { isState, state } from './state'
2
2
  import {
3
3
  type ComputedCallback,
4
4
  isComputed,
5
+ isComputedCallback,
5
6
  computed,
6
7
  } from './computed'
7
- import { isFunction } from './util'
8
8
 
9
9
  /* === Types === */
10
10
 
@@ -13,11 +13,15 @@ type Signal<T extends {}> = {
13
13
  }
14
14
  type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>
15
15
 
16
+ type SignalValues<S extends Signal<{}>[]> = {
17
+ [K in keyof S]: S[K] extends Signal<infer T> ? T : never
18
+ }
19
+
16
20
  /* === Constants === */
17
21
 
18
22
  const UNSET: any = Symbol()
19
23
 
20
- /* === Exported Functions === */
24
+ /* === Functions === */
21
25
 
22
26
  /**
23
27
  * Check whether a value is a Signal or not
@@ -30,17 +34,6 @@ const isSignal = /*#__PURE__*/ <T extends {}>(
30
34
  value: unknown,
31
35
  ): value is Signal<T> => isState(value) || isComputed(value)
32
36
 
33
- /**
34
- * Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
35
- *
36
- * @since 0.12.0
37
- * @param {unknown} value - value to check
38
- * @returns {boolean} - true if value is a callback or callbacks object, false otherwise
39
- */
40
- const isComputedCallback = /*#__PURE__*/ <T extends {}>(
41
- value: unknown,
42
- ): value is ComputedCallback<T> => isFunction(value) && value.length < 2
43
-
44
37
  /**
45
38
  * Convert a value to a Signal if it's not already a Signal
46
39
  *
@@ -62,6 +55,7 @@ const toSignal = /*#__PURE__*/ <T extends {}>(
62
55
  export {
63
56
  type Signal,
64
57
  type MaybeSignal,
58
+ type SignalValues,
65
59
  UNSET,
66
60
  isSignal,
67
61
  isComputedCallback,
package/src/state.d.ts CHANGED
@@ -4,6 +4,7 @@ type State<T extends {}> = {
4
4
  set(v: T): void;
5
5
  update(fn: (v: T) => T): void;
6
6
  };
7
+ declare const TYPE_STATE = "State";
7
8
  /**
8
9
  * Create a new state signal
9
10
  *
@@ -20,4 +21,4 @@ declare const state: <T extends {}>(initialValue: T) => State<T>;
20
21
  * @returns {boolean} - true if the value is a State instance, false otherwise
21
22
  */
22
23
  declare const isState: <T extends {}>(value: unknown) => value is State<T>;
23
- export { type State, state, isState };
24
+ export { type State, TYPE_STATE, state, isState };
package/src/state.ts CHANGED
@@ -86,4 +86,4 @@ const isState = /*#__PURE__*/ <T extends {}>(
86
86
 
87
87
  /* === Exports === */
88
88
 
89
- export { type State, state, isState }
89
+ export { type State, TYPE_STATE, state, isState }
package/src/util.d.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  declare const isFunction: <T>(value: unknown) => value is (...args: unknown[]) => T;
2
- declare const isAsyncFunction: <T>(value: unknown) => value is (...args: unknown[]) => Promise<T>;
3
2
  declare const isObjectOfType: <T>(value: unknown, type: string) => value is T;
4
- declare const isError: (value: unknown) => value is Error;
5
- declare const isAbortError: (value: unknown) => value is DOMException;
6
- declare const isPromise: <T>(value: unknown) => value is Promise<T>;
7
3
  declare const toError: (reason: unknown) => Error;
8
4
  declare class CircularDependencyError extends Error {
9
5
  constructor(where: string);
10
6
  }
11
- export { isFunction, isAsyncFunction, isObjectOfType, isError, isAbortError, isPromise, toError, CircularDependencyError, };
7
+ export { isFunction, isObjectOfType, toError, CircularDependencyError };
package/src/util.ts CHANGED
@@ -4,24 +4,13 @@ const isFunction = /*#__PURE__*/ <T>(
4
4
  value: unknown,
5
5
  ): value is (...args: unknown[]) => T => typeof value === 'function'
6
6
 
7
- const isAsyncFunction = /*#__PURE__*/ <T>(
8
- value: unknown,
9
- ): value is (...args: unknown[]) => Promise<T> =>
10
- isFunction(value) && value.constructor.name === 'AsyncFunction'
11
-
12
7
  const isObjectOfType = /*#__PURE__*/ <T>(
13
8
  value: unknown,
14
9
  type: string,
15
10
  ): value is T => Object.prototype.toString.call(value) === `[object ${type}]`
16
11
 
17
- const isError = /*#__PURE__*/ (value: unknown): value is Error =>
18
- value instanceof Error
19
- const isAbortError = /*#__PURE__*/ (value: unknown): value is DOMException =>
20
- value instanceof DOMException && value.name === 'AbortError'
21
- const isPromise = /*#__PURE__*/ <T>(value: unknown): value is Promise<T> =>
22
- value instanceof Promise
23
12
  const toError = (reason: unknown): Error =>
24
- isError(reason) ? reason : Error(String(reason))
13
+ reason instanceof Error ? reason : Error(String(reason))
25
14
 
26
15
  class CircularDependencyError extends Error {
27
16
  constructor(where: string) {
@@ -32,13 +21,4 @@ class CircularDependencyError extends Error {
32
21
 
33
22
  /* === Exports === */
34
23
 
35
- export {
36
- isFunction,
37
- isAsyncFunction,
38
- isObjectOfType,
39
- isError,
40
- isAbortError,
41
- isPromise,
42
- toError,
43
- CircularDependencyError,
44
- }
24
+ export { isFunction, isObjectOfType, toError, CircularDependencyError }
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { state, memo, batch, effect } from '../'
2
+ import { state, computed, batch, effect } from '../'
3
3
 
4
4
  /* === Tests === */
5
5
 
@@ -25,7 +25,7 @@ describe('Batch', function () {
25
25
  const a = state(3)
26
26
  const b = state(4)
27
27
  const c = state(5)
28
- const sum = memo(() => a.get() + b.get() + c.get())
28
+ const sum = computed(() => a.get() + b.get() + c.get())
29
29
  let result = 0
30
30
  let count = 0
31
31
  effect({
@@ -50,7 +50,7 @@ describe('Batch', function () {
50
50
  const signals = [state(2), state(3), state(5)]
51
51
 
52
52
  // Computed: derive a calculation ...
53
- const sum = memo(() => {
53
+ const sum = computed(() => {
54
54
  const v = signals.reduce((total, v) => total + v.get(), 0)
55
55
  if (!Number.isFinite(v)) throw new Error('Invalid value')
56
56
  return v
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, mock } from 'bun:test'
2
- import { state, memo, effect, batch } from '../'
2
+ import { state, computed, effect, batch } from '../'
3
3
  import { makeGraph, runGraph, Counter } from './util/dependency-graph'
4
4
  import {
5
5
  type ReactiveFramework,
@@ -25,7 +25,7 @@ const framework = {
25
25
  }
26
26
  },
27
27
  computed: <T extends {}>(fn: () => T) => {
28
- const c = memo(fn)
28
+ const c = computed(fn)
29
29
  return {
30
30
  read: () => c.get(),
31
31
  }