@zeix/cause-effect 0.13.1 → 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/lib/computed.ts DELETED
@@ -1,206 +0,0 @@
1
- import { type Signal, type ComputedCallback, match, UNSET } from './signal'
2
- import { CircularDependencyError, isAbortError, isAsyncFunction, isFunction, isObjectOfType, isPromise, toError } from './util'
3
- import { type Watcher, flush, notify, subscribe, watch } from './scheduler'
4
- import { type TapMatcher, type EffectMatcher, effect } from './effect'
5
-
6
- /* === Types === */
7
-
8
- export type ComputedMatcher<S extends Signal<{}>[], R extends {}> = {
9
- signals: S,
10
- abort?: AbortSignal
11
- ok: (...values: {
12
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never
13
- }) => R | Promise<R>
14
- err?: (...errors: Error[]) => R | Promise<R>
15
- nil?: () => R | Promise<R>
16
- }
17
-
18
- export type Computed<T extends {}> = {
19
- [Symbol.toStringTag]: 'Computed'
20
- get(): T
21
- map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>
22
- tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void
23
- }
24
-
25
- /* === Constants === */
26
-
27
- const TYPE_COMPUTED = 'Computed'
28
-
29
- /* === Private Functions === */
30
-
31
- const isEquivalentError = /*#__PURE__*/ (
32
- error1: Error,
33
- error2: Error | undefined
34
- ): boolean => {
35
- if (!error2) return false
36
- return error1.name === error2.name && error1.message === error2.message
37
- }
38
-
39
- /* === Computed Factory === */
40
-
41
- /**
42
- * Create a derived signal from existing signals
43
- *
44
- * @since 0.9.0
45
- * @param {ComputedMatcher<S, T> | ComputedCallback<T>} matcher - computed matcher or callback
46
- * @returns {Computed<T>} - Computed signal
47
- */
48
- export const computed = <T extends {}, S extends Signal<{}>[] = []>(
49
- matcher: ComputedMatcher<S, T> | ComputedCallback<T>,
50
- ): Computed<T> => {
51
- const watchers: Set<Watcher> = new Set()
52
- const m = isFunction(matcher) ? undefined : {
53
- nil: () => UNSET,
54
- err: (...errors: Error[]) => {
55
- if (errors.length > 1) throw new AggregateError(errors)
56
- else throw errors[0]
57
- },
58
- ...matcher,
59
- } as Required<ComputedMatcher<S, T>>
60
- const fn = (m ? m.ok : matcher) as ComputedCallback<T>
61
-
62
- // Internal state
63
- let value: T = UNSET
64
- let error: Error | undefined
65
- let dirty = true
66
- let changed = false
67
- let computing = false
68
- let controller: AbortController | undefined
69
-
70
- // Functions to update internal state
71
- const ok = (v: T) => {
72
- if (!Object.is(v, value)) {
73
- value = v
74
- dirty = false
75
- error = undefined
76
- changed = true
77
- }
78
- }
79
- const nil = () => {
80
- changed = (UNSET !== value)
81
- value = UNSET
82
- error = undefined
83
- }
84
- const err = (e: unknown) => {
85
- const newError = toError(e)
86
- changed = !isEquivalentError(newError, error)
87
- value = UNSET
88
- error = newError
89
- }
90
- const resolve = (v: T) => {
91
- computing = false
92
- controller = undefined
93
- ok(v)
94
- if (changed) notify(watchers)
95
- }
96
- const reject = (e: unknown) => {
97
- computing = false
98
- controller = undefined
99
- err(e)
100
- if (changed) notify(watchers)
101
- }
102
- const abort = () => {
103
- computing = false
104
- controller = undefined
105
- compute() // retry
106
- }
107
-
108
- // Called when notified from sources (push)
109
- const mark = (() => {
110
- dirty = true
111
- controller?.abort('Aborted because source signal changed')
112
- if (watchers.size) {
113
- if (changed) notify(watchers)
114
- } else {
115
- mark.cleanups.forEach((fn: () => void) => fn())
116
- mark.cleanups.clear()
117
- }
118
- }) as Watcher
119
- mark.cleanups = new Set()
120
-
121
- // Called when requested by dependencies (pull)
122
- const compute = () => watch(() => {
123
- if (computing) throw new CircularDependencyError('computed')
124
- changed = false
125
- if (isAsyncFunction(fn)) {
126
- if (controller) return value // return current value until promise resolves
127
- controller = new AbortController()
128
- if (m) m.abort = m.abort instanceof AbortSignal
129
- ? AbortSignal.any([m.abort, controller.signal])
130
- : controller.signal
131
- controller.signal.addEventListener('abort', abort, { once: true })
132
- }
133
- let result: T | Promise<T>
134
- computing = true
135
- try {
136
- result = m && m.signals.length
137
- ? match<S, T>(m)
138
- : fn(controller?.signal)
139
- } catch (e) {
140
- isAbortError(e) ? nil() : err(e)
141
- computing = false
142
- return
143
- }
144
- if (isPromise(result)) result.then(resolve, reject)
145
- else if (null == result || UNSET === result) nil()
146
- else ok(result)
147
- computing = false
148
- }, mark)
149
-
150
- const c: Computed<T> = {
151
- [Symbol.toStringTag]: TYPE_COMPUTED,
152
-
153
- /**
154
- * Get the current value of the computed
155
- *
156
- * @since 0.9.0
157
- * @returns {T} - current value of the computed
158
- */
159
- get: (): T => {
160
- subscribe(watchers)
161
- flush()
162
- if (dirty) compute()
163
- if (error) throw error
164
- return value
165
- },
166
-
167
- /**
168
- * Create a computed signal from the current computed signal
169
- *
170
- * @since 0.9.0
171
- * @param {((v: T) => U | Promise<U>)} fn - computed callback
172
- * @returns {Computed<U>} - computed signal
173
- */
174
- map: <U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U> =>
175
- computed({
176
- signals: [c],
177
- ok: fn
178
- }),
179
-
180
- /**
181
- * Case matching for the computed signal with effect callbacks
182
- *
183
- * @since 0.13.0
184
- * @param {TapMatcher<T> | ((v: T) => void | (() => void))} matcher - tap matcher or effect callback
185
- * @returns {() => void} - cleanup function for the effect
186
- */
187
- tap: (matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void =>
188
- effect({
189
- signals: [c],
190
- ...(isFunction(matcher) ? { ok: matcher } : matcher)
191
- } as EffectMatcher<[Computed<T>]>)
192
- }
193
- return c
194
- }
195
-
196
- /* === Helper Functions === */
197
-
198
- /**
199
- * Check if a value is a computed state
200
- *
201
- * @since 0.9.0
202
- * @param {unknown} value - value to check
203
- * @returns {boolean} - true if value is a computed state, false otherwise
204
- */
205
- export const isComputed = /*#__PURE__*/ <T extends {}>(value: unknown): value is Computed<T> =>
206
- isObjectOfType(value, TYPE_COMPUTED)
package/lib/effect.d.ts DELETED
@@ -1,22 +0,0 @@
1
- import { type Signal } from './signal';
2
- export type TapMatcher<T extends {}> = {
3
- ok: (value: T) => void | (() => void);
4
- err?: (error: Error) => void | (() => void);
5
- nil?: () => void | (() => void);
6
- };
7
- export type EffectMatcher<S extends Signal<{}>[]> = {
8
- signals: S;
9
- ok: (...values: {
10
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never;
11
- }) => void | (() => void);
12
- err?: (...errors: Error[]) => void | (() => void);
13
- nil?: () => void | (() => void);
14
- };
15
- /**
16
- * Define what happens when a reactive state changes
17
- *
18
- * @since 0.1.0
19
- * @param {EffectMatcher<S> | (() => void | (() => void))} matcher - effect matcher or callback
20
- * @returns {() => void} - cleanup function for the effect
21
- */
22
- export declare function effect<S extends Signal<{}>[]>(matcher: EffectMatcher<S> | (() => void | (() => void))): () => void;
package/lib/effect.ts DELETED
@@ -1,61 +0,0 @@
1
- import { type Signal, match } from './signal'
2
- import { CircularDependencyError, isFunction, toError } from './util'
3
- import { watch, type Watcher } from './scheduler'
4
-
5
- /* === Types === */
6
-
7
- export type TapMatcher<T extends {}> = {
8
- ok: (value: T) => void | (() => void)
9
- err?: (error: Error) => void | (() => void)
10
- nil?: () => void | (() => void)
11
- }
12
-
13
- export type EffectMatcher<S extends Signal<{}>[]> = {
14
- signals: S
15
- ok: (...values: {
16
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never
17
- }) => void | (() => void)
18
- err?: (...errors: Error[]) => void | (() => void)
19
- nil?: () => void | (() => void)
20
- }
21
-
22
- /* === Exported Functions === */
23
-
24
- /**
25
- * Define what happens when a reactive state changes
26
- *
27
- * @since 0.1.0
28
- * @param {EffectMatcher<S> | (() => void | (() => void))} matcher - effect matcher or callback
29
- * @returns {() => void} - cleanup function for the effect
30
- */
31
- export function effect<S extends Signal<{}>[]>(
32
- matcher: EffectMatcher<S> | (() => void | (() => void))
33
- ): () => void {
34
- const {
35
- signals,
36
- ok,
37
- err = console.error,
38
- nil = () => {}
39
- } = isFunction(matcher)
40
- ? { signals: [] as unknown as S, ok: matcher }
41
- : matcher
42
- let running = false
43
- const run = (() => watch(() => {
44
- if (running) throw new CircularDependencyError('effect')
45
- running = true
46
- let cleanup: void | (() => void) = undefined
47
- try {
48
- cleanup = match<S, void | (() => void)>({ signals, ok, err, nil }) as void | (() => void)
49
- } catch (e) {
50
- err(toError(e))
51
- }
52
- if (isFunction(cleanup)) run.cleanups.add(cleanup)
53
- running = false
54
- }, run)) as Watcher
55
- run.cleanups = new Set()
56
- run()
57
- return () => {
58
- run.cleanups.forEach((fn: () => void) => fn())
59
- run.cleanups.clear()
60
- }
61
- }
package/lib/signal.d.ts DELETED
@@ -1,45 +0,0 @@
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>;
6
- declare const UNSET: any;
7
- /**
8
- * Check whether a value is a Signal or not
9
- *
10
- * @since 0.9.0
11
- * @param {unknown} value - value to check
12
- * @returns {boolean} - true if value is a Signal, false otherwise
13
- */
14
- declare const isSignal: <T extends {}>(value: any) => 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
- /**
24
- * Convert a value to a Signal if it's not already a Signal
25
- *
26
- * @since 0.9.6
27
- * @param {MaybeSignal<T>} value - value to convert to a Signal
28
- * @returns {Signal<T>} - converted Signal
29
- */
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, };
package/lib/signal.ts DELETED
@@ -1,102 +0,0 @@
1
- import { type State, isState, state } from "./state"
2
- import { type Computed, computed, isComputed } from "./computed"
3
- import { isAbortError, isFunction, toError } from "./util"
4
-
5
- /* === Types === */
6
-
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>
10
-
11
- /* === Constants === */
12
-
13
- const UNSET: any = Symbol()
14
-
15
- /* === Exported Functions === */
16
-
17
- /**
18
- * Check whether a value is a Signal or not
19
- *
20
- * @since 0.9.0
21
- * @param {unknown} value - value to check
22
- * @returns {boolean} - true if value is a Signal, false otherwise
23
- */
24
- const isSignal = /*#__PURE__*/ <T extends {}>(value: any): value is Signal<T> =>
25
- isState(value) || isComputed(value)
26
-
27
- /**
28
- * Check if the provided value is a callback that may be used as input for toSignal() to derive a computed state
29
- *
30
- * @since 0.12.0
31
- * @param {unknown} value - value to check
32
- * @returns {boolean} - true if value is a callback or callbacks object, false otherwise
33
- */
34
- const isComputedCallback = /*#__PURE__*/ <T extends {}>(
35
- value: unknown
36
- ): value is ComputedCallback<T> =>
37
- isFunction(value) && value.length < 2
38
-
39
- /**
40
- * Convert a value to a Signal if it's not already a Signal
41
- *
42
- * @since 0.9.6
43
- * @param {MaybeSignal<T>} value - value to convert to a Signal
44
- * @returns {Signal<T>} - converted Signal
45
- */
46
- 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
-
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
- }
98
-
99
- export {
100
- type Signal, type MaybeSignal, type ComputedCallback,
101
- UNSET, isSignal, isComputedCallback, toSignal, match,
102
- }
package/lib/state.ts DELETED
@@ -1,118 +0,0 @@
1
- import { UNSET } from './signal'
2
- import { type Computed, computed } from './computed'
3
- import { isFunction, isObjectOfType } from './util'
4
- import { type Watcher, notify, subscribe } from './scheduler'
5
- import { type TapMatcher, type EffectMatcher, effect } from './effect'
6
-
7
- /* === Types === */
8
-
9
- export type State<T extends {}> = {
10
- [Symbol.toStringTag]: 'State'
11
- get(): T
12
- set(v: T): void
13
- 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
- }
17
-
18
- /* === Constants === */
19
-
20
- const TYPE_STATE = 'State'
21
-
22
- /* === State Factory === */
23
-
24
- /**
25
- * Create a new state signal
26
- *
27
- * @since 0.9.0
28
- * @param {T} initialValue - initial value of the state
29
- * @returns {State<T>} - new state signal
30
- */
31
- export const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> => {
32
- const watchers: Set<Watcher> = new Set()
33
- let value: T = initialValue
34
-
35
- const s: State<T> = {
36
- [Symbol.toStringTag]: TYPE_STATE,
37
-
38
- /**
39
- * Get the current value of the state
40
- *
41
- * @since 0.9.0
42
- * @returns {T} - current value of the state
43
- */
44
- get: (): T => {
45
- subscribe(watchers)
46
- return value
47
- },
48
-
49
- /**
50
- * Set a new value of the state
51
- *
52
- * @since 0.9.0
53
- * @param {T} v
54
- * @returns {void}
55
- */
56
- set: (v: T): void => {
57
- if (Object.is(value, v)) return
58
- value = v
59
- notify(watchers)
60
-
61
- // Setting to UNSET clears the watchers so the signal can be garbage collected
62
- if (UNSET === value) watchers.clear()
63
- },
64
-
65
- /**
66
- * Update the state with a new value using a function
67
- *
68
- * @since 0.10.0
69
- * @param {(v: T) => T} fn - function to update the state
70
- * @returns {void} - updates the state with the result of the function
71
- */
72
- update: (fn: (v: T) => T): void => {
73
- s.set(fn(value))
74
- },
75
-
76
- /**
77
- * Create a computed signal from the current state signal
78
- *
79
- * @since 0.9.0
80
- * @param {(v: T) => U | Promise<U>} fn - computed callback
81
- * @returns {Computed<U>} - computed signal
82
- */
83
- map: <U extends {}>(
84
- fn: (v: T) => U | Promise<U>
85
- ): 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
- }
106
-
107
- return s
108
- }
109
-
110
- /**
111
- * Check if the provided value is a State instance
112
- *
113
- * @since 0.9.0
114
- * @param {unknown} value - value to check
115
- * @returns {boolean} - true if the value is a State instance, false otherwise
116
- */
117
- export const isState = /*#__PURE__*/ <T extends {}>(value: unknown): value is State<T> =>
118
- isObjectOfType(value, TYPE_STATE)