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 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
+ }