jrx 0.0.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.
- package/README.md +295 -0
- package/index.d.ts +13 -0
- package/index.js +96 -0
- package/package.json +43 -0
- package/retry.d.ts +12 -0
- package/retry.js +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# jrx
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install jrx
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Automatic cleanup for all effects and subscriptions
|
|
14
|
+
- Type-safe disposer pattern
|
|
15
|
+
- Retry logic with exponential backoff and cancellation
|
|
16
|
+
- Single dependency (jdisposer)
|
|
17
|
+
- Composable reactive utilities
|
|
18
|
+
- Browser and Node.js compatible
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
|
|
22
|
+
### `makeRenderLoop()`
|
|
23
|
+
|
|
24
|
+
Creates a render loop with automatic cleanup management.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { makeRenderLoop } from 'jrx'
|
|
28
|
+
|
|
29
|
+
const { loop, setLoop } = makeRenderLoop()
|
|
30
|
+
|
|
31
|
+
// Set the loop function
|
|
32
|
+
const dispose = setLoop((time) => {
|
|
33
|
+
console.log('Frame time:', time)
|
|
34
|
+
|
|
35
|
+
// Optional: return cleanup function
|
|
36
|
+
return () => {
|
|
37
|
+
console.log('Cleanup previous frame')
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Call loop on each animation frame
|
|
42
|
+
requestAnimationFrame(loop)
|
|
43
|
+
|
|
44
|
+
// Cleanup
|
|
45
|
+
dispose()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `addInterval(cb, ms)`
|
|
49
|
+
|
|
50
|
+
Creates a repeating interval with cleanup. The callback can optionally return a cleanup function that runs before the next invocation.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { addInterval } from 'jrx'
|
|
54
|
+
|
|
55
|
+
const dispose = addInterval(() => {
|
|
56
|
+
console.log('Tick')
|
|
57
|
+
|
|
58
|
+
// Optional: return cleanup function
|
|
59
|
+
return () => {
|
|
60
|
+
console.log('Cleanup')
|
|
61
|
+
}
|
|
62
|
+
}, 1000)
|
|
63
|
+
|
|
64
|
+
// Stop the interval
|
|
65
|
+
dispose()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `addIntervalAsync(cb, ms)`
|
|
69
|
+
|
|
70
|
+
Async version of `addInterval`. Waits for the callback to complete before scheduling the next invocation.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { addIntervalAsync } from 'jrx'
|
|
74
|
+
|
|
75
|
+
const dispose = addIntervalAsync(async (disposer) => {
|
|
76
|
+
await fetchData()
|
|
77
|
+
|
|
78
|
+
// Check if disposed during async operation
|
|
79
|
+
if (disposer.signal.aborted) return
|
|
80
|
+
|
|
81
|
+
processData()
|
|
82
|
+
}, 5000)
|
|
83
|
+
|
|
84
|
+
dispose()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `addRequestAnimationFrame(cb)`
|
|
88
|
+
|
|
89
|
+
Creates a `requestAnimationFrame` loop with cleanup.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { addRequestAnimationFrame } from 'jrx'
|
|
93
|
+
|
|
94
|
+
const dispose = addRequestAnimationFrame((now) => {
|
|
95
|
+
updateAnimation(now)
|
|
96
|
+
|
|
97
|
+
// Optional: return cleanup function
|
|
98
|
+
return () => {
|
|
99
|
+
cleanupAnimation()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
dispose()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `addSubs(subs, cb, options?)`
|
|
107
|
+
|
|
108
|
+
Manages multiple subscriptions with a single callback.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { addSubs } from 'jrx'
|
|
112
|
+
|
|
113
|
+
const sub1 = (listener) => {
|
|
114
|
+
eventEmitter.on('event1', listener)
|
|
115
|
+
return () => eventEmitter.off('event1', listener)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const sub2 = (listener) => {
|
|
119
|
+
eventEmitter.on('event2', listener)
|
|
120
|
+
return () => eventEmitter.off('event2', listener)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const dispose = addSubs([sub1, sub2], () => {
|
|
124
|
+
console.log('Any event fired')
|
|
125
|
+
|
|
126
|
+
// Optional: return cleanup function
|
|
127
|
+
return () => {
|
|
128
|
+
console.log('Cleanup')
|
|
129
|
+
}
|
|
130
|
+
}, { now: true }) // Call immediately with now: true
|
|
131
|
+
|
|
132
|
+
dispose()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `addTimeout(cb, ms)`
|
|
136
|
+
|
|
137
|
+
Creates a timeout with cleanup.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { addTimeout } from 'jrx'
|
|
141
|
+
|
|
142
|
+
const cancel = addTimeout(() => {
|
|
143
|
+
console.log('Timeout fired')
|
|
144
|
+
}, 1000)
|
|
145
|
+
|
|
146
|
+
// Cancel if needed
|
|
147
|
+
cancel()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `addTransition(cb, durationMs)`
|
|
151
|
+
|
|
152
|
+
Creates an animation transition with progress tracking (0 to 1).
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { addTransition } from 'jrx'
|
|
156
|
+
|
|
157
|
+
const dispose = addTransition((progress) => {
|
|
158
|
+
element.style.opacity = progress.toString()
|
|
159
|
+
|
|
160
|
+
// Optional: return cleanup function
|
|
161
|
+
return () => {
|
|
162
|
+
console.log('Frame cleanup')
|
|
163
|
+
}
|
|
164
|
+
}, 1000)
|
|
165
|
+
|
|
166
|
+
dispose()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `computed(fn, getDeps?)`
|
|
170
|
+
|
|
171
|
+
Creates a memoized computed value with optional dependency tracking.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { computed } from 'jrx'
|
|
175
|
+
|
|
176
|
+
// Without dependencies - always recomputes
|
|
177
|
+
const value1 = computed(() => expensiveCalculation())
|
|
178
|
+
console.log(value1.value) // Computed
|
|
179
|
+
console.log(value1.value) // Computed again
|
|
180
|
+
|
|
181
|
+
// With dependencies - memoizes when deps unchanged
|
|
182
|
+
let a = 1, b = 2
|
|
183
|
+
const value2 = computed(
|
|
184
|
+
() => a + b,
|
|
185
|
+
() => [a, b] // Dependencies
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
console.log(value2.value) // Computed: 3
|
|
189
|
+
console.log(value2.value) // Cached: 3
|
|
190
|
+
|
|
191
|
+
a = 10
|
|
192
|
+
console.log(value2.value) // Recomputed: 12
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `retry(cb, options?)`
|
|
196
|
+
|
|
197
|
+
Retries an async operation with exponential backoff on failure.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import retry from 'jrx/retry'
|
|
201
|
+
|
|
202
|
+
// Basic usage - retries with default backoff
|
|
203
|
+
const result = await retry(async (disposer, { resetBackoff }) => {
|
|
204
|
+
const response = await fetch('/api/data')
|
|
205
|
+
if (!response.ok) throw new Error('Failed to fetch')
|
|
206
|
+
return response.json()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Custom backoff schedule (in seconds)
|
|
210
|
+
await retry(
|
|
211
|
+
async (disposer, { resetBackoff }) => {
|
|
212
|
+
return await fetchData()
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
backoffSec: [1, 2, 5, 10, -1] // -1 means retry forever with last delay
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
// With disposer for cancellation
|
|
220
|
+
import { makeDisposer } from 'jdisposer'
|
|
221
|
+
|
|
222
|
+
const disposer = makeDisposer()
|
|
223
|
+
|
|
224
|
+
const data = await retry(
|
|
225
|
+
async (loopDisposer, { resetBackoff }) => {
|
|
226
|
+
// Check if aborted
|
|
227
|
+
if (loopDisposer.signal.aborted) return
|
|
228
|
+
|
|
229
|
+
const result = await fetchData()
|
|
230
|
+
|
|
231
|
+
// Reset backoff on successful partial progress
|
|
232
|
+
if (result.isPartialSuccess) {
|
|
233
|
+
resetBackoff()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
disposer,
|
|
240
|
+
backoffSec: [5, 10, 20, 40, -1]
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// Cancel the retry loop
|
|
245
|
+
disposer.dispose()
|
|
246
|
+
|
|
247
|
+
// Returns undefined when disposed
|
|
248
|
+
console.log(data) // T | undefined
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Options:**
|
|
252
|
+
- `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]`
|
|
253
|
+
- `disposer`: Optional disposer for cancellation. When provided, the return type is `T | undefined`. Otherwise, the return type is `T`.
|
|
254
|
+
|
|
255
|
+
**Callback parameters:**
|
|
256
|
+
- `disposer`: A disposer for the current retry attempt. Check `disposer.signal.aborted` to handle cancellation
|
|
257
|
+
- `info.resetBackoff()`: Call this to reset the backoff counter to the beginning (useful when making partial progress)
|
|
258
|
+
|
|
259
|
+
## Cleanup Pattern
|
|
260
|
+
|
|
261
|
+
All functions return disposer functions that clean up resources:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import {addInterval, addTimeout, addRequestAnimationFrame} from 'jrx'
|
|
265
|
+
import {makeDisposer} from 'jdisposer'
|
|
266
|
+
|
|
267
|
+
const disposer = makeDisposer()
|
|
268
|
+
|
|
269
|
+
// Collect disposers
|
|
270
|
+
disposer.add(addInterval(() => console.log('tick'), 1000))
|
|
271
|
+
disposer.add(addTimeout(() => console.log('timeout'), 5000))
|
|
272
|
+
disposer.add(addRequestAnimationFrame((now) => render(now)))
|
|
273
|
+
|
|
274
|
+
// Cleanup all at once
|
|
275
|
+
disposer.dispose()
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## TypeScript
|
|
279
|
+
|
|
280
|
+
This library is written in TypeScript and includes type definitions.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import type { Disposer } from 'jdisposer'
|
|
284
|
+
|
|
285
|
+
// All disposer functions follow this pattern
|
|
286
|
+
type DisposerFunction = () => void
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
MIT
|
|
292
|
+
|
|
293
|
+
## Repository
|
|
294
|
+
|
|
295
|
+
https://github.com/tranvansang/jrx
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Disposer } from 'jdisposer';
|
|
2
|
+
export declare function makeRenderLoop(): {
|
|
3
|
+
loop(this: void, time: DOMHighResTimeStamp): void;
|
|
4
|
+
setLoop(this: void, loop: (time: DOMHighResTimeStamp) => void | (() => void)): () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function addInterval(cb: () => void | (() => any), ms: number): () => void;
|
|
7
|
+
export declare function addIntervalAsync(cb: (disposer: Disposer) => void | (() => any) | Promise<void> | Promise<() => any>, ms: number): () => void;
|
|
8
|
+
export declare function addRequestAnimationFrame(cb: (now: DOMHighResTimeStamp) => void | (() => any)): () => void;
|
|
9
|
+
export declare function addSubs<Subs extends any[]>(subs: Subs, cb: () => void | (() => void), { now }?: {
|
|
10
|
+
now?: boolean;
|
|
11
|
+
}): (this: void) => void;
|
|
12
|
+
export declare function addTimeout(cb: () => void, ms: number): () => void;
|
|
13
|
+
export declare function addTransition(cb: (progress: number) => void | (() => void), durationMs: number): () => void;
|
package/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { makeDisposer, makeReset } from 'jdisposer';
|
|
2
|
+
export function makeRenderLoop() {
|
|
3
|
+
let loop_;
|
|
4
|
+
const reset = makeReset();
|
|
5
|
+
return {
|
|
6
|
+
loop(time) {
|
|
7
|
+
reset().add(loop_?.(time));
|
|
8
|
+
},
|
|
9
|
+
setLoop(loop) {
|
|
10
|
+
loop_ = loop;
|
|
11
|
+
return () => {
|
|
12
|
+
reset();
|
|
13
|
+
loop_ = undefined;
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function addInterval(cb, ms) {
|
|
19
|
+
const reset = makeReset();
|
|
20
|
+
let timeout;
|
|
21
|
+
wrapper();
|
|
22
|
+
return () => {
|
|
23
|
+
reset();
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
};
|
|
26
|
+
function wrapper() {
|
|
27
|
+
reset().add(cb());
|
|
28
|
+
timeout = setTimeout(wrapper, ms);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function addIntervalAsync(cb, ms) {
|
|
32
|
+
const reset = makeReset();
|
|
33
|
+
let timeout;
|
|
34
|
+
void wrapper();
|
|
35
|
+
return () => {
|
|
36
|
+
reset();
|
|
37
|
+
clearTimeout(timeout);
|
|
38
|
+
};
|
|
39
|
+
async function wrapper() {
|
|
40
|
+
const disposer = reset();
|
|
41
|
+
await cb(disposer);
|
|
42
|
+
if (!disposer.signal.aborted)
|
|
43
|
+
timeout = setTimeout(wrapper, ms);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function addRequestAnimationFrame(cb) {
|
|
47
|
+
const reset = makeReset();
|
|
48
|
+
let raf = requestAnimationFrame(wrapper);
|
|
49
|
+
return () => {
|
|
50
|
+
reset();
|
|
51
|
+
cancelAnimationFrame(raf);
|
|
52
|
+
};
|
|
53
|
+
function wrapper(now) {
|
|
54
|
+
reset().add(cb(now));
|
|
55
|
+
raf = requestAnimationFrame(wrapper);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function addSubs(subs, cb, { now } = {}) {
|
|
59
|
+
const disposer = makeDisposer();
|
|
60
|
+
const reset = makeReset();
|
|
61
|
+
disposer.add(reset);
|
|
62
|
+
for (const sub of subs)
|
|
63
|
+
disposer.add(sub(() => reset().add(cb())));
|
|
64
|
+
if (now)
|
|
65
|
+
reset().add(cb());
|
|
66
|
+
return disposer.dispose;
|
|
67
|
+
}
|
|
68
|
+
export function addTimeout(cb, ms) {
|
|
69
|
+
const timeout = setTimeout(cb, ms);
|
|
70
|
+
return () => clearTimeout(timeout);
|
|
71
|
+
}
|
|
72
|
+
export function addTransition(cb, durationMs) {
|
|
73
|
+
const reset = makeReset();
|
|
74
|
+
let start;
|
|
75
|
+
let raf = requestAnimationFrame(wrapper);
|
|
76
|
+
return () => {
|
|
77
|
+
reset();
|
|
78
|
+
cancelAnimationFrame(raf);
|
|
79
|
+
};
|
|
80
|
+
function wrapper(now) {
|
|
81
|
+
if (start === undefined) {
|
|
82
|
+
start = now;
|
|
83
|
+
reset().add(cb(0));
|
|
84
|
+
raf = requestAnimationFrame(wrapper);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const progress = (now - start) / durationMs;
|
|
88
|
+
if (progress >= 1)
|
|
89
|
+
reset().add(cb(1));
|
|
90
|
+
else {
|
|
91
|
+
reset().add(cb(progress));
|
|
92
|
+
raf = requestAnimationFrame(wrapper);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jrx",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"prepublishOnly": "npx tsc index.ts retry.ts --target esnext --declaration --moduleResolution node",
|
|
7
|
+
"test": "node --test tests/*.test.ts",
|
|
8
|
+
"test:coverage": "node --test --experimental-test-coverage tests/*.test.ts"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"import": "./index.js"
|
|
14
|
+
},
|
|
15
|
+
"./retry": {
|
|
16
|
+
"types": "./retry.d.ts",
|
|
17
|
+
"import": "./retry.js"
|
|
18
|
+
},
|
|
19
|
+
"./computed": {
|
|
20
|
+
"types": "./computed.d.ts",
|
|
21
|
+
"import": "./computed.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/tranvansang/jrx.git"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"index.d.ts",
|
|
31
|
+
"retry.js",
|
|
32
|
+
"retry.d.ts",
|
|
33
|
+
"computed.js",
|
|
34
|
+
"computed.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^25.2.1",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"jdisposer": "^0.0.1"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/retry.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Disposer } from 'jdisposer';
|
|
2
|
+
export default function retry<T>(cb: (disposer: Disposer, info: {
|
|
3
|
+
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>;
|
package/retry.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { 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, } = {}) {
|
|
4
|
+
const reset = makeReset();
|
|
5
|
+
disposer?.add(reset);
|
|
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;
|
|
26
|
+
}
|
|
27
|
+
console.warn('Retrying due to error:', e);
|
|
28
|
+
loopDisposer = reset();
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, backoffSec[count - 1] * 1000));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|