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 +132 -202
- package/index.d.ts +17 -14
- package/index.js +53 -41
- package/package.json +5 -10
- package/retry.d.ts +2 -16
- package/retry.js +30 -32
- package/addEvtListener.d.ts +0 -15
- package/addEvtListener.js +0 -6
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
|
|
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
|
-
-
|
|
20
|
+
- Built on the native `DisposableStack` / `AsyncDisposableStack` API
|
|
15
21
|
- Retry logic with exponential backoff and cancellation
|
|
16
|
-
-
|
|
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
|
-
- [`
|
|
24
|
-
- [`
|
|
25
|
-
- [`
|
|
26
|
-
- [`
|
|
27
|
-
- [`
|
|
28
|
-
- [`
|
|
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,
|
|
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
|
-
### `
|
|
42
|
+
### `makeReset()`
|
|
38
43
|
|
|
39
|
-
Creates a
|
|
44
|
+
Creates a resettable `DisposableStack`. Each call disposes the previous stack and returns a new one.
|
|
40
45
|
|
|
41
46
|
```typescript
|
|
42
|
-
import {
|
|
47
|
+
import {makeReset} from 'jrx'
|
|
43
48
|
|
|
44
|
-
const
|
|
49
|
+
const reset = makeReset()
|
|
50
|
+
const stack = reset() // Get a fresh DisposableStack
|
|
45
51
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
console.log('Frame time:', time)
|
|
52
|
+
// Add disposables
|
|
53
|
+
stack.use(someDisposable)
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
+
// Reset - disposes previous stack, returns new one
|
|
56
|
+
const newStack = reset()
|
|
57
|
+
```
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
requestAnimationFrame(loop)
|
|
59
|
+
### `makeAsyncReset()`
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
### `
|
|
70
|
+
### `makeRenderLoop()`
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
Creates a render loop with automatic cleanup management.
|
|
66
73
|
|
|
67
74
|
```typescript
|
|
68
|
-
import {
|
|
75
|
+
import {makeRenderLoop} from 'jrx'
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
//
|
|
76
|
-
|
|
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
|
-
### `
|
|
98
|
+
### `makeInterval(cb, ms)`
|
|
86
99
|
|
|
87
|
-
Creates a repeating interval with cleanup. The callback can optionally return a
|
|
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 {
|
|
105
|
+
import {makeInterval} from 'jrx'
|
|
93
106
|
|
|
94
|
-
const
|
|
107
|
+
const handle = makeInterval(() => {
|
|
95
108
|
console.log('Tick') // Called immediately, then every 1000ms after completion
|
|
96
109
|
|
|
97
|
-
// Optional: return cleanup
|
|
98
|
-
return
|
|
99
|
-
|
|
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
|
-
### `
|
|
122
|
+
### `makeIntervalAsync(cb, ms)`
|
|
108
123
|
|
|
109
|
-
Async version of `
|
|
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 {
|
|
129
|
+
import {makeIntervalAsync} from 'jrx'
|
|
115
130
|
|
|
116
|
-
const dispose =
|
|
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
|
-
### `
|
|
140
|
+
### `makeAnimationFrame(cb)`
|
|
130
141
|
|
|
131
142
|
Executes a callback on the next animation frame with cleanup.
|
|
132
143
|
|
|
133
144
|
```typescript
|
|
134
|
-
import {
|
|
145
|
+
import {makeAnimationFrame} from 'jrx'
|
|
135
146
|
|
|
136
|
-
const dispose =
|
|
147
|
+
const dispose = makeAnimationFrame((now) => {
|
|
137
148
|
updateAnimation(now)
|
|
138
149
|
|
|
139
|
-
// Optional: return cleanup
|
|
140
|
-
return
|
|
141
|
-
|
|
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
|
-
### `
|
|
162
|
+
### `makeAnimationFrameLoop(cb)`
|
|
150
163
|
|
|
151
164
|
Creates a continuous `requestAnimationFrame` loop with cleanup.
|
|
152
165
|
|
|
153
166
|
```typescript
|
|
154
|
-
import {
|
|
167
|
+
import {makeAnimationFrameLoop} from 'jrx'
|
|
155
168
|
|
|
156
|
-
const dispose =
|
|
169
|
+
const dispose = makeAnimationFrameLoop((now) => {
|
|
157
170
|
updateAnimation(now)
|
|
158
171
|
|
|
159
|
-
// Optional: return cleanup
|
|
160
|
-
return
|
|
161
|
-
|
|
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
|
-
### `
|
|
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 {
|
|
189
|
+
import {makeTimeout} from 'jrx'
|
|
204
190
|
|
|
205
|
-
const cancel =
|
|
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
|
-
### `
|
|
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 {
|
|
204
|
+
import {makeTransition} from 'jrx'
|
|
219
205
|
|
|
220
|
-
const
|
|
206
|
+
const handle = makeTransition((progress) => {
|
|
221
207
|
element.style.opacity = progress.toString()
|
|
222
208
|
|
|
223
|
-
// Optional: return cleanup
|
|
224
|
-
return
|
|
225
|
-
|
|
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 {
|
|
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,
|
|
246
|
+
### `retry(cb, backoffSec?)`
|
|
259
247
|
|
|
260
|
-
Retries an
|
|
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(
|
|
269
|
-
const
|
|
270
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
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
|
-
|
|
280
|
+
r[Symbol.dispose]()
|
|
311
281
|
|
|
312
282
|
// Returns undefined when disposed
|
|
313
|
-
|
|
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
|
-
**
|
|
354
|
-
- `
|
|
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
|
-
|
|
292
|
+
Functions that manage ongoing effects return either a dispose function or a `Disposable` object:
|
|
359
293
|
|
|
360
294
|
```typescript
|
|
361
|
-
import {
|
|
362
|
-
|
|
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
|
-
|
|
301
|
+
interval[Symbol.dispose]()
|
|
302
|
+
transition[Symbol.dispose]()
|
|
365
303
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
308
|
+
disposeTimeout()
|
|
309
|
+
disposeRaf()
|
|
373
310
|
```
|
|
374
311
|
|
|
375
312
|
## TypeScript
|
|
376
313
|
|
|
377
|
-
This library is written in TypeScript and
|
|
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
|
|
2
|
-
import retry, { addRetry } from './retry.js';
|
|
1
|
+
import retry from './retry.js';
|
|
3
2
|
import computed from './computed.js';
|
|
4
|
-
|
|
5
|
-
export
|
|
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) =>
|
|
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
|
|
2
|
-
import retry, { addRetry } from './retry.js';
|
|
1
|
+
import retry from './retry.js';
|
|
3
2
|
import computed from './computed.js';
|
|
4
|
-
|
|
5
|
-
export
|
|
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().
|
|
23
|
+
reset().use(loop_?.(time));
|
|
12
24
|
},
|
|
13
25
|
setLoop(loop) {
|
|
14
26
|
loop_ = loop;
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
return {
|
|
28
|
+
[Symbol.dispose]() {
|
|
29
|
+
reset();
|
|
30
|
+
loop_ = undefined;
|
|
31
|
+
},
|
|
18
32
|
};
|
|
19
33
|
},
|
|
20
34
|
};
|
|
21
35
|
}
|
|
22
|
-
export function
|
|
36
|
+
export function makeInterval(cb, ms) {
|
|
23
37
|
const reset = makeReset();
|
|
24
38
|
let timeout;
|
|
25
39
|
wrapper();
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
return {
|
|
41
|
+
[Symbol.dispose]() {
|
|
42
|
+
reset();
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
},
|
|
29
45
|
};
|
|
30
46
|
function wrapper() {
|
|
31
|
-
reset().
|
|
47
|
+
reset().use(cb());
|
|
32
48
|
timeout = setTimeout(wrapper, ms);
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
|
-
export function
|
|
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
|
|
45
|
-
await cb(
|
|
46
|
-
if (!
|
|
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
|
|
51
|
-
const
|
|
52
|
-
const raf = requestAnimationFrame(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
|
-
|
|
74
|
+
stack.dispose();
|
|
55
75
|
cancelAnimationFrame(raf);
|
|
56
76
|
};
|
|
57
77
|
}
|
|
58
|
-
export function
|
|
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().
|
|
86
|
+
reset().use(cb(now));
|
|
67
87
|
raf = requestAnimationFrame(wrapper);
|
|
68
88
|
}
|
|
69
89
|
}
|
|
70
|
-
export function
|
|
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
|
|
94
|
+
export function makeTransition(cb, durationMs) {
|
|
85
95
|
const reset = makeReset();
|
|
86
96
|
let start;
|
|
87
97
|
let raf = requestAnimationFrame(wrapper);
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
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().
|
|
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().
|
|
113
|
+
reset().use(cb(1));
|
|
102
114
|
else {
|
|
103
|
-
reset().
|
|
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
|
|
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": "^
|
|
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
|
-
|
|
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
|
|
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 {
|
|
2
|
-
export default
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
stack.defer(reset);
|
|
6
6
|
let count = 0;
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
count
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
}
|
package/addEvtListener.d.ts
DELETED
|
@@ -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 {};
|