@zeix/cause-effect 0.13.2 → 0.14.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/src/signal.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { type State } from "./state";
2
- import { type Computed } from "./computed";
3
- type Signal<T extends {}> = State<T> | Computed<T>;
4
- type ComputedCallback<T extends {}> = (abort?: AbortSignal) => T | Promise<T>;
5
- type MaybeSignal<T extends {}> = Signal<T> | T | ComputedCallback<T>;
1
+ import { type ComputedCallback } from './computed';
2
+ type Signal<T extends {}> = {
3
+ get(): T;
4
+ };
5
+ type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>;
6
6
  declare const UNSET: any;
7
7
  /**
8
8
  * Check whether a value is a Signal or not
@@ -28,18 +28,4 @@ declare const isComputedCallback: <T extends {}>(value: unknown) => value is Com
28
28
  * @returns {Signal<T>} - converted Signal
29
29
  */
30
30
  declare const toSignal: <T extends {}>(value: MaybeSignal<T>) => Signal<T>;
31
- /**
32
- * Resolve signals or functions using signals and apply callbacks based on the results
33
- *
34
- * @since 0.13.0
35
- * @param {SignalMatcher<S, R>} matcher - SignalMatcher to match
36
- * @returns {R | Promise<R>} - result of the matched callback
37
- */
38
- declare const match: <S extends Signal<{}>[], R>(matcher: {
39
- signals: S;
40
- abort?: AbortSignal;
41
- ok: ((...values: { [K in keyof S]: S[K] extends Signal<infer T> ? T : never; }) => R | Promise<R>);
42
- err: ((...errors: Error[]) => R | Promise<R>);
43
- nil: (abort?: AbortSignal) => R | Promise<R>;
44
- }) => R | Promise<R>;
45
- export { type Signal, type MaybeSignal, type ComputedCallback, UNSET, isSignal, isComputedCallback, toSignal, match, };
31
+ export { type Signal, type MaybeSignal, UNSET, isSignal, isComputedCallback, toSignal, };
package/src/signal.ts CHANGED
@@ -1,12 +1,17 @@
1
- import { type State, isState, state } from "./state"
2
- import { type Computed, computed, isComputed } from "./computed"
3
- import { isAbortError, isFunction, toError } from "./util"
1
+ import { isState, state } from './state'
2
+ import {
3
+ type ComputedCallback,
4
+ isComputed,
5
+ computed,
6
+ } from './computed'
7
+ import { isFunction } from './util'
4
8
 
5
9
  /* === Types === */
6
10
 
7
- type Signal<T extends {}> = State<T> | Computed<T>
8
- type ComputedCallback<T extends {}> = (abort?: AbortSignal) => T | Promise<T>
9
- type MaybeSignal<T extends {}> = Signal<T> | T | ComputedCallback<T>
11
+ type Signal<T extends {}> = {
12
+ get(): T
13
+ }
14
+ type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>
10
15
 
11
16
  /* === Constants === */
12
17
 
@@ -16,87 +21,49 @@ const UNSET: any = Symbol()
16
21
 
17
22
  /**
18
23
  * Check whether a value is a Signal or not
19
- *
24
+ *
20
25
  * @since 0.9.0
21
26
  * @param {unknown} value - value to check
22
27
  * @returns {boolean} - true if value is a Signal, false otherwise
23
28
  */
24
- const isSignal = /*#__PURE__*/ <T extends {}>(value: unknown): value is Signal<T> =>
25
- isState(value) || isComputed(value)
29
+ const isSignal = /*#__PURE__*/ <T extends {}>(
30
+ value: unknown,
31
+ ): value is Signal<T> => isState(value) || isComputed(value)
26
32
 
27
33
  /**
28
34
  * Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
29
- *
35
+ *
30
36
  * @since 0.12.0
31
37
  * @param {unknown} value - value to check
32
38
  * @returns {boolean} - true if value is a callback or callbacks object, false otherwise
33
39
  */
34
40
  const isComputedCallback = /*#__PURE__*/ <T extends {}>(
35
- value: unknown
36
- ): value is ComputedCallback<T> =>
37
- isFunction(value) && value.length < 2
41
+ value: unknown,
42
+ ): value is ComputedCallback<T> => isFunction(value) && value.length < 2
38
43
 
39
44
  /**
40
45
  * Convert a value to a Signal if it's not already a Signal
41
- *
46
+ *
42
47
  * @since 0.9.6
43
48
  * @param {MaybeSignal<T>} value - value to convert to a Signal
44
49
  * @returns {Signal<T>} - converted Signal
45
50
  */
46
51
  const toSignal = /*#__PURE__*/ <T extends {}>(
47
- value: MaybeSignal<T>
48
- ): Signal<T> => isSignal<T>(value) ? value
49
- : isComputedCallback<T>(value) ? computed(value)
50
- : state(value as T)
51
-
52
+ value: MaybeSignal<T>,
53
+ ): Signal<T> =>
54
+ isSignal<T>(value)
55
+ ? value
56
+ : isComputedCallback<T>(value)
57
+ ? computed(value)
58
+ : state(value as T)
52
59
 
53
- /**
54
- * Resolve signals or functions using signals and apply callbacks based on the results
55
- *
56
- * @since 0.13.0
57
- * @param {SignalMatcher<S, R>} matcher - SignalMatcher to match
58
- * @returns {R | Promise<R>} - result of the matched callback
59
- */
60
- const match = <S extends Signal<{}>[], R>(
61
- matcher: {
62
- signals: S,
63
- abort?: AbortSignal,
64
- ok: ((...values: {
65
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never
66
- }) => R | Promise<R>),
67
- err: ((...errors: Error[]) => R | Promise<R>),
68
- nil: (abort?: AbortSignal) => R | Promise<R>
69
- }
70
- ): R | Promise<R> => {
71
- const { signals, abort, ok, err, nil } = matcher
72
-
73
- const errors: Error[] = []
74
- let suspense = false
75
- const values = signals.map(signal => {
76
- try {
77
- const value = signal.get()
78
- if (value === UNSET) suspense = true
79
- return value
80
- } catch (e) {
81
- if (isAbortError(e)) throw e
82
- errors.push(toError(e))
83
- }
84
- }) as {
85
- [K in keyof S]: S[K] extends Signal<infer T extends {}> ? T : never
86
- }
87
-
88
- try {
89
- return suspense ? nil(abort)
90
- : errors.length ? err(...errors)
91
- : ok(...values)
92
- } catch (e) {
93
- if (isAbortError(e)) throw e
94
- const error = toError(e)
95
- return err(error)
96
- }
97
- }
60
+ /* === Exports === */
98
61
 
99
62
  export {
100
- type Signal, type MaybeSignal, type ComputedCallback,
101
- UNSET, isSignal, isComputedCallback, toSignal, match,
102
- }
63
+ type Signal,
64
+ type MaybeSignal,
65
+ UNSET,
66
+ isSignal,
67
+ isComputedCallback,
68
+ toSignal,
69
+ }
package/src/state.d.ts CHANGED
@@ -1,12 +1,8 @@
1
- import { type Computed } from './computed';
2
- import { type TapMatcher } from './effect';
3
- export type State<T extends {}> = {
1
+ type State<T extends {}> = {
4
2
  [Symbol.toStringTag]: 'State';
5
3
  get(): T;
6
4
  set(v: T): void;
7
5
  update(fn: (v: T) => T): void;
8
- map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>;
9
- tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void;
10
6
  };
11
7
  /**
12
8
  * Create a new state signal
@@ -15,7 +11,7 @@ export type State<T extends {}> = {
15
11
  * @param {T} initialValue - initial value of the state
16
12
  * @returns {State<T>} - new state signal
17
13
  */
18
- export declare const state: <T extends {}>(initialValue: T) => State<T>;
14
+ declare const state: <T extends {}>(initialValue: T) => State<T>;
19
15
  /**
20
16
  * Check if the provided value is a State instance
21
17
  *
@@ -23,4 +19,5 @@ export declare const state: <T extends {}>(initialValue: T) => State<T>;
23
19
  * @param {unknown} value - value to check
24
20
  * @returns {boolean} - true if the value is a State instance, false otherwise
25
21
  */
26
- export declare const isState: <T extends {}>(value: unknown) => value is State<T>;
22
+ declare const isState: <T extends {}>(value: unknown) => value is State<T>;
23
+ export { type State, state, isState };
package/src/state.ts CHANGED
@@ -1,25 +1,21 @@
1
1
  import { UNSET } from './signal'
2
- import { type Computed, computed } from './computed'
3
- import { isFunction, isObjectOfType } from './util'
2
+ import { isObjectOfType } from './util'
4
3
  import { type Watcher, notify, subscribe } from './scheduler'
5
- import { type TapMatcher, type EffectMatcher, effect } from './effect'
6
4
 
7
5
  /* === Types === */
8
6
 
9
- export type State<T extends {}> = {
7
+ type State<T extends {}> = {
10
8
  [Symbol.toStringTag]: 'State'
11
9
  get(): T
12
10
  set(v: T): void
13
11
  update(fn: (v: T) => T): void
14
- map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>
15
- tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void
16
12
  }
17
13
 
18
14
  /* === Constants === */
19
15
 
20
16
  const TYPE_STATE = 'State'
21
17
 
22
- /* === State Factory === */
18
+ /* === Functions === */
23
19
 
24
20
  /**
25
21
  * Create a new state signal
@@ -28,9 +24,7 @@ const TYPE_STATE = 'State'
28
24
  * @param {T} initialValue - initial value of the state
29
25
  * @returns {State<T>} - new state signal
30
26
  */
31
- export const state = /*#__PURE__*/ <T extends {}>(
32
- initialValue: T,
33
- ): State<T> => {
27
+ const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> => {
34
28
  const watchers: Set<Watcher> = new Set()
35
29
  let value: T = initialValue
36
30
 
@@ -74,34 +68,6 @@ export const state = /*#__PURE__*/ <T extends {}>(
74
68
  update: (fn: (v: T) => T): void => {
75
69
  s.set(fn(value))
76
70
  },
77
-
78
- /**
79
- * Create a computed signal from the current state signal
80
- *
81
- * @since 0.9.0
82
- * @param {(v: T) => U | Promise<U>} fn - computed callback
83
- * @returns {Computed<U>} - computed signal
84
- */
85
- map: <U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U> =>
86
- computed({
87
- signals: [s],
88
- ok: fn,
89
- }),
90
-
91
- /**
92
- * Case matching for the state signal with effect callbacks
93
- *
94
- * @since 0.13.0
95
- * @param {TapMatcher<T> | ((v: T) => void | (() => void))} matcher - tap matcher or effect callback
96
- * @returns {() => void} - cleanup function for the effect
97
- */
98
- tap: (
99
- matcher: TapMatcher<T> | ((v: T) => void | (() => void)),
100
- ): (() => void) =>
101
- effect({
102
- signals: [s],
103
- ...(isFunction(matcher) ? { ok: matcher } : matcher),
104
- } as EffectMatcher<[State<T>]>),
105
71
  }
106
72
 
107
73
  return s
@@ -114,6 +80,10 @@ export const state = /*#__PURE__*/ <T extends {}>(
114
80
  * @param {unknown} value - value to check
115
81
  * @returns {boolean} - true if the value is a State instance, false otherwise
116
82
  */
117
- export const isState = /*#__PURE__*/ <T extends {}>(
83
+ const isState = /*#__PURE__*/ <T extends {}>(
118
84
  value: unknown,
119
85
  ): value is State<T> => isObjectOfType(value, TYPE_STATE)
86
+
87
+ /* === Exports === */
88
+
89
+ export { type State, state, isState }
package/src/task.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type Computed } from './computed';
2
+ /**
3
+ * Callback for async computation tasks
4
+ * This explicitly returns a Promise<T> to differentiate from MemoCallback
5
+ *
6
+ * @since 0.14.0
7
+ */
8
+ type TaskCallback<T extends {}> = (abort: AbortSignal) => Promise<T>;
9
+ /**
10
+ * Create a derived signal that supports asynchronous computations
11
+ *
12
+ * @since 0.14.0
13
+ * @param {TaskCallback<T>} fn - async computation callback
14
+ * @returns {Computed<T>} - Computed signal
15
+ */
16
+ declare const task: <T extends {}>(fn: TaskCallback<T>) => Computed<T>;
17
+ export { type TaskCallback, task };
package/src/task.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { UNSET } from './signal'
2
+ import {
3
+ CircularDependencyError,
4
+ isAbortError,
5
+ isPromise,
6
+ toError,
7
+ } from './util'
8
+ import {
9
+ type Cleanup,
10
+ type Watcher,
11
+ flush,
12
+ notify,
13
+ subscribe,
14
+ watch,
15
+ } from './scheduler'
16
+ import {
17
+ type Computed,
18
+ TYPE_COMPUTED,
19
+ } from './computed'
20
+
21
+ /* === Types === */
22
+
23
+ /**
24
+ * Callback for async computation tasks
25
+ * This explicitly returns a Promise<T> to differentiate from MemoCallback
26
+ *
27
+ * @since 0.14.0
28
+ */
29
+ type TaskCallback<T extends {}> = (abort: AbortSignal) => Promise<T>
30
+
31
+ /* === Function === */
32
+
33
+ /**
34
+ * Create a derived signal that supports asynchronous computations
35
+ *
36
+ * @since 0.14.0
37
+ * @param {TaskCallback<T>} fn - async computation callback
38
+ * @returns {Computed<T>} - Computed signal
39
+ */
40
+ const task = <T extends {}>(fn: TaskCallback<T>): Computed<T> => {
41
+ const watchers: Set<Watcher> = new Set()
42
+
43
+ // Internal state
44
+ let value: T = UNSET
45
+ let error: Error | undefined
46
+ let dirty = true
47
+ let changed = false
48
+ let computing = false
49
+ let controller: AbortController | undefined
50
+
51
+ // Functions to update internal state
52
+ const ok = (v: T) => {
53
+ if (!Object.is(v, value)) {
54
+ value = v
55
+ dirty = false
56
+ error = undefined
57
+ changed = true
58
+ }
59
+ }
60
+ const nil = () => {
61
+ changed = UNSET !== value
62
+ value = UNSET
63
+ error = undefined
64
+ }
65
+ const err = (e: unknown) => {
66
+ const newError = toError(e)
67
+ changed = !(
68
+ error &&
69
+ newError.name === error.name &&
70
+ newError.message === error.message
71
+ )
72
+ value = UNSET
73
+ error = newError
74
+ }
75
+ const resolve = (v: T) => {
76
+ computing = false
77
+ controller = undefined
78
+ ok(v)
79
+ if (changed) notify(watchers)
80
+ }
81
+ const reject = (e: unknown) => {
82
+ computing = false
83
+ controller = undefined
84
+ err(e)
85
+ if (changed) notify(watchers)
86
+ }
87
+ const abort = () => {
88
+ computing = false
89
+ controller = undefined
90
+ compute() // retry
91
+ }
92
+
93
+ // Called when notified from sources (push)
94
+ const mark = (() => {
95
+ dirty = true
96
+ controller?.abort('Aborted because source signal changed')
97
+ if (watchers.size) {
98
+ notify(watchers)
99
+ } else {
100
+ mark.cleanups.forEach(fn => fn())
101
+ mark.cleanups.clear()
102
+ }
103
+ }) as Watcher
104
+ mark.cleanups = new Set<Cleanup>()
105
+
106
+ // Called when requested by dependencies (pull)
107
+ const compute = () =>
108
+ watch(() => {
109
+ if (computing) throw new CircularDependencyError('task')
110
+ changed = false
111
+ controller = new AbortController()
112
+ controller.signal.addEventListener('abort', abort, {
113
+ once: true,
114
+ })
115
+
116
+ let result: T | Promise<T>
117
+ computing = true
118
+ try {
119
+ result = fn(controller.signal)
120
+ } catch (e) {
121
+ if (isAbortError(e)) nil()
122
+ else err(e)
123
+ computing = false
124
+ return
125
+ }
126
+ if (isPromise(result)) result.then(resolve, reject)
127
+ else if (null == result || UNSET === result) nil()
128
+ else ok(result)
129
+ computing = false
130
+ }, mark)
131
+
132
+ const c: Computed<T> = {
133
+ [Symbol.toStringTag]: TYPE_COMPUTED,
134
+
135
+ /**
136
+ * Get the current value of the computed
137
+ *
138
+ * @returns {T} - current value of the computed
139
+ */
140
+ get: (): T => {
141
+ subscribe(watchers)
142
+ flush()
143
+ if (dirty) compute()
144
+ if (error) throw error
145
+ return value
146
+ },
147
+ }
148
+ return c
149
+ }
150
+
151
+ /* === Exports === */
152
+
153
+ export { type TaskCallback, task }
package/src/util.ts CHANGED
@@ -30,6 +30,8 @@ class CircularDependencyError extends Error {
30
30
  }
31
31
  }
32
32
 
33
+ /* === Exports === */
34
+
33
35
  export {
34
36
  isFunction,
35
37
  isAsyncFunction,
@@ -1,9 +1,5 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
- import { state, computed, batch } from '../'
3
-
4
- /* === Utility Functions === */
5
-
6
- const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
2
+ import { state, memo, batch, effect } from '../'
7
3
 
8
4
  /* === Tests === */
9
5
 
@@ -12,8 +8,8 @@ describe('Batch', function () {
12
8
  const cause = state(0)
13
9
  let result = 0
14
10
  let count = 0
15
- cause.tap(res => {
16
- result = res
11
+ effect(() => {
12
+ result = cause.get()
17
13
  count++
18
14
  })
19
15
  batch(() => {
@@ -29,10 +25,11 @@ describe('Batch', function () {
29
25
  const a = state(3)
30
26
  const b = state(4)
31
27
  const c = state(5)
32
- const sum = computed(() => a.get() + b.get() + c.get())
28
+ const sum = memo(() => a.get() + b.get() + c.get())
33
29
  let result = 0
34
30
  let count = 0
35
- sum.tap({
31
+ effect({
32
+ signals: [sum],
36
33
  ok: res => {
37
34
  result = res
38
35
  count++
@@ -53,10 +50,8 @@ describe('Batch', function () {
53
50
  const signals = [state(2), state(3), state(5)]
54
51
 
55
52
  // Computed: derive a calculation ...
56
- const sum = computed(() =>
57
- signals.reduce((total, v) => total + v.get(), 0),
58
- ).map(v => {
59
- // ... perform validation and handle errors
53
+ const sum = memo(() => {
54
+ const v = signals.reduce((total, v) => total + v.get(), 0)
60
55
  if (!Number.isFinite(v)) throw new Error('Invalid value')
61
56
  return v
62
57
  })
@@ -66,7 +61,8 @@ describe('Batch', function () {
66
61
  let errCount = 0
67
62
 
68
63
  // Effect: switch cases for the result
69
- sum.tap({
64
+ effect({
65
+ signals: [sum],
70
66
  ok: v => {
71
67
  result = v
72
68
  okCount++