@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/computed.ts CHANGED
@@ -1,220 +1,36 @@
1
- import { type Signal, type ComputedCallback, match, UNSET } from './signal'
2
- import {
3
- CircularDependencyError,
4
- isAbortError,
5
- isAsyncFunction,
6
- isFunction,
7
- isObjectOfType,
8
- isPromise,
9
- toError,
10
- } from './util'
11
- import { type Watcher, flush, notify, subscribe, watch } from './scheduler'
12
- import { type TapMatcher, type EffectMatcher, effect } from './effect'
1
+ import { isAsyncFunction, isObjectOfType } from './util'
2
+ import { type MemoCallback, memo } from './memo'
3
+ import { type TaskCallback, task } from './task'
13
4
 
14
5
  /* === Types === */
15
6
 
16
- export type ComputedMatcher<S extends Signal<{}>[], R extends {}> = {
17
- signals: S
18
- abort?: AbortSignal
19
- ok: (
20
- ...values: {
21
- [K in keyof S]: S[K] extends Signal<infer T> ? T : never
22
- }
23
- ) => R | Promise<R>
24
- err?: (...errors: Error[]) => R | Promise<R>
25
- nil?: () => R | Promise<R>
26
- }
27
-
28
- export type Computed<T extends {}> = {
7
+ type Computed<T extends {}> = {
29
8
  [Symbol.toStringTag]: 'Computed'
30
9
  get(): T
31
- map<U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U>
32
- tap(matcher: TapMatcher<T> | ((v: T) => void | (() => void))): () => void
33
10
  }
11
+ type ComputedCallback<T extends {} & { then?: void }> =
12
+ | TaskCallback<T>
13
+ | MemoCallback<T>
34
14
 
35
15
  /* === Constants === */
36
16
 
37
17
  const TYPE_COMPUTED = 'Computed'
38
18
 
39
- /* === Private Functions === */
40
-
41
- const isEquivalentError = /*#__PURE__*/ (
42
- error1: Error,
43
- error2: Error | undefined,
44
- ): boolean => {
45
- if (!error2) return false
46
- return error1.name === error2.name && error1.message === error2.message
47
- }
48
-
49
- /* === Computed Factory === */
19
+ /* === Functions === */
50
20
 
51
21
  /**
52
22
  * Create a derived signal from existing signals
53
23
  *
24
+ * This function delegates to either memo() for synchronous computations
25
+ * or task() for asynchronous computations, providing better performance
26
+ * for each case.
27
+ *
54
28
  * @since 0.9.0
55
- * @param {ComputedMatcher<S, T> | ComputedCallback<T>} matcher - computed matcher or callback
29
+ * @param {ComputedCallback<T>} fn - computation callback function
56
30
  * @returns {Computed<T>} - Computed signal
57
31
  */
58
- export const computed = <T extends {}, S extends Signal<{}>[] = []>(
59
- matcher: ComputedMatcher<S, T> | ComputedCallback<T>,
60
- ): Computed<T> => {
61
- const watchers: Set<Watcher> = new Set()
62
- const m = isFunction(matcher)
63
- ? undefined
64
- : ({
65
- nil: () => UNSET,
66
- err: (...errors: Error[]) => {
67
- if (errors.length > 1) throw new AggregateError(errors)
68
- else throw errors[0]
69
- },
70
- ...matcher,
71
- } as Required<ComputedMatcher<S, T>>)
72
- const fn = (m ? m.ok : matcher) as ComputedCallback<T>
73
-
74
- // Internal state
75
- let value: T = UNSET
76
- let error: Error | undefined
77
- let dirty = true
78
- let changed = false
79
- let computing = false
80
- let controller: AbortController | undefined
81
-
82
- // Functions to update internal state
83
- const ok = (v: T) => {
84
- if (!Object.is(v, value)) {
85
- value = v
86
- dirty = false
87
- error = undefined
88
- changed = true
89
- }
90
- }
91
- const nil = () => {
92
- changed = UNSET !== value
93
- value = UNSET
94
- error = undefined
95
- }
96
- const err = (e: unknown) => {
97
- const newError = toError(e)
98
- changed = !isEquivalentError(newError, error)
99
- value = UNSET
100
- error = newError
101
- }
102
- const resolve = (v: T) => {
103
- computing = false
104
- controller = undefined
105
- ok(v)
106
- if (changed) notify(watchers)
107
- }
108
- const reject = (e: unknown) => {
109
- computing = false
110
- controller = undefined
111
- err(e)
112
- if (changed) notify(watchers)
113
- }
114
- const abort = () => {
115
- computing = false
116
- controller = undefined
117
- compute() // retry
118
- }
119
-
120
- // Called when notified from sources (push)
121
- const mark = (() => {
122
- dirty = true
123
- controller?.abort('Aborted because source signal changed')
124
- if (watchers.size) {
125
- notify(watchers)
126
- } else {
127
- mark.cleanups.forEach((fn: () => void) => fn())
128
- mark.cleanups.clear()
129
- }
130
- }) as Watcher
131
- mark.cleanups = new Set()
132
-
133
- // Called when requested by dependencies (pull)
134
- const compute = () =>
135
- watch(() => {
136
- if (computing) throw new CircularDependencyError('computed')
137
- changed = false
138
- if (isAsyncFunction(fn)) {
139
- if (controller) return value // return current value until promise resolves
140
- controller = new AbortController()
141
- if (m)
142
- m.abort =
143
- m.abort instanceof AbortSignal
144
- ? AbortSignal.any([m.abort, controller.signal])
145
- : controller.signal
146
- controller.signal.addEventListener('abort', abort, {
147
- once: true,
148
- })
149
- }
150
- let result: T | Promise<T>
151
- computing = true
152
- try {
153
- result =
154
- m && m.signals.length
155
- ? match<S, T>(m)
156
- : fn(controller?.signal)
157
- } catch (e) {
158
- if (isAbortError(e)) nil()
159
- else err(e)
160
- computing = false
161
- return
162
- }
163
- if (isPromise(result)) result.then(resolve, reject)
164
- else if (null == result || UNSET === result) nil()
165
- else ok(result)
166
- computing = false
167
- }, mark)
168
-
169
- const c: Computed<T> = {
170
- [Symbol.toStringTag]: TYPE_COMPUTED,
171
-
172
- /**
173
- * Get the current value of the computed
174
- *
175
- * @since 0.9.0
176
- * @returns {T} - current value of the computed
177
- */
178
- get: (): T => {
179
- subscribe(watchers)
180
- flush()
181
- if (dirty) compute()
182
- if (error) throw error
183
- return value
184
- },
185
-
186
- /**
187
- * Create a computed signal from the current computed signal
188
- *
189
- * @since 0.9.0
190
- * @param {((v: T) => U | Promise<U>)} fn - computed callback
191
- * @returns {Computed<U>} - computed signal
192
- */
193
- map: <U extends {}>(fn: (v: T) => U | Promise<U>): Computed<U> =>
194
- computed({
195
- signals: [c],
196
- ok: fn,
197
- }),
198
-
199
- /**
200
- * Case matching for the computed signal with effect callbacks
201
- *
202
- * @since 0.13.0
203
- * @param {TapMatcher<T> | ((v: T) => void | (() => void))} matcher - tap matcher or effect callback
204
- * @returns {() => void} - cleanup function for the effect
205
- */
206
- tap: (
207
- matcher: TapMatcher<T> | ((v: T) => void | (() => void)),
208
- ): (() => void) =>
209
- effect({
210
- signals: [c],
211
- ...(isFunction(matcher) ? { ok: matcher } : matcher),
212
- } as EffectMatcher<[Computed<T>]>),
213
- }
214
- return c
215
- }
216
-
217
- /* === Helper Functions === */
32
+ const computed = <T extends {}>(fn: ComputedCallback<T>): Computed<T> =>
33
+ isAsyncFunction<T>(fn) ? task<T>(fn) : memo<T>(fn as MemoCallback<T>)
218
34
 
219
35
  /**
220
36
  * Check if a value is a computed state
@@ -223,6 +39,16 @@ export const computed = <T extends {}, S extends Signal<{}>[] = []>(
223
39
  * @param {unknown} value - value to check
224
40
  * @returns {boolean} - true if value is a computed state, false otherwise
225
41
  */
226
- export const isComputed = /*#__PURE__*/ <T extends {}>(
42
+ const isComputed = /*#__PURE__*/ <T extends {}>(
227
43
  value: unknown,
228
44
  ): value is Computed<T> => isObjectOfType(value, TYPE_COMPUTED)
45
+
46
+ /* === Exports === */
47
+
48
+ export {
49
+ type Computed,
50
+ type ComputedCallback,
51
+ TYPE_COMPUTED,
52
+ computed,
53
+ isComputed,
54
+ }
package/src/effect.d.ts CHANGED
@@ -1,22 +1,19 @@
1
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<{}>[]> = {
2
+ import { type Cleanup } from './scheduler';
3
+ type EffectMatcher<S extends Signal<{}>[]> = {
8
4
  signals: S;
9
5
  ok: (...values: {
10
6
  [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);
7
+ }) => void | Cleanup;
8
+ err?: (...errors: Error[]) => void | Cleanup;
9
+ nil?: () => void | Cleanup;
14
10
  };
15
11
  /**
16
12
  * Define what happens when a reactive state changes
17
13
  *
18
14
  * @since 0.1.0
19
- * @param {EffectMatcher<S> | (() => void | (() => void))} matcher - effect matcher or callback
20
- * @returns {() => void} - cleanup function for the effect
15
+ * @param {EffectMatcher<S> | (() => void | Cleanup)} matcher - effect matcher or callback
16
+ * @returns {Cleanup} - cleanup function for the effect
21
17
  */
22
- export declare function effect<S extends Signal<{}>[]>(matcher: EffectMatcher<S> | (() => void | (() => void))): () => void;
18
+ declare function effect<S extends Signal<{}>[]>(matcher: EffectMatcher<S> | (() => void | Cleanup)): Cleanup;
19
+ export { type EffectMatcher, effect };
package/src/effect.ts CHANGED
@@ -1,38 +1,37 @@
1
- import { type Signal, match } from './signal'
2
- import { CircularDependencyError, isFunction, toError } from './util'
3
- import { watch, type Watcher } from './scheduler'
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'
4
9
 
5
10
  /* === Types === */
6
11
 
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<{}>[]> = {
12
+ type EffectMatcher<S extends Signal<{}>[]> = {
14
13
  signals: S
15
14
  ok: (
16
15
  ...values: {
17
16
  [K in keyof S]: S[K] extends Signal<infer T> ? T : never
18
17
  }
19
- ) => void | (() => void)
20
- err?: (...errors: Error[]) => void | (() => void)
21
- nil?: () => void | (() => void)
18
+ ) => void | Cleanup
19
+ err?: (...errors: Error[]) => void | Cleanup
20
+ nil?: () => void | Cleanup
22
21
  }
23
22
 
24
- /* === Exported Functions === */
23
+ /* === Functions === */
25
24
 
26
25
  /**
27
26
  * Define what happens when a reactive state changes
28
27
  *
29
28
  * @since 0.1.0
30
- * @param {EffectMatcher<S> | (() => void | (() => void))} matcher - effect matcher or callback
31
- * @returns {() => void} - cleanup function for the effect
29
+ * @param {EffectMatcher<S> | (() => void | Cleanup)} matcher - effect matcher or callback
30
+ * @returns {Cleanup} - cleanup function for the effect
32
31
  */
33
- export function effect<S extends Signal<{}>[]>(
34
- matcher: EffectMatcher<S> | (() => void | (() => void)),
35
- ): () => void {
32
+ function effect<S extends Signal<{}>[]>(
33
+ matcher: EffectMatcher<S> | (() => void | Cleanup),
34
+ ): Cleanup {
36
35
  const {
37
36
  signals,
38
37
  ok,
@@ -46,24 +45,51 @@ export function effect<S extends Signal<{}>[]>(
46
45
  watch(() => {
47
46
  if (running) throw new CircularDependencyError('effect')
48
47
  running = true
49
- let cleanup: void | (() => void) = undefined
48
+ let cleanup: void | Cleanup = undefined
50
49
  try {
51
- cleanup = match<S, void | (() => void)>({
52
- signals,
53
- ok,
54
- err,
55
- nil,
56
- }) as void | (() => void)
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
+
68
+ try {
69
+ cleanup = suspense
70
+ ? nil()
71
+ : errors.length
72
+ ? err(...errors)
73
+ : ok(...values)
74
+ } catch (e) {
75
+ if (isAbortError(e)) throw e
76
+ const error = toError(e)
77
+ cleanup = err(error)
78
+ }
57
79
  } catch (e) {
58
80
  err(toError(e))
59
81
  }
60
82
  if (isFunction(cleanup)) run.cleanups.add(cleanup)
61
83
  running = false
62
84
  }, run)) as Watcher
63
- run.cleanups = new Set()
85
+ run.cleanups = new Set<Cleanup>()
64
86
  run()
65
87
  return () => {
66
- run.cleanups.forEach((fn: () => void) => fn())
88
+ run.cleanups.forEach(fn => fn())
67
89
  run.cleanups.clear()
68
90
  }
69
91
  }
92
+
93
+ /* === Exports === */
94
+
95
+ export { type EffectMatcher, effect }
package/src/memo.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { type Computed } from './computed';
2
+ type MemoCallback<T extends {} & {
3
+ then?: void;
4
+ }> = () => T;
5
+ /**
6
+ * Create a derived signal for synchronous computations
7
+ *
8
+ * @since 0.14.0
9
+ * @param {MemoCallback<T>} fn - synchronous computation callback
10
+ * @returns {Computed<T>} - Computed signal
11
+ */
12
+ declare const memo: <T extends {}>(fn: MemoCallback<T>) => Computed<T>;
13
+ export { type MemoCallback, memo };
package/src/memo.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { UNSET } from './signal'
2
+ import { CircularDependencyError } from './util'
3
+ import {
4
+ type Cleanup,
5
+ type Watcher,
6
+ flush,
7
+ notify,
8
+ subscribe,
9
+ watch,
10
+ } from './scheduler'
11
+ import { type Computed, TYPE_COMPUTED } from './computed'
12
+
13
+ /* === Types === */
14
+
15
+ type MemoCallback<T extends {} & { then?: void }> = () => T
16
+
17
+ /* === Functions === */
18
+
19
+ /**
20
+ * Create a derived signal for synchronous computations
21
+ *
22
+ * @since 0.14.0
23
+ * @param {MemoCallback<T>} fn - synchronous computation callback
24
+ * @returns {Computed<T>} - Computed signal
25
+ */
26
+ const memo = <T extends {}>(fn: MemoCallback<T>): Computed<T> => {
27
+ const watchers: Set<Watcher> = new Set()
28
+
29
+ // Internal state - simplified for sync only
30
+ let value: T = UNSET
31
+ let error: Error | undefined
32
+ let dirty = true
33
+ let computing = false
34
+
35
+ // Called when notified from sources (push)
36
+ const mark = (() => {
37
+ dirty = true
38
+ if (watchers.size) {
39
+ notify(watchers)
40
+ } else {
41
+ mark.cleanups.forEach(fn => fn())
42
+ mark.cleanups.clear()
43
+ }
44
+ }) as Watcher
45
+ mark.cleanups = new Set<Cleanup>()
46
+
47
+ // Called when requested by dependencies (pull)
48
+ const compute = () =>
49
+ watch(() => {
50
+ if (computing) throw new CircularDependencyError('memo')
51
+ computing = true
52
+ try {
53
+ const result = fn()
54
+ if (null == result || UNSET === result) {
55
+ value = UNSET
56
+ error = undefined
57
+ } else {
58
+ value = result
59
+ dirty = false
60
+ error = undefined
61
+ }
62
+ } catch (e) {
63
+ value = UNSET
64
+ error = e instanceof Error ? e : new Error(String(e))
65
+ } finally {
66
+ computing = false
67
+ }
68
+ }, mark)
69
+
70
+ const c: Computed<T> = {
71
+ [Symbol.toStringTag]: TYPE_COMPUTED,
72
+
73
+ /**
74
+ * Get the current value of the computed
75
+ *
76
+ * @returns {T} - current value of the computed
77
+ */
78
+ get: (): T => {
79
+ subscribe(watchers)
80
+ flush()
81
+ if (dirty) compute()
82
+ if (error) throw error
83
+ return value
84
+ },
85
+ }
86
+ return c
87
+ }
88
+
89
+ /* === Exports === */
90
+
91
+ export { type MemoCallback, memo }
@@ -1,42 +1,46 @@
1
- export type EnqueueDedupe = [Element, string];
2
- export type Watcher = {
1
+ type Cleanup = () => void;
2
+ type Watcher = {
3
3
  (): void;
4
- cleanups: Set<() => void>;
4
+ cleanups: Set<Cleanup>;
5
5
  };
6
- export type Updater = <T>() => T | boolean | void;
6
+ type Updater = <T>() => T | boolean | void;
7
7
  /**
8
8
  * Add active watcher to the Set of watchers
9
9
  *
10
10
  * @param {Set<Watcher>} watchers - watchers of the signal
11
11
  */
12
- export declare const subscribe: (watchers: Set<Watcher>) => void;
12
+ declare const subscribe: (watchers: Set<Watcher>) => void;
13
13
  /**
14
14
  * Add watchers to the pending set of change notifications
15
15
  *
16
16
  * @param {Set<Watcher>} watchers - watchers of the signal
17
17
  */
18
- export declare const notify: (watchers: Set<Watcher>) => void;
18
+ declare const notify: (watchers: Set<Watcher>) => void;
19
19
  /**
20
20
  * Flush all pending changes to notify watchers
21
21
  */
22
- export declare const flush: () => void;
22
+ declare const flush: () => void;
23
23
  /**
24
24
  * Batch multiple changes in a single signal graph and DOM update cycle
25
25
  *
26
26
  * @param {() => void} fn - function with multiple signal writes to be batched
27
27
  */
28
- export declare const batch: (fn: () => void) => void;
28
+ declare const batch: (fn: () => void) => void;
29
29
  /**
30
30
  * Run a function in a reactive context
31
31
  *
32
32
  * @param {() => void} run - function to run the computation or effect
33
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)
34
34
  */
35
- export declare const watch: (run: () => void, mark?: Watcher) => void;
35
+ declare const watch: (run: () => void, mark?: Watcher) => void;
36
36
  /**
37
37
  * Enqueue a function to be executed on the next animation frame
38
38
  *
39
+ * If the same Symbol is provided for multiple calls before the next animation frame,
40
+ * only the latest call will be executed (deduplication).
41
+ *
39
42
  * @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void
40
- * @param {EnqueueDedupe} dedupe - [element, operation] pair for deduplication
43
+ * @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed
41
44
  */
42
- export declare const enqueue: <T>(fn: Updater, dedupe: EnqueueDedupe) => Promise<boolean | void | T>;
45
+ 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, };
package/src/scheduler.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  /* === Types === */
2
2
 
3
- export type EnqueueDedupe = [Element, string]
3
+ type Cleanup = () => void
4
4
 
5
- export type Watcher = {
5
+ type Watcher = {
6
6
  (): void
7
- cleanups: Set<() => void>
7
+ cleanups: Set<Cleanup>
8
8
  }
9
9
 
10
- export type Updater = <T>() => T | boolean | void
10
+ type Updater = <T>() => T | boolean | void
11
11
 
12
12
  /* === Internal === */
13
13
 
@@ -18,8 +18,8 @@ let active: Watcher | undefined
18
18
  const pending = new Set<Watcher>()
19
19
  let batchDepth = 0
20
20
 
21
- // Map of DOM elements to update functions
22
- const updateMap = new Map<EnqueueDedupe, () => void>()
21
+ // Map of deduplication symbols to update functions (using Symbol keys prevents unintended overwrites)
22
+ const updateMap = new Map<symbol, Updater>()
23
23
  let requestId: number | undefined
24
24
 
25
25
  const updateDOM = () => {
@@ -39,14 +39,14 @@ const requestTick = () => {
39
39
  // Initial render when the call stack is empty
40
40
  queueMicrotask(updateDOM)
41
41
 
42
- /* === Exported Functions === */
42
+ /* === Functions === */
43
43
 
44
44
  /**
45
45
  * Add active watcher to the Set of watchers
46
46
  *
47
47
  * @param {Set<Watcher>} watchers - watchers of the signal
48
48
  */
49
- export const subscribe = (watchers: Set<Watcher>) => {
49
+ const subscribe = (watchers: Set<Watcher>) => {
50
50
  // if (!active) console.warn('Calling .get() outside of a reactive context')
51
51
  if (active && !watchers.has(active)) {
52
52
  const watcher = active
@@ -62,7 +62,7 @@ export const subscribe = (watchers: Set<Watcher>) => {
62
62
  *
63
63
  * @param {Set<Watcher>} watchers - watchers of the signal
64
64
  */
65
- export const notify = (watchers: Set<Watcher>) => {
65
+ const notify = (watchers: Set<Watcher>) => {
66
66
  for (const mark of watchers) {
67
67
  if (batchDepth) pending.add(mark)
68
68
  else mark()
@@ -72,7 +72,7 @@ export const notify = (watchers: Set<Watcher>) => {
72
72
  /**
73
73
  * Flush all pending changes to notify watchers
74
74
  */
75
- export const flush = () => {
75
+ const flush = () => {
76
76
  while (pending.size) {
77
77
  const watchers = Array.from(pending)
78
78
  pending.clear()
@@ -87,7 +87,7 @@ export const flush = () => {
87
87
  *
88
88
  * @param {() => void} fn - function with multiple signal writes to be batched
89
89
  */
90
- export const batch = (fn: () => void) => {
90
+ const batch = (fn: () => void) => {
91
91
  batchDepth++
92
92
  try {
93
93
  fn()
@@ -103,7 +103,7 @@ export const batch = (fn: () => void) => {
103
103
  * @param {() => void} run - function to run the computation or effect
104
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)
105
105
  */
106
- export const watch = (run: () => void, mark?: Watcher): void => {
106
+ const watch = (run: () => void, mark?: Watcher): void => {
107
107
  const prev = active
108
108
  active = mark
109
109
  try {
@@ -116,12 +116,15 @@ export const watch = (run: () => void, mark?: Watcher): void => {
116
116
  /**
117
117
  * Enqueue a function to be executed on the next animation frame
118
118
  *
119
+ * If the same Symbol is provided for multiple calls before the next animation frame,
120
+ * only the latest call will be executed (deduplication).
121
+ *
119
122
  * @param {Updater} fn - function to be executed on the next animation frame; can return updated value <T>, success <boolean> or void
120
- * @param {EnqueueDedupe} dedupe - [element, operation] pair for deduplication
123
+ * @param {symbol} dedupe - Symbol for deduplication; if not provided, a unique Symbol is created ensuring the update is always executed
121
124
  */
122
- export const enqueue = <T>(fn: Updater, dedupe: EnqueueDedupe) =>
125
+ const enqueue = <T>(fn: Updater, dedupe?: symbol) =>
123
126
  new Promise<T | boolean | void>((resolve, reject) => {
124
- updateMap.set(dedupe, () => {
127
+ updateMap.set(dedupe || Symbol(), () => {
125
128
  try {
126
129
  resolve(fn())
127
130
  } catch (error) {
@@ -130,3 +133,17 @@ export const enqueue = <T>(fn: Updater, dedupe: EnqueueDedupe) =>
130
133
  })
131
134
  requestTick()
132
135
  })
136
+
137
+ /* === Exports === */
138
+
139
+ export {
140
+ type Cleanup,
141
+ type Watcher,
142
+ type Updater,
143
+ subscribe,
144
+ notify,
145
+ flush,
146
+ batch,
147
+ watch,
148
+ enqueue,
149
+ }