@zeix/cause-effect 0.14.0 → 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.
@@ -1,8 +1,7 @@
1
1
  import { describe, test, expect } from 'bun:test'
2
2
  import {
3
3
  state,
4
- memo,
5
- task,
4
+ computed,
6
5
  UNSET,
7
6
  isComputed,
8
7
  isState,
@@ -19,37 +18,37 @@ const increment = (n: number) => (Number.isFinite(n) ? n + 1 : UNSET)
19
18
  describe('Computed', function () {
20
19
  test('should identify computed signals with isComputed()', () => {
21
20
  const count = state(42)
22
- const doubled = memo(() => count.get() * 2)
21
+ const doubled = computed(() => count.get() * 2)
23
22
  expect(isComputed(doubled)).toBe(true)
24
23
  expect(isState(doubled)).toBe(false)
25
24
  })
26
25
 
27
26
  test('should compute a function', function () {
28
- const derived = memo(() => 1 + 2)
27
+ const derived = computed(() => 1 + 2)
29
28
  expect(derived.get()).toBe(3)
30
29
  })
31
30
 
32
31
  test('should compute function dependent on a signal', function () {
33
32
  const cause = state(42)
34
- const derived = memo(() => cause.get() + 1)
33
+ const derived = computed(() => cause.get() + 1)
35
34
  expect(derived.get()).toBe(43)
36
35
  })
37
36
 
38
37
  test('should compute function dependent on an updated signal', function () {
39
38
  const cause = state(42)
40
- const derived = memo(() => cause.get() + 1)
39
+ const derived = computed(() => cause.get() + 1)
41
40
  cause.set(24)
42
41
  expect(derived.get()).toBe(25)
43
42
  })
44
43
 
45
44
  test('should compute function dependent on an async signal', async function () {
46
45
  const status = state('pending')
47
- const promised = task(async () => {
46
+ const promised = computed(async () => {
48
47
  await wait(100)
49
48
  status.set('success')
50
49
  return 42
51
50
  })
52
- const derived = memo(() => increment(promised.get()))
51
+ const derived = computed(() => increment(promised.get()))
53
52
  expect(derived.get()).toBe(UNSET)
54
53
  expect(status.get()).toBe('pending')
55
54
  await wait(110)
@@ -60,13 +59,13 @@ describe('Computed', function () {
60
59
  test('should handle errors from an async signal gracefully', async function () {
61
60
  const status = state('pending')
62
61
  const error = state('')
63
- const promised = task(async () => {
62
+ const promised = computed(async () => {
64
63
  await wait(100)
65
64
  status.set('error')
66
65
  error.set('error occurred')
67
66
  return 0
68
67
  })
69
- const derived = memo(() => increment(promised.get()))
68
+ const derived = computed(() => increment(promised.get()))
70
69
  expect(derived.get()).toBe(UNSET)
71
70
  expect(status.get()).toBe('pending')
72
71
  await wait(110)
@@ -75,15 +74,15 @@ describe('Computed', function () {
75
74
  })
76
75
 
77
76
  test('should compute task signals in parallel without waterfalls', async function () {
78
- const a = task(async () => {
77
+ const a = computed(async () => {
79
78
  await wait(100)
80
79
  return 10
81
80
  })
82
- const b = task(async () => {
81
+ const b = computed(async () => {
83
82
  await wait(100)
84
83
  return 20
85
84
  })
86
- const c = memo(() => {
85
+ const c = computed(() => {
87
86
  const aValue = a.get()
88
87
  const bValue = b.get()
89
88
  return aValue === UNSET || bValue === UNSET
@@ -97,17 +96,17 @@ describe('Computed', function () {
97
96
 
98
97
  test('should compute function dependent on a chain of computed states dependent on a signal', function () {
99
98
  const x = state(42)
100
- const a = memo(() => x.get() + 1)
101
- const b = memo(() => a.get() * 2)
102
- const c = memo(() => b.get() + 1)
99
+ const a = computed(() => x.get() + 1)
100
+ const b = computed(() => a.get() * 2)
101
+ const c = computed(() => b.get() + 1)
103
102
  expect(c.get()).toBe(87)
104
103
  })
105
104
 
106
105
  test('should compute function dependent on a chain of computed states dependent on an updated signal', function () {
107
106
  const x = state(42)
108
- const a = memo(() => x.get() + 1)
109
- const b = memo(() => a.get() * 2)
110
- const c = memo(() => b.get() + 1)
107
+ const a = computed(() => x.get() + 1)
108
+ const b = computed(() => a.get() * 2)
109
+ const c = computed(() => b.get() + 1)
111
110
  x.set(24)
112
111
  expect(c.get()).toBe(51)
113
112
  })
@@ -115,9 +114,9 @@ describe('Computed', function () {
115
114
  test('should drop X->B->X updates', function () {
116
115
  let count = 0
117
116
  const x = state(2)
118
- const a = memo(() => x.get() - 1)
119
- const b = memo(() => x.get() + a.get())
120
- const c = memo(() => {
117
+ const a = computed(() => x.get() - 1)
118
+ const b = computed(() => x.get() + a.get())
119
+ const c = computed(() => {
121
120
  count++
122
121
  return 'c: ' + b.get()
123
122
  })
@@ -131,9 +130,9 @@ describe('Computed', function () {
131
130
  test('should only update every signal once (diamond graph)', function () {
132
131
  let count = 0
133
132
  const x = state('a')
134
- const a = memo(() => x.get())
135
- const b = memo(() => x.get())
136
- const c = memo(() => {
133
+ const a = computed(() => x.get())
134
+ const b = computed(() => x.get())
135
+ const c = computed(() => {
137
136
  count++
138
137
  return a.get() + ' ' + b.get()
139
138
  })
@@ -148,10 +147,10 @@ describe('Computed', function () {
148
147
  test('should only update every signal once (diamond graph + tail)', function () {
149
148
  let count = 0
150
149
  const x = state('a')
151
- const a = memo(() => x.get())
152
- const b = memo(() => x.get())
153
- const c = memo(() => a.get() + ' ' + b.get())
154
- const d = memo(() => {
150
+ const a = computed(() => x.get())
151
+ const b = computed(() => x.get())
152
+ const c = computed(() => a.get() + ' ' + b.get())
153
+ const d = computed(() => {
155
154
  count++
156
155
  return c.get()
157
156
  })
@@ -166,7 +165,7 @@ describe('Computed', function () {
166
165
  const a = state(3)
167
166
  const b = state(4)
168
167
  let count = 0
169
- const sum = memo(() => {
168
+ const sum = computed(() => {
170
169
  count++
171
170
  return a.get() + b.get()
172
171
  })
@@ -189,11 +188,11 @@ describe('Computed', function () {
189
188
  test('should bail out if result is the same', function () {
190
189
  let count = 0
191
190
  const x = state('a')
192
- const a = memo(() => {
191
+ const a = computed(() => {
193
192
  x.get()
194
193
  return 'foo'
195
194
  })
196
- const b = memo(() => {
195
+ const b = computed(() => {
197
196
  count++
198
197
  return a.get()
199
198
  })
@@ -209,9 +208,9 @@ describe('Computed', function () {
209
208
  test('should block if result remains unchanged', function () {
210
209
  let count = 0
211
210
  const x = state(42)
212
- const a = memo(() => x.get() % 2)
213
- const b = memo(() => (a.get() ? 'odd' : 'even'))
214
- const c = memo(() => {
211
+ const a = computed(() => x.get() % 2)
212
+ const b = computed(() => (a.get() ? 'odd' : 'even'))
213
+ const c = computed(() => {
215
214
  count++
216
215
  return `c: ${b.get()}`
217
216
  })
@@ -226,11 +225,11 @@ describe('Computed', function () {
226
225
 
227
226
  test('should detect and throw error for circular dependencies', function () {
228
227
  const a = state(1)
229
- const b = memo(() => c.get() + 1)
230
- const c = memo(() => b.get() + a.get())
228
+ const b = computed(() => c.get() + 1)
229
+ const c = computed(() => b.get() + a.get())
231
230
  expect(() => {
232
231
  b.get() // This should trigger the circular dependency
233
- }).toThrow('Circular dependency in memo detected')
232
+ }).toThrow('Circular dependency in computed detected')
234
233
  expect(a.get()).toBe(1)
235
234
  })
236
235
 
@@ -238,13 +237,13 @@ describe('Computed', function () {
238
237
  let okCount = 0
239
238
  let errCount = 0
240
239
  const x = state(0)
241
- const a = memo(() => {
240
+ const a = computed(() => {
242
241
  if (x.get() === 1) throw new Error('Calculation error')
243
242
  return 1
244
243
  })
245
244
 
246
245
  // Replace matcher with try/catch in a computed
247
- const b = memo(() => {
246
+ const b = computed(() => {
248
247
  try {
249
248
  a.get() // just check if it works
250
249
  return `c: success`
@@ -253,7 +252,7 @@ describe('Computed', function () {
253
252
  return `c: recovered`
254
253
  }
255
254
  })
256
- const c = memo(() => {
255
+ const c = computed(() => {
257
256
  okCount++
258
257
  return b.get()
259
258
  })
@@ -276,7 +275,7 @@ describe('Computed', function () {
276
275
 
277
276
  test('should create an effect that reacts on async computed changes', async function () {
278
277
  const cause = state(42)
279
- const derived = task(async () => {
278
+ const derived = computed(async () => {
280
279
  await wait(100)
281
280
  return cause.get() + 1
282
281
  })
@@ -306,11 +305,11 @@ describe('Computed', function () {
306
305
 
307
306
  test('should handle complex computed signal with error and async dependencies', async function () {
308
307
  const toggleState = state(true)
309
- const errorProne = memo(() => {
308
+ const errorProne = computed(() => {
310
309
  if (toggleState.get()) throw new Error('Intentional error')
311
310
  return 42
312
311
  })
313
- const asyncValue = task(async () => {
312
+ const asyncValue = computed(async () => {
314
313
  await wait(50)
315
314
  return 10
316
315
  })
@@ -319,8 +318,7 @@ describe('Computed', function () {
319
318
  let errCount = 0
320
319
  // let _result: number = 0
321
320
 
322
- // Replace matcher with try/catch in a computed
323
- const complexComputed = memo(() => {
321
+ const complexComputed = computed(() => {
324
322
  try {
325
323
  const x = errorProne.get()
326
324
  const y = asyncValue.get()
@@ -355,7 +353,7 @@ describe('Computed', function () {
355
353
  test('should handle signal changes during async computation', async function () {
356
354
  const source = state(1)
357
355
  let computationCount = 0
358
- const derived = task(async abort => {
356
+ const derived = computed(async abort => {
359
357
  computationCount++
360
358
  expect(abort?.aborted).toBe(false)
361
359
  await wait(100)
@@ -376,7 +374,7 @@ describe('Computed', function () {
376
374
  test('should handle multiple rapid changes during async computation', async function () {
377
375
  const source = state(1)
378
376
  let computationCount = 0
379
- const derived = task(async abort => {
377
+ const derived = computed(async abort => {
380
378
  computationCount++
381
379
  expect(abort?.aborted).toBe(false)
382
380
  await wait(100)
@@ -400,7 +398,7 @@ describe('Computed', function () {
400
398
 
401
399
  test('should handle errors in aborted computations', async function () {
402
400
  const source = state(1)
403
- const derived = task(async () => {
401
+ const derived = computed(async () => {
404
402
  await wait(100)
405
403
  const value = source.get()
406
404
  if (value === 2) throw new Error('Intentional error')
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, mock } from 'bun:test'
2
- import { state, task, effect, UNSET, memo } from '../'
2
+ import { state, computed, effect, UNSET } from '../'
3
3
 
4
4
  /* === Utility Functions === */
5
5
 
@@ -21,11 +21,11 @@ describe('Effect', function () {
21
21
  })
22
22
 
23
23
  test('should be triggered after computed async signals resolve without waterfalls', async function () {
24
- const a = task(async () => {
24
+ const a = computed(async () => {
25
25
  await wait(100)
26
26
  return 10
27
27
  })
28
- const b = task(async () => {
28
+ const b = computed(async () => {
29
29
  await wait(100)
30
30
  return 20
31
31
  })
@@ -62,7 +62,7 @@ describe('Effect', function () {
62
62
 
63
63
  test('should handle errors in effects', function () {
64
64
  const a = state(1)
65
- const b = memo(() => {
65
+ const b = computed(() => {
66
66
  const v = a.get()
67
67
  if (v > 5) throw new Error('Value too high')
68
68
  return v * 2
@@ -99,7 +99,7 @@ describe('Effect', function () {
99
99
  })
100
100
 
101
101
  test('should handle UNSET values in effects', async function () {
102
- const a = task(async () => {
102
+ const a = computed(async () => {
103
103
  await wait(100)
104
104
  return 42
105
105
  })
@@ -133,7 +133,7 @@ describe('Effect', function () {
133
133
 
134
134
  try {
135
135
  const a = state(1)
136
- const b = memo(() => {
136
+ const b = computed(() => {
137
137
  const v = a.get()
138
138
  if (v > 5) throw new Error('Value too high')
139
139
  return v * 2
package/src/memo.d.ts DELETED
@@ -1,13 +0,0 @@
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 DELETED
@@ -1,91 +0,0 @@
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 }
package/src/task.d.ts DELETED
@@ -1,17 +0,0 @@
1
- import { type Computed } from './computed';
2
- /**
3
- * Callback for async computation tasks
4
- * This explicitly returns a Promise<T> to differentiate from MemoCallback
5
- *
6
- * @since 0.14.0
7
- */
8
- type TaskCallback<T extends {}> = (abort: AbortSignal) => Promise<T>;
9
- /**
10
- * Create a derived signal that supports asynchronous computations
11
- *
12
- * @since 0.14.0
13
- * @param {TaskCallback<T>} fn - async computation callback
14
- * @returns {Computed<T>} - Computed signal
15
- */
16
- declare const task: <T extends {}>(fn: TaskCallback<T>) => Computed<T>;
17
- export { type TaskCallback, task };
package/src/task.ts DELETED
@@ -1,153 +0,0 @@
1
- import { UNSET } from './signal'
2
- import {
3
- CircularDependencyError,
4
- isAbortError,
5
- isPromise,
6
- toError,
7
- } from './util'
8
- import {
9
- type Cleanup,
10
- type Watcher,
11
- flush,
12
- notify,
13
- subscribe,
14
- watch,
15
- } from './scheduler'
16
- import {
17
- type Computed,
18
- TYPE_COMPUTED,
19
- } from './computed'
20
-
21
- /* === Types === */
22
-
23
- /**
24
- * Callback for async computation tasks
25
- * This explicitly returns a Promise<T> to differentiate from MemoCallback
26
- *
27
- * @since 0.14.0
28
- */
29
- type TaskCallback<T extends {}> = (abort: AbortSignal) => Promise<T>
30
-
31
- /* === Function === */
32
-
33
- /**
34
- * Create a derived signal that supports asynchronous computations
35
- *
36
- * @since 0.14.0
37
- * @param {TaskCallback<T>} fn - async computation callback
38
- * @returns {Computed<T>} - Computed signal
39
- */
40
- const task = <T extends {}>(fn: TaskCallback<T>): Computed<T> => {
41
- const watchers: Set<Watcher> = new Set()
42
-
43
- // Internal state
44
- let value: T = UNSET
45
- let error: Error | undefined
46
- let dirty = true
47
- let changed = false
48
- let computing = false
49
- let controller: AbortController | undefined
50
-
51
- // Functions to update internal state
52
- const ok = (v: T) => {
53
- if (!Object.is(v, value)) {
54
- value = v
55
- dirty = false
56
- error = undefined
57
- changed = true
58
- }
59
- }
60
- const nil = () => {
61
- changed = UNSET !== value
62
- value = UNSET
63
- error = undefined
64
- }
65
- const err = (e: unknown) => {
66
- const newError = toError(e)
67
- changed = !(
68
- error &&
69
- newError.name === error.name &&
70
- newError.message === error.message
71
- )
72
- value = UNSET
73
- error = newError
74
- }
75
- const resolve = (v: T) => {
76
- computing = false
77
- controller = undefined
78
- ok(v)
79
- if (changed) notify(watchers)
80
- }
81
- const reject = (e: unknown) => {
82
- computing = false
83
- controller = undefined
84
- err(e)
85
- if (changed) notify(watchers)
86
- }
87
- const abort = () => {
88
- computing = false
89
- controller = undefined
90
- compute() // retry
91
- }
92
-
93
- // Called when notified from sources (push)
94
- const mark = (() => {
95
- dirty = true
96
- controller?.abort('Aborted because source signal changed')
97
- if (watchers.size) {
98
- notify(watchers)
99
- } else {
100
- mark.cleanups.forEach(fn => fn())
101
- mark.cleanups.clear()
102
- }
103
- }) as Watcher
104
- mark.cleanups = new Set<Cleanup>()
105
-
106
- // Called when requested by dependencies (pull)
107
- const compute = () =>
108
- watch(() => {
109
- if (computing) throw new CircularDependencyError('task')
110
- changed = false
111
- controller = new AbortController()
112
- controller.signal.addEventListener('abort', abort, {
113
- once: true,
114
- })
115
-
116
- let result: T | Promise<T>
117
- computing = true
118
- try {
119
- result = fn(controller.signal)
120
- } catch (e) {
121
- if (isAbortError(e)) nil()
122
- else err(e)
123
- computing = false
124
- return
125
- }
126
- if (isPromise(result)) result.then(resolve, reject)
127
- else if (null == result || UNSET === result) nil()
128
- else ok(result)
129
- computing = false
130
- }, mark)
131
-
132
- const c: Computed<T> = {
133
- [Symbol.toStringTag]: TYPE_COMPUTED,
134
-
135
- /**
136
- * Get the current value of the computed
137
- *
138
- * @returns {T} - current value of the computed
139
- */
140
- get: (): T => {
141
- subscribe(watchers)
142
- flush()
143
- if (dirty) compute()
144
- if (error) throw error
145
- return value
146
- },
147
- }
148
- return c
149
- }
150
-
151
- /* === Exports === */
152
-
153
- export { type TaskCallback, task }