@zeix/cause-effect 0.11.0 → 0.12.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.
@@ -0,0 +1,127 @@
1
+ /* === Types === */
2
+
3
+ export type EnqueueDedupe = [Element, string]
4
+
5
+ export type Watcher = () => void
6
+ export type Updater = <T>() => T
7
+
8
+ /* === Internal === */
9
+
10
+ // Currently active watcher
11
+ let active: Watcher | undefined
12
+
13
+ // Pending queue for batched change notifications
14
+ const pending = new Set<Watcher>()
15
+ let batchDepth = 0
16
+
17
+ // Map of DOM elements to update functions
18
+ const updateMap = new Map<Element, Map<string, () => void>>()
19
+ let requestId: number | undefined
20
+
21
+ const updateDOM = () => {
22
+ requestId = undefined
23
+ for (const elementMap of updateMap.values()) {
24
+ for (const fn of elementMap.values()) {
25
+ fn()
26
+ }
27
+ elementMap.clear()
28
+ }
29
+ }
30
+
31
+ const requestTick = () => {
32
+ if (requestId) cancelAnimationFrame(requestId)
33
+ requestId = requestAnimationFrame(updateDOM)
34
+ }
35
+
36
+ // Initial render when the call stack is empty
37
+ queueMicrotask(updateDOM)
38
+
39
+ /* === Exported Functions === */
40
+
41
+ /**
42
+ * Add active watcher to the array of watchers
43
+ *
44
+ * @param {Watcher[]} watchers - watchers of the signal
45
+ */
46
+ export const subscribe = (watchers: Watcher[]) => {
47
+ // if (!active) console.warn('Calling .get() outside of a reactive context')
48
+ if (active && !watchers.includes(active)) {
49
+ watchers.push(active)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Add watchers to the pending set of change notifications
55
+ *
56
+ * @param {Watcher[]} watchers - watchers of the signal
57
+ */
58
+ export const notify = (watchers: Watcher[]) => {
59
+ for (const mark of watchers) {
60
+ batchDepth ? pending.add(mark) : mark()
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Flush all pending changes to notify watchers
66
+ */
67
+ export const flush = () => {
68
+ while (pending.size) {
69
+ const watchers = Array.from(pending)
70
+ pending.clear()
71
+ for (const mark of watchers) {
72
+ mark()
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Batch multiple changes in a single signal graph and DOM update cycle
79
+ *
80
+ * @param {() => void} fn - function with multiple signal writes to be batched
81
+ */
82
+ export const batch = (fn: () => void) => {
83
+ batchDepth++
84
+ fn()
85
+ flush()
86
+ batchDepth--
87
+ }
88
+
89
+ /**
90
+ * Run a function in a reactive context
91
+ *
92
+ * @param {() => void} run - function to run the computation or effect
93
+ * @param {Watcher} mark - function to be called when the state changes
94
+ */
95
+ export const watch = (run: () => void, mark: Watcher): void => {
96
+ const prev = active
97
+ active = mark
98
+ run()
99
+ active = prev
100
+ }
101
+
102
+ /**
103
+ * Enqueue a function to be executed on the next animation frame
104
+ *
105
+ * @param callback
106
+ * @param dedupe
107
+ * @returns
108
+ */
109
+ export const enqueue = <T>(
110
+ update: Updater,
111
+ dedupe?: EnqueueDedupe
112
+ ) => new Promise<T>((resolve, reject) => {
113
+ const wrappedCallback = () => {
114
+ try {
115
+ resolve(update())
116
+ } catch (error) {
117
+ reject(error)
118
+ }
119
+ }
120
+ 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
+ }
126
+ requestTick()
127
+ })
package/lib/signal.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type State } from "./state";
2
2
  import { type Computed } from "./computed";
3
3
  type Signal<T extends {}> = State<T> | Computed<T>;
4
- type MaybeSignal<T extends {}> = State<T> | Computed<T> | T | ((old?: T) => T);
5
- type Watcher = () => void;
4
+ type UnknownSignal = Signal<{}>;
5
+ type MaybeSignal<T extends {}> = Signal<T> | T | (() => T);
6
+ type SignalValue<T> = T extends Signal<infer U> ? U : never;
6
7
  export declare const UNSET: any;
7
8
  /**
8
9
  * Check whether a value is a Signal or not
@@ -22,28 +23,16 @@ declare const isSignal: <T extends {}>(value: any) => value is Signal<T>;
22
23
  */
23
24
  declare const toSignal: <T extends {}>(value: MaybeSignal<T>) => Signal<T>;
24
25
  /**
25
- * Add notify function of active watchers to the set of watchers
26
+ * Resolve signals and apply callbacks based on the results
26
27
  *
27
- * @param {Watcher[]} watchers - set of current watchers
28
+ * @since 0.12.0
29
+ * @param {U} signals - dependency signals
30
+ * @param {Record<string, (...args) => T | Promise<T> | Error | void>} callbacks - ok, nil, err callbacks
31
+ * @returns {T | Promise<T> | Error | void} - result of chosen callback
28
32
  */
29
- declare const subscribe: (watchers: Watcher[]) => void;
30
- /**
31
- * Notify all subscribers of the state change or add to the pending set if batching is enabled
32
- *
33
- * @param {Watcher[]} watchers
34
- */
35
- declare const notify: (watchers: Watcher[]) => void;
36
- /**
37
- * Run a function in a reactive context
38
- *
39
- * @param {() => void} run - function to run the computation or effect
40
- * @param {Watcher} mark - function to be called when the state changes
41
- */
42
- declare const watch: (run: () => void, mark: Watcher) => void;
43
- /**
44
- * Batch multiple state changes into a single update
45
- *
46
- * @param {() => void} run - function to run the batch of state changes
47
- */
48
- declare const batch: (run: () => void) => void;
49
- export { type Signal, type MaybeSignal, type Watcher, isSignal, toSignal, subscribe, notify, watch, batch };
33
+ declare const resolveSignals: <T extends {}, U extends UnknownSignal[]>(signals: U, callbacks: {
34
+ ok: (...values: { [K in keyof U]: SignalValue<U[K]>; }) => T | Promise<T> | Error | void;
35
+ nil?: () => T | Promise<T> | Error | void;
36
+ err?: (...errors: Error[]) => T | Promise<T> | Error | void;
37
+ }) => T | Promise<T> | Error | void;
38
+ export { type Signal, type UnknownSignal, type SignalValue, type MaybeSignal, isSignal, toSignal, resolveSignals, };
package/lib/signal.ts CHANGED
@@ -1,40 +1,14 @@
1
1
  import { type State, isState, state } from "./state"
2
2
  import { computed, type Computed, isComputed } from "./computed"
3
- import { isComputeFunction } from "./util"
3
+ import { isComputeFunction, toError } from "./util"
4
4
 
5
5
  /* === Types === */
6
6
 
7
7
  type Signal<T extends {}> = State<T> | Computed<T>
8
+ type UnknownSignal = Signal<{}>
9
+ type MaybeSignal<T extends {}> = Signal<T> | T | (() => T)
8
10
 
9
- type MaybeSignal<T extends {}> = State<T> | Computed<T> | T | ((old?: T) => T)
10
-
11
- type Watcher = () => void
12
-
13
- /* === Internals === */
14
-
15
- // Currently active watcher
16
- let active: () => void | undefined
17
-
18
- // Batching state
19
- let batchDepth = 0
20
-
21
- // Pending notifications
22
- const markQueue: Set<Watcher> = new Set()
23
-
24
- // Pending runs
25
- const runQueue: Set<() => void> = new Set()
26
-
27
- /**
28
- * Flush pending notifications and runs
29
- */
30
- const flush = () => {
31
- while (markQueue.size || runQueue.size) {
32
- markQueue.forEach(mark => mark())
33
- markQueue.clear()
34
- runQueue.forEach(run => run())
35
- runQueue.clear()
36
- }
37
- }
11
+ type SignalValue<T> = T extends Signal<infer U> ? U : never
38
12
 
39
13
  /* === Constants === */
40
14
 
@@ -67,50 +41,52 @@ const toSignal = /*#__PURE__*/ <T extends {}>(
67
41
  : isComputeFunction<T>(value) ? computed(value)
68
42
  : state(value)
69
43
 
70
- /**
71
- * Add notify function of active watchers to the set of watchers
72
- *
73
- * @param {Watcher[]} watchers - set of current watchers
74
- */
75
- const subscribe = (watchers: Watcher[]) => {
76
- if (active && !watchers.includes(active)) watchers.push(active)
77
- }
78
44
 
79
45
  /**
80
- * Notify all subscribers of the state change or add to the pending set if batching is enabled
46
+ * Resolve signals and apply callbacks based on the results
81
47
  *
82
- * @param {Watcher[]} watchers
48
+ * @since 0.12.0
49
+ * @param {U} signals - dependency signals
50
+ * @param {Record<string, (...args) => T | Promise<T> | Error | void>} callbacks - ok, nil, err callbacks
51
+ * @returns {T | Promise<T> | Error | void} - result of chosen callback
83
52
  */
84
- const notify = (watchers: Watcher[]) => {
85
- watchers.forEach(mark => batchDepth ? markQueue.add(mark) : mark())
86
- }
87
-
88
- /**
89
- * Run a function in a reactive context
90
- *
91
- * @param {() => void} run - function to run the computation or effect
92
- * @param {Watcher} mark - function to be called when the state changes
93
- */
94
- const watch = (run: () => void, mark: Watcher): void => {
95
- const prev = active
96
- active = mark
97
- run()
98
- active = prev
99
- }
100
-
101
- /**
102
- * Batch multiple state changes into a single update
103
- *
104
- * @param {() => void} run - function to run the batch of state changes
105
- */
106
- const batch = (run: () => void): void => {
107
- batchDepth++
108
- run()
109
- batchDepth--
110
- if (!batchDepth) flush()
53
+ const resolveSignals = <T extends {}, U extends UnknownSignal[]>(
54
+ signals: U,
55
+ callbacks: {
56
+ ok: (...values: { [K in keyof U]: SignalValue<U[K]> }) => T | Promise<T> | Error | void
57
+ nil?: () => T | Promise<T> | Error | void
58
+ err?: (...errors: Error[]) => T | Promise<T> | Error | void
59
+ }
60
+ ): T | Promise<T> | Error | void => {
61
+ const { ok, nil, err } = callbacks
62
+ const values = [] as { [K in keyof U]: SignalValue<U[K]> }
63
+ const errors: Error[] = []
64
+ let hasUnset = false
65
+
66
+ for (const signal of signals) {
67
+ try {
68
+ const value = signal.get()
69
+ if (value === UNSET) hasUnset = true
70
+ values.push(value)
71
+ } catch (e) {
72
+ errors.push(toError(e))
73
+ }
74
+ }
75
+
76
+ let result: T | Promise<T> | Error | void = undefined
77
+ try {
78
+ if (hasUnset && nil) result = nil()
79
+ else if (errors.length) result = err ? err(...errors) : errors[0]
80
+ else if (!hasUnset) result = ok(...values)
81
+ } catch (e) {
82
+ result = toError(e)
83
+ if (err) result = err(result)
84
+ } finally {
85
+ return result
86
+ }
111
87
  }
112
88
 
113
89
  export {
114
- type Signal, type MaybeSignal, type Watcher,
115
- isSignal, toSignal, subscribe, notify, watch, batch
90
+ type Signal, type UnknownSignal, type SignalValue, type MaybeSignal,
91
+ isSignal, toSignal, resolveSignals,
116
92
  }
package/lib/state.d.ts CHANGED
@@ -1,58 +1,21 @@
1
- import { type Computed } from "./computed";
2
- /**
3
- * Define a reactive state
4
- *
5
- * @since 0.9.0
6
- * @class State
7
- */
8
- export declare class State<T extends {}> {
9
- private value;
10
- private watchers;
11
- constructor(value: T);
12
- /**
13
- * Get the current value of the state
14
- *
15
- * @since 0.9.0
16
- * @method of State<T>
17
- * @returns {T} - current value of the state
18
- */
1
+ import { type Computed } from './computed';
2
+ import { type EffectCallbacks } from './effect';
3
+ export type State<T extends {}> = {
4
+ [Symbol.toStringTag]: 'State';
19
5
  get(): T;
20
- /**
21
- * Set a new value of the state
22
- *
23
- * @since 0.9.0
24
- * @method of State<T>
25
- * @param {T} value
26
- * @returns {void}
27
- */
28
6
  set(value: T): void;
29
- /**
30
- * Update the state with a new value using a function
31
- *
32
- * @since 0.10.0
33
- * @method of State<T>
34
- * @param {(value: T) => T} fn
35
- * @returns {void} - updates the state with the result of the function
36
- */
37
7
  update(fn: (value: T) => T): void;
38
- /**
39
- * Create a derived state from an existing state
40
- *
41
- * @since 0.9.0
42
- * @method of State<T>
43
- * @param {(value: T) => U} fn
44
- * @returns {Computed<U>} - derived state
45
- */
46
8
  map<U extends {}>(fn: (value: T) => U): Computed<U>;
47
- }
9
+ match: (callbacks: EffectCallbacks<[State<T>]>) => void;
10
+ };
48
11
  /**
49
12
  * Create a new state signal
50
13
  *
51
14
  * @since 0.9.0
52
- * @param {T} value - initial value of the state
15
+ * @param {T} initialValue - initial value of the state
53
16
  * @returns {State<T>} - new state signal
54
17
  */
55
- export declare const state: <T extends {}>(value: T) => State<T>;
18
+ export declare const state: <T extends {}>(v: T) => State<T>;
56
19
  /**
57
20
  * Check if the provided value is a State instance
58
21
  *
package/lib/state.ts CHANGED
@@ -1,84 +1,108 @@
1
- import { type Watcher, subscribe, notify, UNSET } from "./signal"
2
- import { type Computed, computed } from "./computed"
1
+ import { UNSET } from './signal'
2
+ import { type Computed, computed } from './computed'
3
+ import { isObjectOfType } from './util';
4
+ import { type Watcher, notify, subscribe } from './scheduler'
5
+ import { type EffectCallbacks, effect } from './effect';
3
6
 
4
- /* === Class State === */
7
+ /* === Types === */
8
+
9
+ export type State<T extends {}> = {
10
+ [Symbol.toStringTag]: 'State';
11
+ get(): T;
12
+ set(value: T): void;
13
+ update(fn: (value: T) => T): void;
14
+ map<U extends {}>(fn: (value: T) => U): Computed<U>;
15
+ match: (callbacks: EffectCallbacks<[State<T>]>) => void
16
+ }
17
+
18
+ /* === Constants === */
19
+
20
+ const TYPE_STATE = 'State'
21
+
22
+ /* === State Factory === */
5
23
 
6
24
  /**
7
- * Define a reactive state
25
+ * Create a new state signal
8
26
  *
9
27
  * @since 0.9.0
10
- * @class State
28
+ * @param {T} initialValue - initial value of the state
29
+ * @returns {State<T>} - new state signal
11
30
  */
12
- export class State<T extends {}> {
13
- private watchers: Watcher[] = []
31
+ export const state = /*#__PURE__*/ <T extends {}>(v: T): State<T> => {
32
+ const watchers: Watcher[] = []
33
+ let value: T = v
14
34
 
15
- constructor(private value: T) {}
35
+ const s: State<T> = {
36
+ [Symbol.toStringTag]: TYPE_STATE,
16
37
 
17
- /**
18
- * Get the current value of the state
19
- *
20
- * @since 0.9.0
21
- * @method of State<T>
22
- * @returns {T} - current value of the state
23
- */
24
- get(): T {
25
- subscribe(this.watchers)
26
- return this.value
27
- }
38
+ /**
39
+ * Get the current value of the state
40
+ *
41
+ * @since 0.9.0
42
+ * @method of State<T>
43
+ * @returns {T} - current value of the state
44
+ */
45
+ get: (): T => {
46
+ subscribe(watchers)
47
+ return value
48
+ },
28
49
 
29
- /**
30
- * Set a new value of the state
31
- *
32
- * @since 0.9.0
33
- * @method of State<T>
34
- * @param {T} value
35
- * @returns {void}
36
- */
37
- set(value: T): void {
38
- if (Object.is(this.value, value)) return
39
- this.value = value
40
- notify(this.watchers)
50
+ /**
51
+ * Set a new value of the state
52
+ *
53
+ * @since 0.9.0
54
+ * @method of State<T>
55
+ * @param {T} v
56
+ * @returns {void}
57
+ */
58
+ set: (v: T): void => {
59
+ if (Object.is(value, v)) return
60
+ value = v
61
+ notify(watchers)
41
62
 
42
- // Setting to UNSET clears the watchers so the signal can be garbage collected
43
- if (UNSET === value) this.watchers = []
44
- }
63
+ // Setting to UNSET clears the watchers so the signal can be garbage collected
64
+ if (UNSET === value) watchers.length = 0 // head = tail = undefined
65
+ },
45
66
 
46
- /**
47
- * Update the state with a new value using a function
48
- *
49
- * @since 0.10.0
50
- * @method of State<T>
51
- * @param {(value: T) => T} fn
52
- * @returns {void} - updates the state with the result of the function
53
- */
54
- update(fn: (value: T) => T): void {
55
- this.set(fn(this.value))
56
- }
67
+ /**
68
+ * Update the state with a new value using a function
69
+ *
70
+ * @since 0.10.0
71
+ * @method of State<T>
72
+ * @param {(v: T) => T} fn
73
+ * @returns {void} - updates the state with the result of the function
74
+ */
75
+ update: (fn: (v: T) => T): void => {
76
+ s.set(fn(value))
77
+ },
57
78
 
58
- /**
59
- * Create a derived state from an existing state
60
- *
61
- * @since 0.9.0
62
- * @method of State<T>
63
- * @param {(value: T) => U} fn
64
- * @returns {Computed<U>} - derived state
65
- */
66
- map<U extends {}>(fn: (value: T) => U): Computed<U> {
67
- return computed<U>(() => fn(this.get()))
68
- }
69
- }
79
+ /**
80
+ * Create a computed signal from the current state signal
81
+ *
82
+ * @since 0.9.0
83
+ * @method of State<T>
84
+ * @param {(v: T) => R} fn
85
+ * @returns {Computed<R>} - computed signal
86
+ */
87
+ map: <R extends {}>(fn: (v: T) => R): Computed<R> =>
88
+ computed(() => fn(s.get())),
70
89
 
71
- /* === Helper Functions === */
90
+ /**
91
+ * Case matching for the state signal with effect callbacks
92
+ *
93
+ * @since 0.12.0
94
+ * @method of State<T>
95
+ * @param {EffectCallbacks[<T>]} callbacks
96
+ * @returns {State<T>} - self, for chaining effect callbacks
97
+ */
98
+ match: (callbacks: EffectCallbacks<[State<T>]>): State<T> => {
99
+ effect(callbacks, s)
100
+ return s
101
+ }
102
+ }
72
103
 
73
- /**
74
- * Create a new state signal
75
- *
76
- * @since 0.9.0
77
- * @param {T} value - initial value of the state
78
- * @returns {State<T>} - new state signal
79
- */
80
- export const state = /*#__PURE__*/ <T extends {}>(value: T): State<T> =>
81
- new State(value)
104
+ return s
105
+ }
82
106
 
83
107
  /**
84
108
  * Check if the provided value is a State instance
@@ -88,4 +112,4 @@ export const state = /*#__PURE__*/ <T extends {}>(value: T): State<T> =>
88
112
  * @returns {boolean} - true if the value is a State instance, false otherwise
89
113
  */
90
114
  export const isState = /*#__PURE__*/ <T extends {}>(value: unknown): value is State<T> =>
91
- value instanceof State
115
+ isObjectOfType(value, TYPE_STATE)
package/lib/util.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  declare const isFunction: <T>(value: unknown) => value is (...args: unknown[]) => T;
2
2
  declare const isAsyncFunction: <T>(value: unknown) => value is (...args: unknown[]) => Promise<T> | PromiseLike<T>;
3
3
  declare const isComputeFunction: <T>(value: unknown) => value is ((old?: T) => T);
4
+ declare const isObjectOfType: <T>(value: unknown, type: string) => value is T;
4
5
  declare const isInstanceOf: <T>(type: new (...args: any[]) => T) => (value: unknown) => value is T;
5
6
  declare const isError: (value: unknown) => value is Error;
6
7
  declare const isPromise: (value: unknown) => value is Promise<unknown>;
7
8
  declare const toError: (value: unknown) => Error;
8
- export { isFunction, isAsyncFunction, isComputeFunction, isInstanceOf, isError, isPromise, toError };
9
+ declare const isEquivalentError: (error1: Error, error2: Error | undefined) => boolean;
10
+ export { isFunction, isAsyncFunction, isComputeFunction, isObjectOfType, isInstanceOf, isError, isPromise, toError, isEquivalentError };
package/lib/util.ts CHANGED
@@ -9,6 +9,9 @@ const isAsyncFunction = /*#__PURE__*/ <T>(value: unknown): value is (...args: un
9
9
  const isComputeFunction = /*#__PURE__*/ <T>(value: unknown): value is ((old?: T) => T) =>
10
10
  isFunction(value) && value.length < 2
11
11
 
12
+ const isObjectOfType = <T>(value: unknown, type: string): value is T =>
13
+ Object.prototype.toString.call(value) === `[object ${type}]`
14
+
12
15
  const isInstanceOf = /*#__PURE__*/ <T>(type: new (...args: any[]) => T) =>
13
16
  (value: unknown): value is T =>
14
17
  value instanceof type
@@ -19,7 +22,15 @@ const isPromise = /*#__PURE__*/ isInstanceOf(Promise)
19
22
  const toError = (value: unknown): Error =>
20
23
  isError(value) ? value : new Error(String(value))
21
24
 
25
+ const isEquivalentError = /*#__PURE__*/ (
26
+ error1: Error,
27
+ error2: Error | undefined
28
+ ): boolean => {
29
+ if (!error2) return false
30
+ return error1.name === error2.name && error1.message === error2.message
31
+ }
32
+
22
33
  export {
23
34
  isFunction, isAsyncFunction, isComputeFunction,
24
- isInstanceOf, isError, isPromise, toError
35
+ isObjectOfType, isInstanceOf, isError, isPromise, toError, isEquivalentError
25
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeix/cause-effect",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "author": "Esther Brunner",
5
5
  "main": "index.js",
6
6
  "module": "index.ts",
@@ -26,5 +26,8 @@
26
26
  "test": "bun test"
27
27
  },
28
28
  "type": "module",
29
- "types": "index.d.ts"
29
+ "types": "index.d.ts",
30
+ "dependencies": {
31
+ "random": "^5.3.0"
32
+ }
30
33
  }