@zeix/cause-effect 0.10.1 → 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,10 @@
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;
7
+ export declare const UNSET: any;
6
8
  /**
7
9
  * Check whether a value is a Signal or not
8
10
  *
@@ -19,30 +21,18 @@ declare const isSignal: <T extends {}>(value: any) => value is Signal<T>;
19
21
  * @param memo
20
22
  * @returns {Signal<T>} - converted Signal
21
23
  */
22
- declare const toSignal: <T extends {}>(value: MaybeSignal<T>, memo?: boolean) => Signal<T>;
24
+ declare const toSignal: <T extends {}>(value: MaybeSignal<T>) => Signal<T>;
23
25
  /**
24
- * Add notify function of active watchers to the set of watchers
26
+ * Resolve signals and apply callbacks based on the results
25
27
  *
26
- * @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
27
32
  */
28
- declare const subscribe: (watchers: Watcher[]) => void;
29
- /**
30
- * Notify all subscribers of the state change or add to the pending set if batching is enabled
31
- *
32
- * @param {Watcher[]} watchers
33
- */
34
- declare const notify: (watchers: Watcher[]) => void;
35
- /**
36
- * Run a function in a reactive context
37
- *
38
- * @param {() => void} run - function to run the computation or effect
39
- * @param {Watcher} mark - function to be called when the state changes
40
- */
41
- declare const watch: (run: () => void, mark: Watcher) => void;
42
- /**
43
- * Batch multiple state changes into a single update
44
- *
45
- * @param {() => void} run - function to run the batch of state changes
46
- */
47
- declare const batch: (run: () => void) => void;
48
- 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,25 +1,18 @@
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)
11
+ type SignalValue<T> = T extends Signal<infer U> ? U : never
10
12
 
11
- type Watcher = () => void
13
+ /* === Constants === */
12
14
 
13
- /* === Internals === */
14
-
15
- // Currently active watcher
16
- let active: () => void | undefined
17
-
18
- // Batching state
19
- let batching = false
20
-
21
- // Pending notifications
22
- const pending: Watcher[] = []
15
+ export const UNSET: any = Symbol()
23
16
 
24
17
  /* === Exported Functions === */
25
18
 
@@ -42,57 +35,58 @@ const isSignal = /*#__PURE__*/ <T extends {}>(value: any): value is Signal<T> =>
42
35
  * @returns {Signal<T>} - converted Signal
43
36
  */
44
37
  const toSignal = /*#__PURE__*/ <T extends {}>(
45
- value: MaybeSignal<T>,
46
- memo: boolean = false
38
+ value: MaybeSignal<T>
47
39
  ): Signal<T> =>
48
40
  isSignal<T>(value) ? value
49
- : isComputeFunction<T>(value) ? computed(value, memo)
41
+ : isComputeFunction<T>(value) ? computed(value)
50
42
  : state(value)
51
43
 
52
- /**
53
- * Add notify function of active watchers to the set of watchers
54
- *
55
- * @param {Watcher[]} watchers - set of current watchers
56
- */
57
- const subscribe = (watchers: Watcher[]) => {
58
- if (active && !watchers.includes(active)) watchers.push(active)
59
- }
60
-
61
- /**
62
- * Notify all subscribers of the state change or add to the pending set if batching is enabled
63
- *
64
- * @param {Watcher[]} watchers
65
- */
66
- const notify = (watchers: Watcher[]) =>
67
- watchers.forEach(n => batching ? pending.push(n) : n())
68
-
69
- /**
70
- * Run a function in a reactive context
71
- *
72
- * @param {() => void} run - function to run the computation or effect
73
- * @param {Watcher} mark - function to be called when the state changes
74
- */
75
- const watch = (run: () => void, mark: Watcher): void => {
76
- const prev = active
77
- active = mark
78
- run()
79
- active = prev
80
- }
81
44
 
82
45
  /**
83
- * Batch multiple state changes into a single update
46
+ * Resolve signals and apply callbacks based on the results
84
47
  *
85
- * @param {() => void} run - function to run the batch of state changes
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
86
52
  */
87
- const batch = (run: () => void): void => {
88
- batching = true
89
- run()
90
- batching = false
91
- pending.forEach(n => n())
92
- pending.length = 0
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
+ }
93
87
  }
94
88
 
95
89
  export {
96
- type Signal, type MaybeSignal, type Watcher,
97
- isSignal, toSignal, subscribe, notify, watch, batch
90
+ type Signal, type UnknownSignal, type SignalValue, type MaybeSignal,
91
+ isSignal, toSignal, resolveSignals,
98
92
  }
package/lib/state.d.ts CHANGED
@@ -1,59 +1,21 @@
1
- import { type Computed } from "./computed";
2
- export declare const UNSET: any;
3
- /**
4
- * Define a reactive state
5
- *
6
- * @since 0.9.0
7
- * @class State
8
- */
9
- export declare class State<T extends {}> {
10
- private value;
11
- private watchers;
12
- constructor(value: T);
13
- /**
14
- * Get the current value of the state
15
- *
16
- * @since 0.9.0
17
- * @method of State<T>
18
- * @returns {T} - current value of the state
19
- */
1
+ import { type Computed } from './computed';
2
+ import { type EffectCallbacks } from './effect';
3
+ export type State<T extends {}> = {
4
+ [Symbol.toStringTag]: 'State';
20
5
  get(): T;
21
- /**
22
- * Set a new value of the state
23
- *
24
- * @since 0.9.0
25
- * @method of State<T>
26
- * @param {T} value
27
- * @returns {void}
28
- */
29
6
  set(value: T): void;
30
- /**
31
- * Update the state with a new value using a function
32
- *
33
- * @since 0.10.0
34
- * @method of State<T>
35
- * @param {(value: T) => T} fn
36
- * @returns {void} - updates the state with the result of the function
37
- */
38
7
  update(fn: (value: T) => T): void;
39
- /**
40
- * Create a derived state from an existing state
41
- *
42
- * @since 0.9.0
43
- * @method of State<T>
44
- * @param {(value: T) => U} fn
45
- * @returns {Computed<U>} - derived state
46
- */
47
8
  map<U extends {}>(fn: (value: T) => U): Computed<U>;
48
- }
9
+ match: (callbacks: EffectCallbacks<[State<T>]>) => void;
10
+ };
49
11
  /**
50
12
  * Create a new state signal
51
13
  *
52
14
  * @since 0.9.0
53
- * @param {T} value - initial value of the state
15
+ * @param {T} initialValue - initial value of the state
54
16
  * @returns {State<T>} - new state signal
55
17
  */
56
- export declare const state: <T extends {}>(value: T) => State<T>;
18
+ export declare const state: <T extends {}>(v: T) => State<T>;
57
19
  /**
58
20
  * Check if the provided value is a State instance
59
21
  *
package/lib/state.ts CHANGED
@@ -1,88 +1,108 @@
1
- import { type Watcher, subscribe, notify } 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';
6
+
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
+ }
3
17
 
4
18
  /* === Constants === */
5
19
 
6
- export const UNSET: any = Symbol()
20
+ const TYPE_STATE = 'State'
7
21
 
8
- /* === Class State === */
22
+ /* === State Factory === */
9
23
 
10
24
  /**
11
- * Define a reactive state
25
+ * Create a new state signal
12
26
  *
13
27
  * @since 0.9.0
14
- * @class State
28
+ * @param {T} initialValue - initial value of the state
29
+ * @returns {State<T>} - new state signal
15
30
  */
16
- export class State<T extends {}> {
17
- private watchers: Watcher[] = []
31
+ export const state = /*#__PURE__*/ <T extends {}>(v: T): State<T> => {
32
+ const watchers: Watcher[] = []
33
+ let value: T = v
18
34
 
19
- constructor(private value: T) {}
35
+ const s: State<T> = {
36
+ [Symbol.toStringTag]: TYPE_STATE,
20
37
 
21
- /**
22
- * Get the current value of the state
23
- *
24
- * @since 0.9.0
25
- * @method of State<T>
26
- * @returns {T} - current value of the state
27
- */
28
- get(): T {
29
- subscribe(this.watchers)
30
- return this.value
31
- }
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
+ },
32
49
 
33
- /**
34
- * Set a new value of the state
35
- *
36
- * @since 0.9.0
37
- * @method of State<T>
38
- * @param {T} value
39
- * @returns {void}
40
- */
41
- set(value: T): void {
42
- if (Object.is(this.value, value)) return
43
- this.value = value
44
- 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)
45
62
 
46
- // Setting to UNSET clears the watchers so the signal can be garbage collected
47
- if (UNSET === value) this.watchers = []
48
- }
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
+ },
49
66
 
50
- /**
51
- * Update the state with a new value using a function
52
- *
53
- * @since 0.10.0
54
- * @method of State<T>
55
- * @param {(value: T) => T} fn
56
- * @returns {void} - updates the state with the result of the function
57
- */
58
- update(fn: (value: T) => T): void {
59
- this.set(fn(this.value))
60
- }
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
+ },
61
78
 
62
- /**
63
- * Create a derived state from an existing state
64
- *
65
- * @since 0.9.0
66
- * @method of State<T>
67
- * @param {(value: T) => U} fn
68
- * @returns {Computed<U>} - derived state
69
- */
70
- map<U extends {}>(fn: (value: T) => U): Computed<U> {
71
- return computed<U>(() => fn(this.get()))
72
- }
73
- }
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())),
74
89
 
75
- /* === 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
+ }
76
103
 
77
- /**
78
- * Create a new state signal
79
- *
80
- * @since 0.9.0
81
- * @param {T} value - initial value of the state
82
- * @returns {State<T>} - new state signal
83
- */
84
- export const state = /*#__PURE__*/ <T extends {}>(value: T): State<T> =>
85
- new State(value)
104
+ return s
105
+ }
86
106
 
87
107
  /**
88
108
  * Check if the provided value is a State instance
@@ -92,4 +112,4 @@ export const state = /*#__PURE__*/ <T extends {}>(value: T): State<T> =>
92
112
  * @returns {boolean} - true if the value is a State instance, false otherwise
93
113
  */
94
114
  export const isState = /*#__PURE__*/ <T extends {}>(value: unknown): value is State<T> =>
95
- value instanceof State
115
+ isObjectOfType(value, TYPE_STATE)
package/lib/util.d.ts CHANGED
@@ -1,7 +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
- export { isFunction, isAsyncFunction, isComputeFunction, isInstanceOf, isError, isPromise };
8
+ declare const toError: (value: unknown) => Error;
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
@@ -16,4 +19,18 @@ const isInstanceOf = /*#__PURE__*/ <T>(type: new (...args: any[]) => T) =>
16
19
  const isError = /*#__PURE__*/ isInstanceOf(Error)
17
20
  const isPromise = /*#__PURE__*/ isInstanceOf(Promise)
18
21
 
19
- export { isFunction, isAsyncFunction, isComputeFunction, isInstanceOf, isError, isPromise }
22
+ const toError = (value: unknown): Error =>
23
+ isError(value) ? value : new Error(String(value))
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
+
33
+ export {
34
+ isFunction, isAsyncFunction, isComputeFunction,
35
+ isObjectOfType, isInstanceOf, isError, isPromise, toError, isEquivalentError
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeix/cause-effect",
3
- "version": "0.10.1",
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
  }