@zeix/cause-effect 0.14.1 → 0.15.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.
Files changed (45) hide show
  1. package/README.md +256 -27
  2. package/biome.json +35 -0
  3. package/index.d.ts +32 -7
  4. package/index.dev.js +629 -0
  5. package/index.js +1 -1
  6. package/index.ts +41 -21
  7. package/package.json +6 -7
  8. package/src/computed.ts +30 -21
  9. package/src/diff.ts +136 -0
  10. package/src/effect.ts +59 -49
  11. package/src/match.ts +57 -0
  12. package/src/resolve.ts +58 -0
  13. package/src/scheduler.ts +3 -3
  14. package/src/signal.ts +48 -15
  15. package/src/state.ts +4 -3
  16. package/src/store.ts +325 -0
  17. package/src/util.ts +57 -5
  18. package/test/batch.test.ts +29 -25
  19. package/test/benchmark.test.ts +81 -45
  20. package/test/computed.test.ts +43 -39
  21. package/test/diff.test.ts +638 -0
  22. package/test/effect.test.ts +657 -49
  23. package/test/match.test.ts +378 -0
  24. package/test/resolve.test.ts +156 -0
  25. package/test/state.test.ts +33 -33
  26. package/test/store.test.ts +719 -0
  27. package/test/util/framework-types.ts +2 -2
  28. package/test/util/perf-tests.ts +2 -2
  29. package/test/util/reactive-framework.ts +1 -1
  30. package/tsconfig.json +9 -10
  31. package/types/index.d.ts +15 -0
  32. package/{src → types/src}/computed.d.ts +2 -2
  33. package/types/src/diff.d.ts +27 -0
  34. package/types/src/effect.d.ts +16 -0
  35. package/types/src/match.d.ts +21 -0
  36. package/types/src/resolve.d.ts +29 -0
  37. package/{src → types/src}/scheduler.d.ts +2 -2
  38. package/types/src/signal.d.ts +40 -0
  39. package/{src → types/src}/state.d.ts +1 -1
  40. package/types/src/store.d.ts +57 -0
  41. package/types/src/util.d.ts +15 -0
  42. package/types/test-new-effect.d.ts +1 -0
  43. package/src/effect.d.ts +0 -17
  44. package/src/signal.d.ts +0 -26
  45. package/src/util.d.ts +0 -7
package/src/signal.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { isState, state } from './state'
2
1
  import {
2
+ type Computed,
3
3
  type ComputedCallback,
4
+ computed,
4
5
  isComputed,
5
6
  isComputedCallback,
6
- computed,
7
7
  } from './computed'
8
+ import { isState, type State, state } from './state'
9
+ import { isStore, type Store, store } from './store'
10
+ import { arrayToRecord, isRecord } from './util'
8
11
 
9
12
  /* === Types === */
10
13
 
@@ -13,12 +16,13 @@ type Signal<T extends {}> = {
13
16
  }
14
17
  type MaybeSignal<T extends {}> = T | Signal<T> | ComputedCallback<T>
15
18
 
16
- type SignalValues<S extends Signal<{}>[]> = {
19
+ type SignalValues<S extends Record<string, Signal<unknown & {}>>> = {
17
20
  [K in keyof S]: S[K] extends Signal<infer T> ? T : never
18
21
  }
19
22
 
20
23
  /* === Constants === */
21
24
 
25
+ // biome-ignore lint/suspicious/noExplicitAny: Deliberately using any to be used as a placeholder value in any signal
22
26
  const UNSET: any = Symbol()
23
27
 
24
28
  /* === Functions === */
@@ -32,23 +36,52 @@ const UNSET: any = Symbol()
32
36
  */
33
37
  const isSignal = /*#__PURE__*/ <T extends {}>(
34
38
  value: unknown,
35
- ): value is Signal<T> => isState(value) || isComputed(value)
39
+ ): value is Signal<T> => isState(value) || isComputed(value) || isStore(value)
36
40
 
37
41
  /**
38
42
  * Convert a value to a Signal if it's not already a Signal
39
43
  *
40
44
  * @since 0.9.6
41
- * @param {MaybeSignal<T>} value - value to convert to a Signal
42
- * @returns {Signal<T>} - converted Signal
43
45
  */
44
- const toSignal = /*#__PURE__*/ <T extends {}>(
45
- value: MaybeSignal<T>,
46
- ): Signal<T> =>
47
- isSignal<T>(value)
48
- ? value
49
- : isComputedCallback<T>(value)
50
- ? computed(value)
51
- : state(value as T)
46
+ function toSignal<T extends Array<unknown & {}>>(
47
+ value: T[],
48
+ ): Store<Record<string, T>>
49
+ function toSignal<T extends Record<keyof T, T[keyof T]>>(value: T): Store<T>
50
+ function toSignal<T extends {}>(value: ComputedCallback<T>): Computed<T>
51
+ function toSignal<T extends {}>(value: Signal<T>): Signal<T>
52
+ function toSignal<T extends {}>(value: T): State<T>
53
+ function toSignal<T extends {}>(
54
+ value: MaybeSignal<T> | T[],
55
+ ): Signal<T> | Store<Record<string, T>> {
56
+ if (isSignal<T>(value)) return value
57
+ if (isComputedCallback<T>(value)) return computed(value)
58
+ if (Array.isArray(value)) return store(arrayToRecord(value))
59
+ if (isRecord(value)) return store(value as T)
60
+ return state(value as T)
61
+ }
62
+
63
+ /**
64
+ * Convert a value to a mutable Signal if it's not already a Signal
65
+ *
66
+ * @since 0.9.6
67
+ */
68
+ function toMutableSignal<T extends Array<unknown & {}>>(
69
+ value: T[],
70
+ ): Store<Record<string, T>>
71
+ function toMutableSignal<T extends Record<keyof T, T[keyof T]>>(
72
+ value: T,
73
+ ): Store<T>
74
+ function toMutableSignal<T extends State<T>>(value: State<T>): State<T>
75
+ function toMutableSignal<T extends Store<T>>(value: Store<T>): Store<T>
76
+ function toMutableSignal<T extends {}>(value: T): State<T>
77
+ function toMutableSignal<T extends {}>(
78
+ value: T | State<T> | Store<T> | T[],
79
+ ): Signal<T> | Store<Record<string, T>> {
80
+ if (isState<T>(value) || isStore<T>(value)) return value
81
+ if (Array.isArray(value)) return store(arrayToRecord(value))
82
+ if (isRecord(value)) return store(value as T)
83
+ return state(value as T)
84
+ }
52
85
 
53
86
  /* === Exports === */
54
87
 
@@ -58,6 +91,6 @@ export {
58
91
  type SignalValues,
59
92
  UNSET,
60
93
  isSignal,
61
- isComputedCallback,
62
94
  toSignal,
95
+ toMutableSignal,
63
96
  }
package/src/state.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import { isEqual } from './diff'
2
+ import { notify, subscribe, type Watcher } from './scheduler'
1
3
  import { UNSET } from './signal'
2
4
  import { isObjectOfType } from './util'
3
- import { type Watcher, notify, subscribe } from './scheduler'
4
5
 
5
6
  /* === Types === */
6
7
 
@@ -50,7 +51,7 @@ const state = /*#__PURE__*/ <T extends {}>(initialValue: T): State<T> => {
50
51
  * @returns {void}
51
52
  */
52
53
  set: (v: T): void => {
53
- if (Object.is(value, v)) return
54
+ if (isEqual(value, v)) return
54
55
  value = v
55
56
  notify(watchers)
56
57
 
@@ -86,4 +87,4 @@ const isState = /*#__PURE__*/ <T extends {}>(
86
87
 
87
88
  /* === Exports === */
88
89
 
89
- export { type State, TYPE_STATE, state, isState }
90
+ export { TYPE_STATE, isState, state, type State }
package/src/store.ts ADDED
@@ -0,0 +1,325 @@
1
+ import { diff, type UnknownRecord } from './diff'
2
+ import { effect } from './effect'
3
+ import {
4
+ batch,
5
+ type Cleanup,
6
+ notify,
7
+ subscribe,
8
+ type Watcher,
9
+ } from './scheduler'
10
+ import { type Signal, toMutableSignal, UNSET } from './signal'
11
+ import { type State, state } from './state'
12
+ import { hasMethod, isObjectOfType } from './util'
13
+
14
+ /* === Constants === */
15
+
16
+ const TYPE_STORE = 'Store'
17
+
18
+ /* === Types === */
19
+
20
+ interface StoreAddEvent<T extends UnknownRecord> extends CustomEvent {
21
+ type: 'store-add'
22
+ detail: Partial<T>
23
+ }
24
+
25
+ interface StoreChangeEvent<T extends UnknownRecord> extends CustomEvent {
26
+ type: 'store-change'
27
+ detail: Partial<T>
28
+ }
29
+
30
+ interface StoreRemoveEvent<T extends UnknownRecord> extends CustomEvent {
31
+ type: 'store-remove'
32
+ detail: Partial<T>
33
+ }
34
+
35
+ type StoreEventMap<T extends UnknownRecord> = {
36
+ 'store-add': StoreAddEvent<T>
37
+ 'store-change': StoreChangeEvent<T>
38
+ 'store-remove': StoreRemoveEvent<T>
39
+ }
40
+
41
+ interface StoreEventTarget<T extends UnknownRecord> extends EventTarget {
42
+ addEventListener<K extends keyof StoreEventMap<T>>(
43
+ type: K,
44
+ listener: (event: StoreEventMap<T>[K]) => void,
45
+ options?: boolean | AddEventListenerOptions,
46
+ ): void
47
+ addEventListener(
48
+ type: string,
49
+ listener: EventListenerOrEventListenerObject,
50
+ options?: boolean | AddEventListenerOptions,
51
+ ): void
52
+
53
+ removeEventListener<K extends keyof StoreEventMap<T>>(
54
+ type: K,
55
+ listener: (event: StoreEventMap<T>[K]) => void,
56
+ options?: boolean | EventListenerOptions,
57
+ ): void
58
+ removeEventListener(
59
+ type: string,
60
+ listener: EventListenerOrEventListenerObject,
61
+ options?: boolean | EventListenerOptions,
62
+ ): void
63
+
64
+ dispatchEvent(event: Event): boolean
65
+ }
66
+
67
+ type Store<T extends UnknownRecord = UnknownRecord> = {
68
+ [K in keyof T & string]: T[K] extends UnknownRecord
69
+ ? Store<T[K]>
70
+ : State<T[K]>
71
+ } & StoreEventTarget<T> & {
72
+ [Symbol.toStringTag]: 'Store'
73
+ [Symbol.iterator](): IterableIterator<[string, Signal<T[keyof T]>]>
74
+
75
+ // Signal methods
76
+ add<K extends keyof T & string>(key: K, value: T[K]): void
77
+ get(): T
78
+ remove<K extends keyof T & string>(key: K): void
79
+ set(value: T): void
80
+ update(updater: (value: T) => T): void
81
+
82
+ // Interals signals
83
+ size: State<number>
84
+ }
85
+
86
+ /* === Functions === */
87
+
88
+ /**
89
+ * Create a new store with deeply nested reactive properties
90
+ *
91
+ * @since 0.15.0
92
+ * @param {T} initialValue - initial object value of the store
93
+ * @returns {Store<T>} - new store with reactive properties
94
+ */
95
+ const store = <T extends UnknownRecord>(initialValue: T): Store<T> => {
96
+ const watchers: Set<Watcher> = new Set()
97
+ const eventTarget = new EventTarget()
98
+ const signals: Map<
99
+ keyof T & string,
100
+ Store<T[keyof T & string]> | State<T[keyof T & string]>
101
+ > = new Map()
102
+ const cleanups = new Map<keyof T & string, Cleanup>()
103
+
104
+ // Internal state
105
+ const size = state(0)
106
+
107
+ // Get current record
108
+ const current = () => {
109
+ const record: Partial<T> = {}
110
+ for (const [key, value] of signals) {
111
+ record[key] = value.get()
112
+ }
113
+ return record as T
114
+ }
115
+
116
+ // Emit event
117
+ const emit = (type: keyof StoreEventMap<T>, detail: Partial<T>) =>
118
+ eventTarget.dispatchEvent(new CustomEvent(type, { detail }))
119
+
120
+ // Add nested signal and effect
121
+ const addSignalAndEffect = <K extends keyof T & string>(
122
+ key: K,
123
+ value: T[K],
124
+ ) => {
125
+ const signal = toMutableSignal<T[keyof T & string]>(value)
126
+ signals.set(key, signal)
127
+ const cleanup = effect(() => {
128
+ const value = signal.get()
129
+ if (value != null)
130
+ emit('store-change', { [key]: value } as unknown as Partial<T>)
131
+ })
132
+ cleanups.set(key, cleanup)
133
+ }
134
+
135
+ // Remove nested signal and effect
136
+ const removeSignalAndEffect = <K extends keyof T & string>(key: K) => {
137
+ signals.delete(key)
138
+ const cleanup = cleanups.get(key)
139
+ if (cleanup) cleanup()
140
+ cleanups.delete(key)
141
+ }
142
+
143
+ // Reconcile data and dispatch events
144
+ const reconcile = (oldValue: T, newValue: T): boolean => {
145
+ const changes = diff(oldValue, newValue)
146
+
147
+ batch(() => {
148
+ if (Object.keys(changes.add).length) {
149
+ for (const key in changes.add) {
150
+ const value = changes.add[key]
151
+ if (value != null) addSignalAndEffect(key, value)
152
+ }
153
+ emit('store-add', changes.add)
154
+ }
155
+ if (Object.keys(changes.change).length) {
156
+ for (const key in changes.change) {
157
+ const signal = signals.get(key as keyof T & string)
158
+ const value = changes.change[key]
159
+ if (
160
+ signal &&
161
+ value != null &&
162
+ hasMethod<Signal<T[keyof T & string]>>(signal, 'set')
163
+ )
164
+ signal.set(value)
165
+ }
166
+ emit('store-change', changes.change)
167
+ }
168
+ if (Object.keys(changes.remove).length) {
169
+ for (const key in changes.remove) {
170
+ removeSignalAndEffect(key)
171
+ }
172
+ emit('store-remove', changes.remove)
173
+ }
174
+
175
+ size.set(signals.size)
176
+ })
177
+
178
+ return changes.changed
179
+ }
180
+
181
+ // Initialize data
182
+ reconcile({} as T, initialValue)
183
+
184
+ // Queue initial additions event to allow listeners to be added first
185
+ setTimeout(() => {
186
+ const initialAdditionsEvent = new CustomEvent('store-add', {
187
+ detail: initialValue as Partial<T>,
188
+ }) as StoreAddEvent<T>
189
+ eventTarget.dispatchEvent(initialAdditionsEvent)
190
+ }, 0)
191
+
192
+ const storeProps = [
193
+ 'add',
194
+ 'get',
195
+ 'remove',
196
+ 'set',
197
+ 'update',
198
+ 'addEventListener',
199
+ 'removeEventListener',
200
+ 'dispatchEvent',
201
+ 'size',
202
+ ]
203
+
204
+ // Return proxy directly with integrated signal methods
205
+ return new Proxy({} as Store<T>, {
206
+ get(_target, prop) {
207
+ const key = String(prop)
208
+
209
+ // Handle signal methods and size property
210
+ switch (prop) {
211
+ case 'add':
212
+ return <K extends keyof T & string>(
213
+ k: K,
214
+ v: T[K],
215
+ ): void => {
216
+ if (!signals.has(k)) {
217
+ addSignalAndEffect(k, v)
218
+ notify(watchers)
219
+ emit('store-add', {
220
+ [k]: v,
221
+ } as unknown as Partial<T>)
222
+ size.set(signals.size)
223
+ }
224
+ }
225
+ case 'get':
226
+ return (): T => {
227
+ subscribe(watchers)
228
+ return current()
229
+ }
230
+ case 'remove':
231
+ return <K extends keyof T & string>(k: K): void => {
232
+ if (signals.has(k)) {
233
+ removeSignalAndEffect(k)
234
+ notify(watchers)
235
+ emit('store-remove', { [k]: UNSET } as Partial<T>)
236
+ size.set(signals.size)
237
+ }
238
+ }
239
+ case 'set':
240
+ return (v: T): void => {
241
+ if (reconcile(current(), v)) {
242
+ notify(watchers)
243
+ if (UNSET === v) watchers.clear()
244
+ }
245
+ }
246
+ case 'update':
247
+ return (fn: (v: T) => T): void => {
248
+ const oldValue = current()
249
+ const newValue = fn(oldValue)
250
+ if (reconcile(oldValue, newValue)) {
251
+ notify(watchers)
252
+ if (UNSET === newValue) watchers.clear()
253
+ }
254
+ }
255
+ case 'addEventListener':
256
+ return eventTarget.addEventListener.bind(eventTarget)
257
+ case 'removeEventListener':
258
+ return eventTarget.removeEventListener.bind(eventTarget)
259
+ case 'dispatchEvent':
260
+ return eventTarget.dispatchEvent.bind(eventTarget)
261
+ case 'size':
262
+ return size
263
+ }
264
+
265
+ // Handle symbol properties
266
+ if (prop === Symbol.toStringTag) return TYPE_STORE
267
+ if (prop === Symbol.iterator) {
268
+ return function* () {
269
+ for (const [key, signal] of signals) {
270
+ yield [key, signal as Signal<T[keyof T]>]
271
+ }
272
+ }
273
+ }
274
+
275
+ // Handle data properties - return signals
276
+ return signals.get(key)
277
+ },
278
+ has(_target, prop) {
279
+ const key = String(prop)
280
+ return (
281
+ signals.has(key) ||
282
+ storeProps.includes(key) ||
283
+ prop === Symbol.toStringTag ||
284
+ prop === Symbol.iterator
285
+ )
286
+ },
287
+ ownKeys() {
288
+ return Array.from(signals.keys())
289
+ },
290
+ getOwnPropertyDescriptor(_target, prop) {
291
+ const signal = signals.get(String(prop))
292
+ return signal
293
+ ? {
294
+ enumerable: true,
295
+ configurable: true,
296
+ writable: true,
297
+ value: signal,
298
+ }
299
+ : undefined
300
+ },
301
+ })
302
+ }
303
+
304
+ /**
305
+ * Check if the provided value is a Store instance
306
+ *
307
+ * @since 0.15.0
308
+ * @param {unknown} value - value to check
309
+ * @returns {boolean} - true if the value is a Store instance, false otherwise
310
+ */
311
+ const isStore = <T extends UnknownRecord>(value: unknown): value is Store<T> =>
312
+ isObjectOfType(value, TYPE_STORE)
313
+
314
+ /* === Exports === */
315
+
316
+ export {
317
+ TYPE_STORE,
318
+ isStore,
319
+ store,
320
+ type Store,
321
+ type StoreAddEvent,
322
+ type StoreChangeEvent,
323
+ type StoreRemoveEvent,
324
+ type StoreEventMap,
325
+ }
package/src/util.ts CHANGED
@@ -1,24 +1,76 @@
1
1
  /* === Utility Functions === */
2
2
 
3
+ const isNumber = /*#__PURE__*/ (value: unknown): value is number =>
4
+ typeof value === 'number'
5
+
6
+ const isString = /*#__PURE__*/ (value: unknown): value is string =>
7
+ typeof value === 'string'
8
+
3
9
  const isFunction = /*#__PURE__*/ <T>(
4
- value: unknown,
5
- ): value is (...args: unknown[]) => T => typeof value === 'function'
10
+ fn: unknown,
11
+ ): fn is (...args: unknown[]) => T => typeof fn === 'function'
12
+
13
+ const isAsyncFunction = /*#__PURE__*/ <T>(
14
+ fn: unknown,
15
+ ): fn is (...args: unknown[]) => Promise<T> =>
16
+ isFunction(fn) && fn.constructor.name === 'AsyncFunction'
6
17
 
7
18
  const isObjectOfType = /*#__PURE__*/ <T>(
8
19
  value: unknown,
9
20
  type: string,
10
21
  ): value is T => Object.prototype.toString.call(value) === `[object ${type}]`
11
22
 
12
- const toError = (reason: unknown): Error =>
23
+ const isRecord = /*#__PURE__*/ <T extends Record<string, unknown>>(
24
+ value: unknown,
25
+ ): value is T => isObjectOfType(value, 'Object')
26
+
27
+ const isPrimitive = /*#__PURE__*/ (value: unknown): boolean =>
28
+ typeof value !== 'object' && !isFunction(value)
29
+
30
+ const arrayToRecord = /*#__PURE__*/ <T extends unknown & {}>(
31
+ array: T[],
32
+ ): Record<string, T> => {
33
+ const record: Record<string, T> = {}
34
+ for (let i = 0; i < array.length; i++) {
35
+ if (i in array) record[String(i)] = array[i]
36
+ }
37
+ return record
38
+ }
39
+
40
+ const hasMethod = /*#__PURE__*/ <
41
+ T extends object & Record<string, (...args: unknown[]) => unknown>,
42
+ >(
43
+ obj: T,
44
+ methodName: string,
45
+ ): obj is T & Record<string, (...args: unknown[]) => unknown> =>
46
+ methodName in obj && isFunction(obj[methodName])
47
+
48
+ const isAbortError = /*#__PURE__*/ (error: unknown): boolean =>
49
+ error instanceof DOMException && error.name === 'AbortError'
50
+
51
+ const toError = /*#__PURE__*/ (reason: unknown): Error =>
13
52
  reason instanceof Error ? reason : Error(String(reason))
14
53
 
15
54
  class CircularDependencyError extends Error {
16
55
  constructor(where: string) {
17
56
  super(`Circular dependency in ${where} detected`)
18
- return this
57
+ this.name = 'CircularDependencyError'
19
58
  }
20
59
  }
21
60
 
22
61
  /* === Exports === */
23
62
 
24
- export { isFunction, isObjectOfType, toError, CircularDependencyError }
63
+ export {
64
+ isNumber,
65
+ isString,
66
+ isFunction,
67
+ isAsyncFunction,
68
+ isObjectOfType,
69
+ isRecord,
70
+ isPrimitive,
71
+ arrayToRecord,
72
+ hasMethod,
73
+ isAbortError,
74
+ toError,
75
+ CircularDependencyError,
76
+ }
@@ -1,14 +1,14 @@
1
- import { describe, test, expect } from 'bun:test'
2
- import { state, computed, batch, effect } from '../'
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { batch, computed, effect, match, resolve, state } from '../'
3
3
 
4
4
  /* === Tests === */
5
5
 
6
- describe('Batch', function () {
7
- test('should be triggered only once after repeated state change', function () {
6
+ describe('Batch', () => {
7
+ test('should be triggered only once after repeated state change', () => {
8
8
  const cause = state(0)
9
9
  let result = 0
10
10
  let count = 0
11
- effect(() => {
11
+ effect((): undefined => {
12
12
  result = cause.get()
13
13
  count++
14
14
  })
@@ -21,20 +21,22 @@ describe('Batch', function () {
21
21
  expect(count).toBe(2) // + 1 for effect initialization
22
22
  })
23
23
 
24
- test('should be triggered only once when multiple signals are set', function () {
24
+ test('should be triggered only once when multiple signals are set', () => {
25
25
  const a = state(3)
26
26
  const b = state(4)
27
27
  const c = state(5)
28
28
  const sum = computed(() => a.get() + b.get() + c.get())
29
29
  let result = 0
30
30
  let count = 0
31
- effect({
32
- signals: [sum],
33
- ok: res => {
34
- result = res
35
- count++
36
- },
37
- err: () => {},
31
+ effect(() => {
32
+ const resolved = resolve({ sum })
33
+ match(resolved, {
34
+ ok: ({ sum: res }) => {
35
+ result = res
36
+ count++
37
+ },
38
+ err: () => {},
39
+ })
38
40
  })
39
41
  batch(() => {
40
42
  a.set(6)
@@ -45,7 +47,7 @@ describe('Batch', function () {
45
47
  expect(count).toBe(2) // + 1 for effect initialization
46
48
  })
47
49
 
48
- test('should prove example from README works', function () {
50
+ test('should prove example from README works', () => {
49
51
  // State: define an array of Signal<number>
50
52
  const signals = [state(2), state(3), state(5)]
51
53
 
@@ -61,17 +63,19 @@ describe('Batch', function () {
61
63
  let errCount = 0
62
64
 
63
65
  // Effect: switch cases for the result
64
- effect({
65
- signals: [sum],
66
- ok: v => {
67
- result = v
68
- okCount++
69
- // console.log('Sum:', v)
70
- },
71
- err: _error => {
72
- errCount++
73
- // console.error('Error:', error)
74
- },
66
+ effect(() => {
67
+ const resolved = resolve({ sum })
68
+ match(resolved, {
69
+ ok: ({ sum: v }) => {
70
+ result = v
71
+ okCount++
72
+ // console.log('Sum:', v)
73
+ },
74
+ err: () => {
75
+ errCount++
76
+ // console.error('Error:', error)
77
+ },
78
+ })
75
79
  })
76
80
 
77
81
  expect(okCount).toBe(1)