jrx 0.2.1 → 0.3.0-alpha.2

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/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # jrx
2
2
 
3
- A lightweight TypeScript library for managing side effects, subscriptions, and animations with automatic cleanup. Built on top of [jdisposer](https://github.com/tranvansang/jdisposer) for safe resource management.
3
+ A lightweight TypeScript library for managing side effects, subscriptions, and animations with automatic cleanup using the [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) API.
4
+
5
+ ## Prerequisites
6
+
7
+ This library requires the [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) globals (`DisposableStack`, `AsyncDisposableStack`, `Symbol.dispose`, `Symbol.asyncDispose`). If your environment does not support them natively, you must load a polyfill before importing jrx (e.g. [`core-js`](https://github.com/nicolo-ribaudo/tc39-proposal-explicit-resource-management-polyfill)).
8
+
9
+ The `using` keyword is **not** required — this library only uses the API objects directly, so no transpiler support for `using` declarations is needed.
4
10
 
5
11
  ## Installation
6
12
 
@@ -11,134 +17,141 @@ npm i jrx
11
17
  ## Features
12
18
 
13
19
  - Automatic cleanup for all effects and subscriptions
14
- - Type-safe disposer pattern
20
+ - Built on the native `DisposableStack` / `AsyncDisposableStack` API
15
21
  - Retry logic with exponential backoff and cancellation
16
- - Single dependency (jdisposer)
22
+ - Zero dependencies
17
23
  - Composable reactive utilities
18
24
  - Browser and Node.js compatible
19
25
 
20
26
  ## API Overview
21
27
 
28
+ - [`makeReset()`](#makereset) - Create a resettable `DisposableStack`
29
+ - [`makeAsyncReset()`](#makeasyncreset) - Create a resettable `AsyncDisposableStack`
22
30
  - [`makeRenderLoop()`](#makerenderloop) - Render loops with automatic cleanup
23
- - [`addEvtListener(target, event, handler, option?)`](#addevtlistenertarget-event-handler-option) - Event listeners with cleanup
24
- - [`addInterval(cb, ms)`](#addintervalcb-ms) - Repeating intervals with cleanup
25
- - [`addIntervalAsync(cb, ms)`](#addintervalasynccb-ms) - Async intervals with cancellation
26
- - [`addRequestAnimationFrame(cb)`](#addrequestanimationframecb) - Single animation frame with cleanup
27
- - [`addRequestAnimationFrameLoop(cb)`](#addrequestanimationframeloopcb) - Animation frame loops
28
- - [`addSubs(subs, cb, options?)`](#addsubssubs-cb-options) - Multiple subscription management
29
- - [`addTimeout(cb, ms)`](#addtimeoutcb-ms) - Timeouts with cleanup
30
- - [`addTransition(cb, durationMs)`](#addtransitioncb-durationms) - Progress-based animations
31
+ - [`makeInterval(cb, ms)`](#makeintervalcb-ms) - Repeating intervals with cleanup
32
+ - [`makeIntervalAsync(cb, ms)`](#makeintervalasynccb-ms) - Async intervals with cancellation
33
+ - [`makeAnimationFrame(cb)`](#makeanimationframecb) - Single animation frame with cleanup
34
+ - [`makeAnimationFrameLoop(cb)`](#makeanimationframeloopcb) - Animation frame loops
35
+ - [`makeTimeout(cb, ms)`](#maketimeoutcb-ms) - Timeouts with cleanup
36
+ - [`makeTransition(cb, durationMs)`](#maketransitioncb-durationms) - Progress-based animations
31
37
  - [`computed(fn, getDeps?)`](#computedfn-getdeps) - Memoized computed values
32
- - [`retry(cb, options?)`](#retrycb-options) - Async retry with exponential backoff
33
- - [`addRetry(cb, options?)`](#addretrycb-options) - Fire-and-forget retry with disposal
38
+ - [`retry(cb, backoffSec?)`](#retrycb-backoffsec) - Retry with exponential backoff
34
39
 
35
40
  ## API
36
41
 
37
- ### `makeRenderLoop()`
42
+ ### `makeReset()`
38
43
 
39
- Creates a render loop with automatic cleanup management.
44
+ Creates a resettable `DisposableStack`. Each call disposes the previous stack and returns a new one.
40
45
 
41
46
  ```typescript
42
- import { makeRenderLoop } from 'jrx'
47
+ import {makeReset} from 'jrx'
43
48
 
44
- const { loop, setLoop } = makeRenderLoop()
49
+ const reset = makeReset()
50
+ const stack = reset() // Get a fresh DisposableStack
45
51
 
46
- // Set the loop function
47
- const dispose = setLoop((time) => {
48
- console.log('Frame time:', time)
52
+ // Add disposables
53
+ stack.use(someDisposable)
49
54
 
50
- // Optional: return cleanup function
51
- return () => {
52
- console.log('Cleanup previous frame')
53
- }
54
- })
55
+ // Reset - disposes previous stack, returns new one
56
+ const newStack = reset()
57
+ ```
55
58
 
56
- // Call loop on each animation frame
57
- requestAnimationFrame(loop)
59
+ ### `makeAsyncReset()`
58
60
 
59
- // Cleanup
60
- dispose()
61
+ Async version of `makeReset` using `AsyncDisposableStack`.
62
+
63
+ ```typescript
64
+ import {makeAsyncReset} from 'jrx'
65
+
66
+ const reset = makeAsyncReset()
67
+ const stack = await reset() // Get a fresh AsyncDisposableStack
61
68
  ```
62
69
 
63
- ### `addEvtListener(target, event, handler, option?)`
70
+ ### `makeRenderLoop()`
64
71
 
65
- Adds an event listener to a target and returns a disposer that removes it. Works with any object that implements `addEventListener`/`removeEventListener` (DOM elements, `window`, `document`, etc.).
72
+ Creates a render loop with automatic cleanup management.
66
73
 
67
74
  ```typescript
68
- import { addEvtListener } from 'jrx'
75
+ import {makeRenderLoop} from 'jrx'
69
76
 
70
- // Basic usage
71
- const dispose = addEvtListener(window, 'resize', (e) => {
72
- console.log('Window resized', e)
77
+ const {loop, setLoop} = makeRenderLoop()
78
+
79
+ // Set the loop function - returns a Disposable
80
+ const handle = setLoop((time) => {
81
+ console.log('Frame time:', time)
82
+
83
+ // Optional: return a Disposable for cleanup
84
+ return {
85
+ [Symbol.dispose]() {
86
+ console.log('Cleanup previous frame')
87
+ },
88
+ }
73
89
  })
74
90
 
75
- // With options
76
- const dispose2 = addEvtListener(element, 'click', (e) => {
77
- console.log('Clicked', e)
78
- }, { capture: true })
91
+ // Call loop on each animation frame
92
+ requestAnimationFrame(loop)
79
93
 
80
94
  // Cleanup
81
- dispose()
82
- dispose2()
95
+ handle[Symbol.dispose]()
83
96
  ```
84
97
 
85
- ### `addInterval(cb, ms)`
98
+ ### `makeInterval(cb, ms)`
86
99
 
87
- Creates a repeating interval with cleanup. The callback can optionally return a cleanup function that runs before the next invocation.
100
+ Creates a repeating interval with cleanup. The callback can optionally return a `Disposable` that is disposed before the next invocation. Returns a `Disposable`.
88
101
 
89
102
  **Note:** The callback fires **immediately** on first call, then waits `ms` milliseconds **after** the previous callback completes. This is not a fixed-rate timer.
90
103
 
91
104
  ```typescript
92
- import { addInterval } from 'jrx'
105
+ import {makeInterval} from 'jrx'
93
106
 
94
- const dispose = addInterval(() => {
107
+ const handle = makeInterval(() => {
95
108
  console.log('Tick') // Called immediately, then every 1000ms after completion
96
109
 
97
- // Optional: return cleanup function
98
- return () => {
99
- console.log('Cleanup')
110
+ // Optional: return a Disposable for cleanup
111
+ return {
112
+ [Symbol.dispose]() {
113
+ console.log('Cleanup')
114
+ },
100
115
  }
101
116
  }, 1000)
102
117
 
103
118
  // Stop the interval
104
- dispose()
119
+ handle[Symbol.dispose]()
105
120
  ```
106
121
 
107
- ### `addIntervalAsync(cb, ms)`
122
+ ### `makeIntervalAsync(cb, ms)`
108
123
 
109
- Async version of `addInterval`. Waits for the callback to complete before scheduling the next invocation.
124
+ Async version of `makeInterval`. Waits for the callback to complete before scheduling the next invocation.
110
125
 
111
126
  **Note:** The callback fires **immediately** on first call, then waits `ms` milliseconds **after** the previous async callback completes.
112
127
 
113
128
  ```typescript
114
- import { addIntervalAsync } from 'jrx'
129
+ import {makeIntervalAsync} from 'jrx'
115
130
 
116
- const dispose = addIntervalAsync(async (disposer) => {
131
+ const dispose = makeIntervalAsync(async () => {
117
132
  // Called immediately, then 5000ms after each completion
118
133
  await fetchData()
119
-
120
- // Check if disposed during async operation
121
- if (disposer.signal.aborted) return
122
-
123
134
  processData()
124
135
  }, 5000)
125
136
 
126
137
  dispose()
127
138
  ```
128
139
 
129
- ### `addRequestAnimationFrame(cb)`
140
+ ### `makeAnimationFrame(cb)`
130
141
 
131
142
  Executes a callback on the next animation frame with cleanup.
132
143
 
133
144
  ```typescript
134
- import { addRequestAnimationFrame } from 'jrx'
145
+ import {makeAnimationFrame} from 'jrx'
135
146
 
136
- const dispose = addRequestAnimationFrame((now) => {
147
+ const dispose = makeAnimationFrame((now) => {
137
148
  updateAnimation(now)
138
149
 
139
- // Optional: return cleanup function
140
- return () => {
141
- cleanupAnimation()
150
+ // Optional: return a Disposable for cleanup
151
+ return {
152
+ [Symbol.dispose]() {
153
+ cleanupAnimation()
154
+ },
142
155
  }
143
156
  })
144
157
 
@@ -146,19 +159,21 @@ const dispose = addRequestAnimationFrame((now) => {
146
159
  dispose()
147
160
  ```
148
161
 
149
- ### `addRequestAnimationFrameLoop(cb)`
162
+ ### `makeAnimationFrameLoop(cb)`
150
163
 
151
164
  Creates a continuous `requestAnimationFrame` loop with cleanup.
152
165
 
153
166
  ```typescript
154
- import { addRequestAnimationFrameLoop } from 'jrx'
167
+ import {makeAnimationFrameLoop} from 'jrx'
155
168
 
156
- const dispose = addRequestAnimationFrameLoop((now) => {
169
+ const dispose = makeAnimationFrameLoop((now) => {
157
170
  updateAnimation(now)
158
171
 
159
- // Optional: return cleanup function
160
- return () => {
161
- cleanupAnimation()
172
+ // Optional: return a Disposable for cleanup
173
+ return {
174
+ [Symbol.dispose]() {
175
+ cleanupAnimation()
176
+ },
162
177
  }
163
178
  })
164
179
 
@@ -166,43 +181,14 @@ const dispose = addRequestAnimationFrameLoop((now) => {
166
181
  dispose()
167
182
  ```
168
183
 
169
- ### `addSubs(subs, cb, options?)`
170
-
171
- Manages multiple subscriptions with a single callback.
172
-
173
- ```typescript
174
- import { addSubs } from 'jrx'
175
-
176
- const sub1 = (listener) => {
177
- eventEmitter.on('event1', listener)
178
- return () => eventEmitter.off('event1', listener)
179
- }
180
-
181
- const sub2 = (listener) => {
182
- eventEmitter.on('event2', listener)
183
- return () => eventEmitter.off('event2', listener)
184
- }
185
-
186
- const dispose = addSubs([sub1, sub2], () => {
187
- console.log('Any event fired')
188
-
189
- // Optional: return cleanup function
190
- return () => {
191
- console.log('Cleanup')
192
- }
193
- }, { now: true }) // Call immediately with now: true
194
-
195
- dispose()
196
- ```
197
-
198
- ### `addTimeout(cb, ms)`
184
+ ### `makeTimeout(cb, ms)`
199
185
 
200
186
  Creates a timeout with cleanup.
201
187
 
202
188
  ```typescript
203
- import { addTimeout } from 'jrx'
189
+ import {makeTimeout} from 'jrx'
204
190
 
205
- const cancel = addTimeout(() => {
191
+ const cancel = makeTimeout(() => {
206
192
  console.log('Timeout fired')
207
193
  }, 1000)
208
194
 
@@ -210,23 +196,25 @@ const cancel = addTimeout(() => {
210
196
  cancel()
211
197
  ```
212
198
 
213
- ### `addTransition(cb, durationMs)`
199
+ ### `makeTransition(cb, durationMs)`
214
200
 
215
- Creates an animation transition with progress tracking (0 to 1).
201
+ Creates an animation transition with progress tracking (0 to 1). Returns a `Disposable`.
216
202
 
217
203
  ```typescript
218
- import { addTransition } from 'jrx'
204
+ import {makeTransition} from 'jrx'
219
205
 
220
- const dispose = addTransition((progress) => {
206
+ const handle = makeTransition((progress) => {
221
207
  element.style.opacity = progress.toString()
222
208
 
223
- // Optional: return cleanup function
224
- return () => {
225
- console.log('Frame cleanup')
209
+ // Optional: return a Disposable for cleanup
210
+ return {
211
+ [Symbol.dispose]() {
212
+ console.log('Frame cleanup')
213
+ },
226
214
  }
227
215
  }, 1000)
228
216
 
229
- dispose()
217
+ handle[Symbol.dispose]()
230
218
  ```
231
219
 
232
220
  ### `computed(fn, getDeps?)`
@@ -234,7 +222,7 @@ dispose()
234
222
  Creates a memoized computed value with optional dependency tracking.
235
223
 
236
224
  ```typescript
237
- import { computed } from 'jrx'
225
+ import {computed} from 'jrx'
238
226
 
239
227
  // Without dependencies - always recomputes
240
228
  const value1 = computed(() => expensiveCalculation())
@@ -255,9 +243,9 @@ a = 10
255
243
  console.log(value2.value) // Recomputed: 12
256
244
  ```
257
245
 
258
- ### `retry(cb, options?)`
246
+ ### `retry(cb, backoffSec?)`
259
247
 
260
- Retries an async operation with exponential backoff on failure.
248
+ Retries an operation with exponential backoff on failure. Returns `Disposable & Promise<T>`.
261
249
 
262
250
  **Default backoff:** `[5, 5, 10, 10, 20, 20, 40, 40, 60, -1]` seconds (where `-1` means retry forever with 60s delay)
263
251
 
@@ -265,123 +253,65 @@ Retries an async operation with exponential backoff on failure.
265
253
  import {retry} from 'jrx'
266
254
 
267
255
  // Basic usage - retries with default backoff
268
- const result = await retry(async (disposer, { resetBackoff }) => {
269
- const response = await fetch('/api/data')
270
- if (!response.ok) throw new Error('Failed to fetch')
271
- return response.json()
256
+ const result = await retry(({resetBackoff}) => {
257
+ const promise = fetch('/api/data').then((r) => r.json())
258
+ return Object.assign(promise, {[Symbol.dispose]() {}})
272
259
  })
273
260
 
274
261
  // Custom backoff schedule (in seconds)
275
262
  await retry(
276
- async (disposer, { resetBackoff }) => {
277
- return await fetchData()
263
+ () => {
264
+ const promise = fetchData()
265
+ return Object.assign(promise, {[Symbol.dispose]() {}})
278
266
  },
279
- {
280
- backoffSec: [1, 2, 5, 10, -1] // -1 means retry forever with last delay
281
- }
267
+ [1, 2, 5, 10, -1], // -1 means retry forever with last delay
282
268
  )
283
269
 
284
- // With disposer for cancellation
285
- import { makeDisposer } from 'jdisposer'
286
-
287
- const disposer = makeDisposer()
288
-
289
- const data = await retry(
290
- async (loopDisposer, { resetBackoff }) => {
291
- // Check if aborted
292
- if (loopDisposer.signal.aborted) return
293
-
294
- const result = await fetchData()
295
-
296
- // Reset backoff on successful partial progress
297
- if (result.isPartialSuccess) {
298
- resetBackoff()
299
- }
300
-
301
- return result
270
+ // Cancellation via Disposable
271
+ const r = retry(
272
+ ({resetBackoff}) => {
273
+ const promise = fetchData()
274
+ return Object.assign(promise, {[Symbol.dispose]() { /* cancel */ }})
302
275
  },
303
- {
304
- disposer,
305
- backoffSec: [5, 10, 20, 40, -1]
306
- }
276
+ [5, 10, 20, 40, -1],
307
277
  )
308
278
 
309
279
  // Cancel the retry loop
310
- disposer.dispose()
280
+ r[Symbol.dispose]()
311
281
 
312
282
  // Returns undefined when disposed
313
- console.log(data) // T | undefined
314
- ```
315
-
316
- **Options:**
317
- - `backoffSec`: Array of retry delays in seconds. Use `-1` for infinite retries with the last delay.
318
- - Default: `[5, 5, 10, 10, 20, 20, 40, 40, 60, -1]`
319
- - `disposer`: Optional disposer for cancellation. When provided, the return type is `T | undefined`. Otherwise, the return type is `T`.
320
-
321
- **Callback parameters:**
322
- - `disposer`: A disposer for the current retry attempt. Check `disposer.signal.aborted` to handle cancellation
323
- - `info.resetBackoff()`: Call this to reset the backoff counter to the beginning (useful when making partial progress)
324
-
325
- ### `addRetry(cb, options?)`
326
-
327
- Fire-and-forget version of `retry`. Starts the retry loop in the background and returns a dispose function to cancel it.
328
-
329
- ```typescript
330
- import {addRetry} from 'jrx'
331
-
332
- // Start a retry loop in the background
333
- const dispose = addRetry(async (disposer, { resetBackoff }) => {
334
- const response = await fetch('/api/data')
335
- if (!response.ok) throw new Error('Failed')
336
- processData(await response.json())
337
- })
338
-
339
- // Cancel the retry loop
340
- dispose()
341
-
342
- // With custom backoff
343
- const dispose2 = addRetry(
344
- async (disposer) => {
345
- await connectWebSocket()
346
- },
347
- { backoffSec: [1, 2, 5, -1] }
348
- )
349
-
350
- dispose2()
283
+ const data = await r // undefined
351
284
  ```
352
285
 
353
- **Options:**
354
- - `backoffSec`: Array of retry delays in seconds (same as `retry`)
286
+ **Parameters:**
287
+ - `cb`: Callback that returns `Disposable & (T | Promise<T>)`. Receives `{ resetBackoff() }` to reset the backoff counter.
288
+ - `backoffSec`: Array of retry delays in seconds. Use `-1` for infinite retries with the last delay. Default: `[5, 5, 10, 10, 20, 20, 40, 40, 60, -1]`
355
289
 
356
290
  ## Cleanup Pattern
357
291
 
358
- All functions return disposer functions that clean up resources:
292
+ Functions that manage ongoing effects return either a dispose function or a `Disposable` object:
359
293
 
360
294
  ```typescript
361
- import {addInterval, addTimeout, addRequestAnimationFrame} from 'jrx'
362
- import {makeDisposer} from 'jdisposer'
295
+ import {makeInterval, makeTimeout, makeAnimationFrame, makeTransition} from 'jrx'
296
+
297
+ // Functions returning Disposable objects (use with `using` or call [Symbol.dispose]())
298
+ const interval = makeInterval(() => console.log('tick'), 1000)
299
+ const transition = makeTransition((p) => console.log(p), 1000)
363
300
 
364
- const disposer = makeDisposer()
301
+ interval[Symbol.dispose]()
302
+ transition[Symbol.dispose]()
365
303
 
366
- // Collect disposers
367
- disposer.add(addInterval(() => console.log('tick'), 1000))
368
- disposer.add(addTimeout(() => console.log('timeout'), 5000))
369
- disposer.add(addRequestAnimationFrameLoop((now) => render(now)))
304
+ // Functions returning dispose functions (call directly)
305
+ const disposeTimeout = makeTimeout(() => console.log('timeout'), 5000)
306
+ const disposeRaf = makeAnimationFrame((now) => render(now))
370
307
 
371
- // Cleanup all at once
372
- disposer.dispose()
308
+ disposeTimeout()
309
+ disposeRaf()
373
310
  ```
374
311
 
375
312
  ## TypeScript
376
313
 
377
- This library is written in TypeScript and includes type definitions.
378
-
379
- ```typescript
380
- import type { Disposer } from 'jdisposer'
381
-
382
- // All disposer functions follow this pattern
383
- type DisposerFunction = () => void
384
- ```
314
+ This library is written in TypeScript and uses the [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management) types (`Disposable`, `DisposableStack`, `AsyncDisposableStack`).
385
315
 
386
316
  ## License
387
317
 
package/index.d.ts CHANGED
@@ -1,18 +1,21 @@
1
- import { type Disposer } from 'jdisposer';
2
- import retry, { addRetry } from './retry.js';
1
+ import retry from './retry.js';
3
2
  import computed from './computed.js';
4
- import addEvtListener from './addEvtListener.js';
5
- export { retry, computed, addEvtListener, addRetry };
3
+ export { retry, computed };
4
+ export declare function makeReset(): () => DisposableStack;
5
+ export declare function makeAsyncReset(): () => Promise<AsyncDisposableStack>;
6
6
  export declare function makeRenderLoop(): {
7
7
  loop(this: void, time: DOMHighResTimeStamp): void;
8
- setLoop(this: void, loop: (time: DOMHighResTimeStamp) => void | (() => void)): () => void;
8
+ setLoop(this: void, loop: (time: DOMHighResTimeStamp) => undefined | Disposable): {
9
+ [Symbol.dispose](): void;
10
+ };
11
+ };
12
+ export declare function makeInterval(cb: () => undefined | Disposable, ms: number): {
13
+ [Symbol.dispose](): void;
14
+ };
15
+ export declare function makeIntervalAsync(cb: () => void | Disposable | Promise<void>, ms: number): () => void;
16
+ export declare function makeAnimationFrame(cb: (now: DOMHighResTimeStamp) => undefined | Disposable): () => void;
17
+ export declare function makeAnimationFrameLoop(cb: (now: DOMHighResTimeStamp) => undefined | Disposable): () => void;
18
+ export declare function makeTimeout(cb: () => void, ms: number): () => void;
19
+ export declare function makeTransition(cb: (progress: number) => undefined | Disposable, durationMs: number): {
20
+ [Symbol.dispose](): void;
9
21
  };
10
- export declare function addInterval(cb: () => void | (() => any), ms: number): () => void;
11
- export declare function addIntervalAsync(cb: (disposer: Disposer) => void | (() => any) | Promise<void> | Promise<() => any>, ms: number): () => void;
12
- export declare function addRequestAnimationFrame(cb: (now: DOMHighResTimeStamp) => void | (() => any)): () => void;
13
- export declare function addRequestAnimationFrameLoop(cb: (now: DOMHighResTimeStamp) => void | (() => any)): () => void;
14
- export declare function addSubs<Subs extends any[]>(subs: Subs, cb: () => void | (() => void), { now }?: {
15
- now?: boolean;
16
- }): (this: void) => void;
17
- export declare function addTimeout(cb: () => void, ms: number): () => void;
18
- export declare function addTransition(cb: (progress: number) => void | (() => void), durationMs: number): () => void;
package/index.js CHANGED
@@ -1,38 +1,54 @@
1
- import { makeDisposer, makeReset } from 'jdisposer';
2
- import retry, { addRetry } from './retry.js';
1
+ import retry from './retry.js';
3
2
  import computed from './computed.js';
4
- import addEvtListener from './addEvtListener.js';
5
- export { retry, computed, addEvtListener, addRetry };
3
+ export { retry, computed };
4
+ export function makeReset() {
5
+ let stack = new DisposableStack();
6
+ return () => {
7
+ stack.dispose();
8
+ return (stack = new DisposableStack());
9
+ };
10
+ }
11
+ export function makeAsyncReset() {
12
+ let stack = new AsyncDisposableStack();
13
+ return async () => {
14
+ await stack.disposeAsync();
15
+ return (stack = new AsyncDisposableStack());
16
+ };
17
+ }
6
18
  export function makeRenderLoop() {
7
19
  let loop_;
8
20
  const reset = makeReset();
9
21
  return {
10
22
  loop(time) {
11
- reset().add(loop_?.(time));
23
+ reset().use(loop_?.(time));
12
24
  },
13
25
  setLoop(loop) {
14
26
  loop_ = loop;
15
- return () => {
16
- reset();
17
- loop_ = undefined;
27
+ return {
28
+ [Symbol.dispose]() {
29
+ reset();
30
+ loop_ = undefined;
31
+ },
18
32
  };
19
33
  },
20
34
  };
21
35
  }
22
- export function addInterval(cb, ms) {
36
+ export function makeInterval(cb, ms) {
23
37
  const reset = makeReset();
24
38
  let timeout;
25
39
  wrapper();
26
- return () => {
27
- reset();
28
- clearTimeout(timeout);
40
+ return {
41
+ [Symbol.dispose]() {
42
+ reset();
43
+ clearTimeout(timeout);
44
+ },
29
45
  };
30
46
  function wrapper() {
31
- reset().add(cb());
47
+ reset().use(cb());
32
48
  timeout = setTimeout(wrapper, ms);
33
49
  }
34
50
  }
35
- export function addIntervalAsync(cb, ms) {
51
+ export function makeIntervalAsync(cb, ms) {
36
52
  const reset = makeReset();
37
53
  let timeout;
38
54
  void wrapper();
@@ -41,21 +57,25 @@ export function addIntervalAsync(cb, ms) {
41
57
  clearTimeout(timeout);
42
58
  };
43
59
  async function wrapper() {
44
- const disposer = reset();
45
- await cb(disposer);
46
- if (!disposer.signal.aborted)
60
+ const stack = reset();
61
+ await stack.adopt(cb(), (v) => v?.[Symbol.dispose]?.());
62
+ if (!stack.disposed)
47
63
  timeout = setTimeout(wrapper, ms);
48
64
  }
49
65
  }
50
- export function addRequestAnimationFrame(cb) {
51
- const disposer = makeDisposer();
52
- const raf = requestAnimationFrame(now => disposer.add(cb(now)));
66
+ export function makeAnimationFrame(cb) {
67
+ const stack = new DisposableStack();
68
+ const raf = requestAnimationFrame(now => {
69
+ if (stack.disposed)
70
+ return;
71
+ stack.use(cb(now));
72
+ });
53
73
  return () => {
54
- disposer.dispose();
74
+ stack.dispose();
55
75
  cancelAnimationFrame(raf);
56
76
  };
57
77
  }
58
- export function addRequestAnimationFrameLoop(cb) {
78
+ export function makeAnimationFrameLoop(cb) {
59
79
  const reset = makeReset();
60
80
  let raf = requestAnimationFrame(wrapper);
61
81
  return () => {
@@ -63,44 +83,36 @@ export function addRequestAnimationFrameLoop(cb) {
63
83
  cancelAnimationFrame(raf);
64
84
  };
65
85
  function wrapper(now) {
66
- reset().add(cb(now));
86
+ reset().use(cb(now));
67
87
  raf = requestAnimationFrame(wrapper);
68
88
  }
69
89
  }
70
- export function addSubs(subs, cb, { now } = {}) {
71
- const disposer = makeDisposer();
72
- const reset = makeReset();
73
- disposer.add(reset);
74
- for (const sub of subs)
75
- disposer.add(sub(() => reset().add(cb())));
76
- if (now)
77
- reset().add(cb());
78
- return disposer.dispose;
79
- }
80
- export function addTimeout(cb, ms) {
90
+ export function makeTimeout(cb, ms) {
81
91
  const timeout = setTimeout(cb, ms);
82
92
  return () => clearTimeout(timeout);
83
93
  }
84
- export function addTransition(cb, durationMs) {
94
+ export function makeTransition(cb, durationMs) {
85
95
  const reset = makeReset();
86
96
  let start;
87
97
  let raf = requestAnimationFrame(wrapper);
88
- return () => {
89
- reset();
90
- cancelAnimationFrame(raf);
98
+ return {
99
+ [Symbol.dispose]() {
100
+ reset();
101
+ cancelAnimationFrame(raf);
102
+ },
91
103
  };
92
104
  function wrapper(now) {
93
105
  if (start === undefined) {
94
106
  start = now;
95
- reset().add(cb(0));
107
+ reset().use(cb(0));
96
108
  raf = requestAnimationFrame(wrapper);
97
109
  }
98
110
  else {
99
111
  const progress = (now - start) / durationMs;
100
112
  if (progress >= 1)
101
- reset().add(cb(1));
113
+ reset().use(cb(1));
102
114
  else {
103
- reset().add(cb(progress));
115
+ reset().use(cb(progress));
104
116
  raf = requestAnimationFrame(wrapper);
105
117
  }
106
118
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "jrx",
3
- "version": "0.2.1",
3
+ "version": "0.3.0-alpha.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "prepublishOnly": "npx tsc",
7
- "test": "node --test tests/*.test.ts",
8
- "test:coverage": "node --test --experimental-test-coverage tests/*.test.ts"
7
+ "test": "npm run prepublishOnly && node --test tests/*.test.ts",
8
+ "test:coverage": "npm run prepublishOnly && node --test --experimental-test-coverage tests/*.test.ts"
9
9
  },
10
10
  "exports": {
11
11
  ".": {
@@ -23,15 +23,10 @@
23
23
  "retry.js",
24
24
  "retry.d.ts",
25
25
  "computed.js",
26
- "computed.d.ts",
27
- "addEvtListener.js",
28
- "addEvtListener.d.ts"
26
+ "computed.d.ts"
29
27
  ],
30
28
  "devDependencies": {
31
29
  "@types/node": "^25.2.1",
32
- "typescript": "^5.9.3"
33
- },
34
- "dependencies": {
35
- "jdisposer": "^0.0.1"
30
+ "typescript": "^6.0.2"
36
31
  }
37
32
  }
package/retry.d.ts CHANGED
@@ -1,17 +1,3 @@
1
- import { type Disposer } from 'jdisposer';
2
- export default function retry<T>(cb: (disposer: Disposer, info: {
1
+ export default function retry<T>(cb: (info: {
3
2
  resetBackoff(): void;
4
- }) => T | Promise<T>, options?: {
5
- backoffSec?: number[];
6
- }): Promise<T>;
7
- export default function retry<T>(cb: (disposer: Disposer, info: {
8
- resetBackoff(): void;
9
- }) => T | Promise<T>, options?: {
10
- backoffSec?: number[];
11
- disposer: Disposer;
12
- }): Promise<T | undefined>;
13
- export declare function addRetry<T>(cb: (disposer: Disposer, info: {
14
- resetBackoff(): void;
15
- }) => T | Promise<T>, options?: {
16
- backoffSec?: number[];
17
- }): (this: void) => void;
3
+ }) => (Disposable | undefined) & (T | Promise<T>), backoffSec?: number[]): Disposable & Promise<T | undefined>;
package/retry.js CHANGED
@@ -1,37 +1,35 @@
1
- import { makeDisposer, makeReset } from 'jdisposer';
2
- export default async function retry(cb, { backoffSec = [5, 5, 10, 10, 20, 20, 40, 40, 60, -1], // -1: retry forever with the last backoff . first element must not be -1
3
- disposer, } = {}) {
1
+ import { makeReset } from './index.js';
2
+ export default function retry(cb, backoffSec = [5, 5, 10, 10, 20, 20, 40, 40, 60, -1]) {
3
+ const stack = new DisposableStack();
4
4
  const reset = makeReset();
5
- disposer?.add(reset);
5
+ stack.defer(reset);
6
6
  let count = 0;
7
- let loopDisposer = reset();
8
- while (true) {
9
- if (backoffSec[count] !== -1)
10
- count++;
11
- try {
12
- if (loopDisposer?.signal.aborted)
13
- return; // only happen if disposer?.signal.aborted
14
- return await cb(loopDisposer, {
15
- resetBackoff() {
16
- count = 1;
17
- },
18
- });
19
- }
20
- catch (e) {
21
- if (disposer?.signal.aborted)
22
- return;
23
- if (count > backoffSec.length) {
24
- console.error('max retries reached:', e);
25
- throw e;
7
+ let loopStack = reset();
8
+ return Object.assign((async () => {
9
+ while (true) {
10
+ if (backoffSec[count] !== -1)
11
+ count++;
12
+ try {
13
+ if (loopStack.disposed)
14
+ return;
15
+ const value = cb({
16
+ resetBackoff() {
17
+ count = 1;
18
+ },
19
+ });
20
+ return value?.[Symbol.dispose] ? loopStack.use(value) : value;
21
+ }
22
+ catch (e) {
23
+ if (stack.disposed)
24
+ return;
25
+ if (count > backoffSec.length) {
26
+ console.error('max retries reached:', e);
27
+ throw e;
28
+ }
29
+ console.warn('Retrying due to error:', e);
30
+ loopStack = reset();
31
+ await new Promise(resolve => setTimeout(resolve, backoffSec[count - 1] * 1000));
26
32
  }
27
- console.warn('Retrying due to error:', e);
28
- loopDisposer = reset();
29
- await new Promise(resolve => setTimeout(resolve, backoffSec[count - 1] * 1000));
30
33
  }
31
- }
32
- }
33
- export function addRetry(cb, options) {
34
- const disposer = makeDisposer();
35
- void retry(cb, { ...options, disposer });
36
- return disposer.dispose;
34
+ })(), { [Symbol.dispose]: stack[Symbol.dispose].bind(stack) });
37
35
  }
@@ -1,15 +0,0 @@
1
- type IEventHandler<Target extends {
2
- addEventListener(event: string, handler: any, option?: any): any;
3
- }, EventName> = Target['addEventListener'] extends (event: EventName, handler: infer Handler, ...args: any[]) => any ? Handler extends (...params: any[]) => any ? Handler : never : never;
4
- type IEventOption<Target extends {
5
- addEventListener(event: string, handler: any, option?: any): any;
6
- }, EventName, Handler> = Target['addEventListener'] extends (event: EventName, handler: Handler, option: infer Option) => any ? Option : never;
7
- export default function addEvtListener<Target extends {
8
- addEventListener(event: string, handler: any, option?: any): any;
9
- removeEventListener(event: string, handler: any, option?: any): any;
10
- }, EventName extends Parameters<Target['addEventListener']>[0], Handler extends IEventHandler<Target, EventName>>(target: Target, event: EventName, handler: Handler, option?: IEventOption<Target, EventName, Handler>): () => void;
11
- export default function addEvtListener<Target extends {
12
- addEventListener(event: string, handler: any, option?: any): any;
13
- removeEventListener(event: string, handler: any, option?: any): any;
14
- }, EventName extends Parameters<Target['addEventListener']>[0]>(target: Target, event: EventName, handler: (...args: any[]) => any, option?: IEventOption<Target, EventName, IEventHandler<Target, EventName>>): () => void;
15
- export {};
package/addEvtListener.js DELETED
@@ -1,6 +0,0 @@
1
- export default function addEvtListener(target, event, handler, option) {
2
- target.addEventListener(event, handler, option);
3
- return function removeEvtListener() {
4
- return target.removeEventListener(event, handler, option);
5
- };
6
- }