@zeix/cause-effect 0.13.2 → 0.14.1

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