@pyreon/reactivity 0.11.5 → 0.11.6

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,9 +1,9 @@
1
- import { effect, onCleanup, renderEffect, setErrorHandler } from "../effect"
2
- import { effectScope, setCurrentScope } from "../scope"
3
- import { signal } from "../signal"
1
+ import { effect, onCleanup, renderEffect, setErrorHandler } from '../effect'
2
+ import { effectScope, setCurrentScope } from '../scope'
3
+ import { signal } from '../signal'
4
4
 
5
- describe("effect", () => {
6
- test("runs immediately", () => {
5
+ describe('effect', () => {
6
+ test('runs immediately', () => {
7
7
  let ran = false
8
8
  effect(() => {
9
9
  ran = true
@@ -11,7 +11,7 @@ describe("effect", () => {
11
11
  expect(ran).toBe(true)
12
12
  })
13
13
 
14
- test("re-runs when tracked signal changes", () => {
14
+ test('re-runs when tracked signal changes', () => {
15
15
  const s = signal(0)
16
16
  let count = 0
17
17
  effect(() => {
@@ -25,7 +25,7 @@ describe("effect", () => {
25
25
  expect(count).toBe(3)
26
26
  })
27
27
 
28
- test("does not re-run after dispose", () => {
28
+ test('does not re-run after dispose', () => {
29
29
  const s = signal(0)
30
30
  let count = 0
31
31
  const e = effect(() => {
@@ -37,7 +37,7 @@ describe("effect", () => {
37
37
  expect(count).toBe(1) // only the initial run
38
38
  })
39
39
 
40
- test("tracks multiple signals", () => {
40
+ test('tracks multiple signals', () => {
41
41
  const a = signal(1)
42
42
  const b = signal(2)
43
43
  let result = 0
@@ -51,7 +51,7 @@ describe("effect", () => {
51
51
  expect(result).toBe(30)
52
52
  })
53
53
 
54
- test("does not track signals accessed after conditional branch", () => {
54
+ test('does not track signals accessed after conditional branch', () => {
55
55
  const toggle = signal(true)
56
56
  const a = signal(1)
57
57
  const b = signal(100)
@@ -69,7 +69,7 @@ describe("effect", () => {
69
69
  expect(result).toBe(100)
70
70
  })
71
71
 
72
- test("catches errors via default error handler", () => {
72
+ test('catches errors via default error handler', () => {
73
73
  const errors: unknown[] = []
74
74
  const origError = console.error
75
75
  console.error = (...args: unknown[]) => errors.push(args)
@@ -77,14 +77,14 @@ describe("effect", () => {
77
77
  const s = signal(0)
78
78
  effect(() => {
79
79
  s()
80
- throw new Error("boom")
80
+ throw new Error('boom')
81
81
  })
82
82
 
83
83
  expect(errors.length).toBe(1)
84
84
  console.error = origError
85
85
  })
86
86
 
87
- test("calls cleanup before re-run", () => {
87
+ test('calls cleanup before re-run', () => {
88
88
  const s = signal(0)
89
89
  let cleanups = 0
90
90
  effect(() => {
@@ -100,7 +100,7 @@ describe("effect", () => {
100
100
  expect(cleanups).toBe(2)
101
101
  })
102
102
 
103
- test("calls cleanup on dispose", () => {
103
+ test('calls cleanup on dispose', () => {
104
104
  const s = signal(0)
105
105
  let cleanups = 0
106
106
  const e = effect(() => {
@@ -117,7 +117,7 @@ describe("effect", () => {
117
117
  expect(cleanups).toBe(1)
118
118
  })
119
119
 
120
- test("cleanup errors are caught by error handler", () => {
120
+ test('cleanup errors are caught by error handler', () => {
121
121
  const caught: unknown[] = []
122
122
  setErrorHandler((err) => caught.push(err))
123
123
 
@@ -125,18 +125,18 @@ describe("effect", () => {
125
125
  effect(() => {
126
126
  s()
127
127
  return () => {
128
- throw new Error("cleanup boom")
128
+ throw new Error('cleanup boom')
129
129
  }
130
130
  })
131
131
  s.set(1) // triggers cleanup which throws
132
132
  expect(caught.length).toBe(1)
133
- expect((caught[0] as Error).message).toBe("cleanup boom")
133
+ expect((caught[0] as Error).message).toBe('cleanup boom')
134
134
 
135
135
  // Restore default handler
136
136
  setErrorHandler((_err) => {})
137
137
  })
138
138
 
139
- test("works with no cleanup return (backwards compatible)", () => {
139
+ test('works with no cleanup return (backwards compatible)', () => {
140
140
  const s = signal(0)
141
141
  let count = 0
142
142
  effect(() => {
@@ -149,24 +149,24 @@ describe("effect", () => {
149
149
  expect(count).toBe(2)
150
150
  })
151
151
 
152
- test("setErrorHandler replaces the error handler", () => {
152
+ test('setErrorHandler replaces the error handler', () => {
153
153
  const caught: unknown[] = []
154
154
  setErrorHandler((err) => caught.push(err))
155
155
 
156
156
  const s = signal(0)
157
157
  effect(() => {
158
158
  s()
159
- throw new Error("custom")
159
+ throw new Error('custom')
160
160
  })
161
161
 
162
162
  expect(caught.length).toBe(1)
163
- expect((caught[0] as Error).message).toBe("custom")
163
+ expect((caught[0] as Error).message).toBe('custom')
164
164
 
165
165
  // Restore default handler
166
166
  setErrorHandler((_err) => {})
167
167
  })
168
168
 
169
- test("effect notifies scope on re-run (not first run)", async () => {
169
+ test('effect notifies scope on re-run (not first run)', async () => {
170
170
  const scope = effectScope()
171
171
  setCurrentScope(scope)
172
172
 
@@ -192,8 +192,8 @@ describe("effect", () => {
192
192
  })
193
193
  })
194
194
 
195
- describe("renderEffect", () => {
196
- test("runs immediately and tracks signals", () => {
195
+ describe('renderEffect', () => {
196
+ test('runs immediately and tracks signals', () => {
197
197
  const s = signal(0)
198
198
  let count = 0
199
199
  renderEffect(() => {
@@ -205,7 +205,7 @@ describe("renderEffect", () => {
205
205
  expect(count).toBe(2)
206
206
  })
207
207
 
208
- test("dispose stops tracking", () => {
208
+ test('dispose stops tracking', () => {
209
209
  const s = signal(0)
210
210
  let count = 0
211
211
  const dispose = renderEffect(() => {
@@ -218,7 +218,7 @@ describe("renderEffect", () => {
218
218
  expect(count).toBe(1)
219
219
  })
220
220
 
221
- test("dispose is idempotent", () => {
221
+ test('dispose is idempotent', () => {
222
222
  const s = signal(0)
223
223
  const dispose = renderEffect(() => {
224
224
  s()
@@ -227,7 +227,7 @@ describe("renderEffect", () => {
227
227
  dispose() // should not throw
228
228
  })
229
229
 
230
- test("tracks dynamic dependencies", () => {
230
+ test('tracks dynamic dependencies', () => {
231
231
  const toggle = signal(true)
232
232
  const a = signal(1)
233
233
  const b = signal(100)
@@ -242,7 +242,7 @@ describe("renderEffect", () => {
242
242
  expect(result).toBe(100)
243
243
  })
244
244
 
245
- test("does not re-run after disposed during signal update", () => {
245
+ test('does not re-run after disposed during signal update', () => {
246
246
  const s = signal(0)
247
247
  let count = 0
248
248
  const dispose = renderEffect(() => {
@@ -255,8 +255,8 @@ describe("renderEffect", () => {
255
255
  })
256
256
  })
257
257
 
258
- describe("onCleanup", () => {
259
- test("runs cleanup before effect re-runs", () => {
258
+ describe('onCleanup', () => {
259
+ test('runs cleanup before effect re-runs', () => {
260
260
  const s = signal(0)
261
261
  const log: string[] = []
262
262
  effect(() => {
@@ -264,14 +264,14 @@ describe("onCleanup", () => {
264
264
  onCleanup(() => log.push(`cleanup-${val}`))
265
265
  log.push(`run-${val}`)
266
266
  })
267
- expect(log).toEqual(["run-0"])
267
+ expect(log).toEqual(['run-0'])
268
268
  s.set(1)
269
- expect(log).toEqual(["run-0", "cleanup-0", "run-1"])
269
+ expect(log).toEqual(['run-0', 'cleanup-0', 'run-1'])
270
270
  s.set(2)
271
- expect(log).toEqual(["run-0", "cleanup-0", "run-1", "cleanup-1", "run-2"])
271
+ expect(log).toEqual(['run-0', 'cleanup-0', 'run-1', 'cleanup-1', 'run-2'])
272
272
  })
273
273
 
274
- test("runs cleanup on dispose", () => {
274
+ test('runs cleanup on dispose', () => {
275
275
  let cleaned = false
276
276
  const e = effect(() => {
277
277
  onCleanup(() => {
@@ -283,36 +283,36 @@ describe("onCleanup", () => {
283
283
  expect(cleaned).toBe(true)
284
284
  })
285
285
 
286
- test("supports multiple onCleanup calls", () => {
286
+ test('supports multiple onCleanup calls', () => {
287
287
  const s = signal(0)
288
288
  const log: string[] = []
289
289
  effect(() => {
290
290
  s()
291
- onCleanup(() => log.push("a"))
292
- onCleanup(() => log.push("b"))
291
+ onCleanup(() => log.push('a'))
292
+ onCleanup(() => log.push('b'))
293
293
  })
294
294
  s.set(1)
295
- expect(log).toEqual(["a", "b"])
295
+ expect(log).toEqual(['a', 'b'])
296
296
  })
297
297
 
298
- test("works alongside return cleanup", () => {
298
+ test('works alongside return cleanup', () => {
299
299
  const s = signal(0)
300
300
  const log: string[] = []
301
301
  effect(() => {
302
302
  s()
303
- onCleanup(() => log.push("onCleanup"))
304
- return () => log.push("return")
303
+ onCleanup(() => log.push('onCleanup'))
304
+ return () => log.push('return')
305
305
  })
306
306
  s.set(1)
307
- expect(log).toEqual(["onCleanup", "return"])
307
+ expect(log).toEqual(['onCleanup', 'return'])
308
308
  })
309
309
 
310
- test("no-ops outside effect", () => {
310
+ test('no-ops outside effect', () => {
311
311
  // Should not throw
312
312
  onCleanup(() => {})
313
313
  })
314
314
 
315
- test("cleanup ordering: onCleanup runs before return cleanup", () => {
315
+ test('cleanup ordering: onCleanup runs before return cleanup', () => {
316
316
  const s = signal(0)
317
317
  const log: string[] = []
318
318
  effect(() => {
@@ -322,37 +322,37 @@ describe("onCleanup", () => {
322
322
  })
323
323
  s.set(1)
324
324
  // onCleanup should fire first, then return cleanup
325
- expect(log).toEqual(["onCleanup-0", "return-0"])
325
+ expect(log).toEqual(['onCleanup-0', 'return-0'])
326
326
  s.set(2)
327
- expect(log).toEqual(["onCleanup-0", "return-0", "onCleanup-1", "return-1"])
327
+ expect(log).toEqual(['onCleanup-0', 'return-0', 'onCleanup-1', 'return-1'])
328
328
  })
329
329
 
330
- test("multiple onCleanup callbacks run in registration order", () => {
330
+ test('multiple onCleanup callbacks run in registration order', () => {
331
331
  const s = signal(0)
332
332
  const log: string[] = []
333
333
  effect(() => {
334
334
  s()
335
- onCleanup(() => log.push("first"))
336
- onCleanup(() => log.push("second"))
337
- onCleanup(() => log.push("third"))
335
+ onCleanup(() => log.push('first'))
336
+ onCleanup(() => log.push('second'))
337
+ onCleanup(() => log.push('third'))
338
338
  })
339
339
  s.set(1)
340
- expect(log).toEqual(["first", "second", "third"])
340
+ expect(log).toEqual(['first', 'second', 'third'])
341
341
  })
342
342
 
343
- test("cleanup runs on dispose even when effect never re-ran", () => {
343
+ test('cleanup runs on dispose even when effect never re-ran', () => {
344
344
  const log: string[] = []
345
345
  const e = effect(() => {
346
- onCleanup(() => log.push("disposed"))
346
+ onCleanup(() => log.push('disposed'))
347
347
  })
348
348
  expect(log).toEqual([])
349
349
  e.dispose()
350
- expect(log).toEqual(["disposed"])
350
+ expect(log).toEqual(['disposed'])
351
351
  })
352
352
  })
353
353
 
354
- describe("effect — error handling", () => {
355
- test("error in effect does not prevent other effects from running", () => {
354
+ describe('effect — error handling', () => {
355
+ test('error in effect does not prevent other effects from running', () => {
356
356
  const caught: unknown[] = []
357
357
  setErrorHandler((err) => caught.push(err))
358
358
 
@@ -361,7 +361,7 @@ describe("effect — error handling", () => {
361
361
 
362
362
  effect(() => {
363
363
  s()
364
- throw new Error("bad effect")
364
+ throw new Error('bad effect')
365
365
  })
366
366
  effect(() => {
367
367
  s()
@@ -378,7 +378,7 @@ describe("effect — error handling", () => {
378
378
  setErrorHandler((_err) => {})
379
379
  })
380
380
 
381
- test("error during cleanup is caught by error handler", () => {
381
+ test('error during cleanup is caught by error handler', () => {
382
382
  const caught: unknown[] = []
383
383
  setErrorHandler((err) => caught.push(err))
384
384
 
@@ -386,13 +386,13 @@ describe("effect — error handling", () => {
386
386
  effect(() => {
387
387
  s()
388
388
  onCleanup(() => {
389
- throw new Error("cleanup error")
389
+ throw new Error('cleanup error')
390
390
  })
391
391
  })
392
392
 
393
393
  s.set(1) // triggers cleanup which throws
394
394
  expect(caught).toHaveLength(1)
395
- expect((caught[0] as Error).message).toBe("cleanup error")
395
+ expect((caught[0] as Error).message).toBe('cleanup error')
396
396
 
397
397
  setErrorHandler((_err) => {})
398
398
  })
@@ -1,8 +1,8 @@
1
- import { createResource } from "../resource"
2
- import { signal } from "../signal"
1
+ import { createResource } from '../resource'
2
+ import { signal } from '../signal'
3
3
 
4
- describe("createResource", () => {
5
- test("fetches data when source changes", async () => {
4
+ describe('createResource', () => {
5
+ test('fetches data when source changes', async () => {
6
6
  const userId = signal(1)
7
7
  const resource = createResource(
8
8
  () => userId(),
@@ -15,12 +15,12 @@ describe("createResource", () => {
15
15
 
16
16
  await new Promise((r) => setTimeout(r, 10))
17
17
 
18
- expect(resource.data()).toBe("user-1")
18
+ expect(resource.data()).toBe('user-1')
19
19
  expect(resource.loading()).toBe(false)
20
20
  expect(resource.error()).toBeUndefined()
21
21
  })
22
22
 
23
- test("re-fetches when source signal changes", async () => {
23
+ test('re-fetches when source signal changes', async () => {
24
24
  const userId = signal(1)
25
25
  const resource = createResource(
26
26
  () => userId(),
@@ -28,32 +28,32 @@ describe("createResource", () => {
28
28
  )
29
29
 
30
30
  await new Promise((r) => setTimeout(r, 10))
31
- expect(resource.data()).toBe("user-1")
31
+ expect(resource.data()).toBe('user-1')
32
32
 
33
33
  userId.set(2)
34
34
  expect(resource.loading()).toBe(true)
35
35
 
36
36
  await new Promise((r) => setTimeout(r, 10))
37
- expect(resource.data()).toBe("user-2")
37
+ expect(resource.data()).toBe('user-2')
38
38
  expect(resource.loading()).toBe(false)
39
39
  })
40
40
 
41
- test("handles fetcher errors", async () => {
41
+ test('handles fetcher errors', async () => {
42
42
  const userId = signal(1)
43
43
  const resource = createResource(
44
44
  () => userId(),
45
- (_id) => Promise.reject(new Error("network error")),
45
+ (_id) => Promise.reject(new Error('network error')),
46
46
  )
47
47
 
48
48
  await new Promise((r) => setTimeout(r, 10))
49
49
 
50
50
  expect(resource.error()).toBeInstanceOf(Error)
51
- expect((resource.error() as Error).message).toBe("network error")
51
+ expect((resource.error() as Error).message).toBe('network error')
52
52
  expect(resource.loading()).toBe(false)
53
53
  expect(resource.data()).toBeUndefined()
54
54
  })
55
55
 
56
- test("refetch re-runs the fetcher with current source", async () => {
56
+ test('refetch re-runs the fetcher with current source', async () => {
57
57
  let fetchCount = 0
58
58
  const userId = signal(1)
59
59
  const resource = createResource(
@@ -65,14 +65,14 @@ describe("createResource", () => {
65
65
  )
66
66
 
67
67
  await new Promise((r) => setTimeout(r, 10))
68
- expect(resource.data()).toBe("user-1-1")
68
+ expect(resource.data()).toBe('user-1-1')
69
69
 
70
70
  resource.refetch()
71
71
  await new Promise((r) => setTimeout(r, 10))
72
- expect(resource.data()).toBe("user-1-2")
72
+ expect(resource.data()).toBe('user-1-2')
73
73
  })
74
74
 
75
- test("ignores stale responses (race condition)", async () => {
75
+ test('ignores stale responses (race condition)', async () => {
76
76
  const userId = signal(1)
77
77
  const resolvers: ((v: string) => void)[] = []
78
78
 
@@ -92,17 +92,17 @@ describe("createResource", () => {
92
92
  expect(resolvers.length).toBe(2)
93
93
 
94
94
  // Resolve the SECOND request first
95
- resolvers[1]?.("user-2")
95
+ resolvers[1]?.('user-2')
96
96
  await new Promise((r) => setTimeout(r, 10))
97
- expect(resource.data()).toBe("user-2")
97
+ expect(resource.data()).toBe('user-2')
98
98
 
99
99
  // Now resolve the FIRST (stale) request — should be ignored
100
- resolvers[0]?.("user-1")
100
+ resolvers[0]?.('user-1')
101
101
  await new Promise((r) => setTimeout(r, 10))
102
- expect(resource.data()).toBe("user-2") // still user-2, not user-1
102
+ expect(resource.data()).toBe('user-2') // still user-2, not user-1
103
103
  })
104
104
 
105
- test("ignores stale errors (race condition)", async () => {
105
+ test('ignores stale errors (race condition)', async () => {
106
106
  const userId = signal(1)
107
107
  const rejecters: ((e: Error) => void)[] = []
108
108
  const resolvers: ((v: string) => void)[] = []
@@ -120,18 +120,18 @@ describe("createResource", () => {
120
120
  userId.set(2)
121
121
 
122
122
  // Resolve second request
123
- resolvers[1]?.("user-2")
123
+ resolvers[1]?.('user-2')
124
124
  await new Promise((r) => setTimeout(r, 10))
125
- expect(resource.data()).toBe("user-2")
125
+ expect(resource.data()).toBe('user-2')
126
126
 
127
127
  // Reject first (stale) request — should be ignored
128
- rejecters[0]?.(new Error("stale error"))
128
+ rejecters[0]?.(new Error('stale error'))
129
129
  await new Promise((r) => setTimeout(r, 10))
130
130
  expect(resource.error()).toBeUndefined()
131
- expect(resource.data()).toBe("user-2")
131
+ expect(resource.data()).toBe('user-2')
132
132
  })
133
133
 
134
- test("loading returns to true on refetch", async () => {
134
+ test('loading returns to true on refetch', async () => {
135
135
  const userId = signal(1)
136
136
  const resource = createResource(
137
137
  () => userId(),
@@ -140,7 +140,7 @@ describe("createResource", () => {
140
140
 
141
141
  await new Promise((r) => setTimeout(r, 10))
142
142
  expect(resource.loading()).toBe(false)
143
- expect(resource.data()).toBe("user-1")
143
+ expect(resource.data()).toBe('user-1')
144
144
 
145
145
  resource.refetch()
146
146
  expect(resource.loading()).toBe(true)
@@ -149,12 +149,12 @@ describe("createResource", () => {
149
149
  expect(resource.loading()).toBe(false)
150
150
  })
151
151
 
152
- test("error is cleared on successful refetch", async () => {
152
+ test('error is cleared on successful refetch', async () => {
153
153
  let shouldFail = true
154
154
  const src = signal(1)
155
155
  const resource = createResource(
156
156
  () => src(),
157
- (_id) => (shouldFail ? Promise.reject(new Error("fail")) : Promise.resolve("ok")),
157
+ (_id) => (shouldFail ? Promise.reject(new Error('fail')) : Promise.resolve('ok')),
158
158
  )
159
159
 
160
160
  await new Promise((r) => setTimeout(r, 10))
@@ -164,18 +164,18 @@ describe("createResource", () => {
164
164
  resource.refetch()
165
165
  await new Promise((r) => setTimeout(r, 10))
166
166
  expect(resource.error()).toBeUndefined()
167
- expect(resource.data()).toBe("ok")
167
+ expect(resource.data()).toBe('ok')
168
168
  })
169
169
 
170
- test("error is cleared before each fetch attempt", async () => {
170
+ test('error is cleared before each fetch attempt', async () => {
171
171
  let callCount = 0
172
172
  const src = signal(1)
173
173
  const resource = createResource(
174
174
  () => src(),
175
175
  (_id) => {
176
176
  callCount++
177
- if (callCount === 1) return Promise.reject(new Error("first fail"))
178
- return Promise.resolve("success")
177
+ if (callCount === 1) return Promise.reject(new Error('first fail'))
178
+ return Promise.resolve('success')
179
179
  },
180
180
  )
181
181
 
@@ -189,14 +189,14 @@ describe("createResource", () => {
189
189
  expect(resource.loading()).toBe(true)
190
190
 
191
191
  await new Promise((r) => setTimeout(r, 10))
192
- expect(resource.data()).toBe("success")
192
+ expect(resource.data()).toBe('success')
193
193
  })
194
194
 
195
- test("data is undefined initially and after error", async () => {
195
+ test('data is undefined initially and after error', async () => {
196
196
  const src = signal(1)
197
197
  const resource = createResource(
198
198
  () => src(),
199
- (_id) => Promise.reject(new Error("always fails")),
199
+ (_id) => Promise.reject(new Error('always fails')),
200
200
  )
201
201
 
202
202
  expect(resource.data()).toBeUndefined()
@@ -206,7 +206,7 @@ describe("createResource", () => {
206
206
  expect(resource.error()).toBeInstanceOf(Error)
207
207
  })
208
208
 
209
- test("refetch uses current source value", async () => {
209
+ test('refetch uses current source value', async () => {
210
210
  const src = signal(1)
211
211
  const results: string[] = []
212
212
  const resource = createResource(
@@ -219,15 +219,15 @@ describe("createResource", () => {
219
219
  )
220
220
 
221
221
  await new Promise((r) => setTimeout(r, 10))
222
- expect(results).toEqual(["user-1"])
222
+ expect(results).toEqual(['user-1'])
223
223
 
224
224
  src.set(5)
225
225
  await new Promise((r) => setTimeout(r, 10))
226
- expect(results).toEqual(["user-1", "user-5"])
226
+ expect(results).toEqual(['user-1', 'user-5'])
227
227
 
228
228
  // Refetch should use current source value (5)
229
229
  resource.refetch()
230
230
  await new Promise((r) => setTimeout(r, 10))
231
- expect(results).toEqual(["user-1", "user-5", "user-5"])
231
+ expect(results).toEqual(['user-1', 'user-5', 'user-5'])
232
232
  })
233
233
  })