@hardlydifficult/poller 1.0.7 → 1.0.8
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 +73 -187
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/poller
|
|
2
2
|
|
|
3
|
-
A generic polling utility
|
|
3
|
+
A lightweight, generic polling utility with debounced triggers, overlapping request handling, and deep equality change detection.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,246 +13,132 @@ npm install @hardlydifficult/poller
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { Poller } from "@hardlydifficult/poller";
|
|
15
15
|
|
|
16
|
+
// Define a fetch function (e.g., API call)
|
|
17
|
+
const fetchUser = async () => {
|
|
18
|
+
const res = await fetch("https://api.example.com/user");
|
|
19
|
+
return res.json();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Create and start the poller
|
|
16
23
|
const poller = new Poller(
|
|
17
|
-
|
|
18
|
-
const res = await fetch("https://api.example.com/status");
|
|
19
|
-
return res.json();
|
|
20
|
-
},
|
|
24
|
+
fetchUser,
|
|
21
25
|
(current, previous) => {
|
|
22
|
-
console.log("
|
|
26
|
+
console.log("User updated:", current);
|
|
23
27
|
},
|
|
24
|
-
5000 //
|
|
28
|
+
5000 // Poll every 5 seconds
|
|
25
29
|
);
|
|
26
30
|
|
|
27
31
|
await poller.start();
|
|
28
|
-
// Polls
|
|
32
|
+
// Polls every 5s, fires onChange only when JSON data changes
|
|
29
33
|
|
|
30
|
-
//
|
|
31
|
-
poller.trigger(1000); //
|
|
34
|
+
// Manually trigger a debounced poll (e.g., after user action)
|
|
35
|
+
poller.trigger(1000); // Waits 1s, then polls once
|
|
32
36
|
|
|
33
|
-
// Stop polling when
|
|
37
|
+
// Stop polling when no longer needed
|
|
34
38
|
poller.stop();
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
## Polling Lifecycle
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
import { Poller } from "@hardlydifficult/poller";
|
|
43
|
+
### Start and Stop
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
(current, previous) => console.log("State changed!", current),
|
|
45
|
-
5 * 60 * 1000 // poll every 5 minutes
|
|
46
|
-
);
|
|
45
|
+
- `start()`: Begins polling at the configured interval. Idempotent—safe to call multiple times.
|
|
46
|
+
- `stop()`: Cancels timers and clears any pending debounced trigger. Safe to call multiple times.
|
|
47
47
|
|
|
48
|
+
```typescript
|
|
48
49
|
await poller.start();
|
|
50
|
+
await poller.start(); // No-op
|
|
49
51
|
|
|
50
|
-
// Manual trigger with debounce (e.g. from a webhook)
|
|
51
|
-
poller.trigger(1000);
|
|
52
|
-
|
|
53
|
-
// Stop polling
|
|
54
52
|
poller.stop();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
## Polling Behavior
|
|
58
|
-
|
|
59
|
-
The `Poller` executes a fetch function at a fixed interval and invokes a callback only when the returned value changes.
|
|
60
|
-
|
|
61
|
-
- Starts polling immediately on `start()`
|
|
62
|
-
- Fetches at the configured `intervalMs`
|
|
63
|
-
- Skips overlapping fetches (if a previous fetch is still in progress)
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
const poller = new Poller(
|
|
67
|
-
async () => fetchJson("/api/data"),
|
|
68
|
-
(current, previous) => console.log("Changed:", current),
|
|
69
|
-
10_000
|
|
70
|
-
);
|
|
71
|
-
await poller.start();
|
|
53
|
+
poller.stop(); // No-op
|
|
72
54
|
```
|
|
73
55
|
|
|
74
56
|
## Change Detection
|
|
75
57
|
|
|
76
|
-
|
|
58
|
+
### Deep Equality via JSON
|
|
77
59
|
|
|
78
|
-
|
|
79
|
-
const poller = new Poller(
|
|
80
|
-
async () => ({ items: [1, 2, 3] }),
|
|
81
|
-
(current, previous) => console.log("Changed:", current),
|
|
82
|
-
1000
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// Even if a new object instance is returned, same structure = no change
|
|
86
|
-
await poller.start();
|
|
87
|
-
// onChange fires only when structure changes
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
- On first poll, `onChange(current, undefined)` is invoked.
|
|
91
|
-
- On subsequent polls, `onChange(current, previous)` is invoked only if the new value differs structurally.
|
|
60
|
+
Change detection uses `JSON.stringify()` comparison, enabling structural equality checks for objects and arrays—even when references differ.
|
|
92
61
|
|
|
93
62
|
```typescript
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log("Changed from", previous, "to", current);
|
|
99
|
-
},
|
|
100
|
-
5000
|
|
101
|
-
);
|
|
63
|
+
const fetchFn = async () => ({ items: [1, 2, 3] });
|
|
64
|
+
const onChange = (current, previous) => {
|
|
65
|
+
// Fires only when structure changes
|
|
66
|
+
};
|
|
102
67
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
## Lifecycle Management
|
|
107
|
-
|
|
108
|
-
Polling can be started and stopped at any time. The `stop()` method cancels both the periodic timer and any pending debounced triggers.
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
await poller.start(); // begins polling
|
|
112
|
-
poller.stop(); // cancels all timers, stops future polls
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### `start()`
|
|
116
|
-
|
|
117
|
-
Starts polling. Fetches immediately, then at the configured interval. Calling `start()` multiple times is safe (idempotent).
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
await poller.start(); // Fetches immediately
|
|
121
|
-
await poller.start(); // No-op, already running
|
|
68
|
+
const poller = new Poller(fetchFn, onChange, 1000);
|
|
69
|
+
// Even if new object reference returned, onChange won’t fire unless JSON differs
|
|
122
70
|
```
|
|
123
71
|
|
|
124
|
-
###
|
|
125
|
-
|
|
126
|
-
Stops polling and cleans up all timers.
|
|
72
|
+
### On Change Callback
|
|
127
73
|
|
|
128
74
|
```typescript
|
|
129
|
-
|
|
130
|
-
// No more polls will fire
|
|
75
|
+
onChange(current: T, previous: T | undefined): void
|
|
131
76
|
```
|
|
132
77
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
You can manually trigger a debounced poll to force an immediate check after a period of inactivity.
|
|
136
|
-
|
|
137
|
-
| Parameter | Type | Default | Description |
|
|
138
|
-
|-------------|----------|---------|----------------------------------------|
|
|
139
|
-
| `debounceMs`| `number` | `1000` | Milliseconds to wait before polling |
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
// Immediate trigger with default debounce
|
|
143
|
-
poller.trigger();
|
|
144
|
-
|
|
145
|
-
// Custom debounce
|
|
146
|
-
poller.trigger(500);
|
|
78
|
+
- `current`: Most recently fetched value
|
|
79
|
+
- `previous`: Prior value, or `undefined` on first poll
|
|
147
80
|
|
|
148
|
-
|
|
149
|
-
poller.trigger();
|
|
150
|
-
poller.trigger();
|
|
151
|
-
poller.trigger(); // only this one will poll after 1s
|
|
152
|
-
```
|
|
81
|
+
## Error Handling
|
|
153
82
|
|
|
154
|
-
###
|
|
83
|
+
### Optional Error Callback
|
|
155
84
|
|
|
156
|
-
|
|
157
|
-
- Multiple rapid calls reset the debounce timer — only the last trigger fires.
|
|
158
|
-
- No-op if the poller is not running.
|
|
85
|
+
Provide an `onError` handler to manage fetch failures; polling continues regardless.
|
|
159
86
|
|
|
160
87
|
```typescript
|
|
161
88
|
const poller = new Poller(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
89
|
+
fetchFn,
|
|
90
|
+
onChange,
|
|
91
|
+
5000,
|
|
92
|
+
(error) => {
|
|
93
|
+
console.warn("Polling error:", error);
|
|
94
|
+
}
|
|
165
95
|
);
|
|
166
|
-
|
|
167
|
-
await poller.start();
|
|
168
|
-
|
|
169
|
-
// Trigger a poll with 1000ms debounce (default)
|
|
170
|
-
poller.trigger();
|
|
171
|
-
|
|
172
|
-
// Trigger with custom debounce
|
|
173
|
-
poller.trigger(500);
|
|
174
|
-
|
|
175
|
-
// Multiple rapid triggers coalesce into one poll
|
|
176
|
-
poller.trigger(500);
|
|
177
|
-
poller.trigger(500);
|
|
178
|
-
poller.trigger(500); // Only one poll fires after 500ms
|
|
179
96
|
```
|
|
180
97
|
|
|
181
|
-
|
|
98
|
+
- Errors do not halt the polling interval.
|
|
99
|
+
- If `onError` is omitted, errors are silently suppressed.
|
|
182
100
|
|
|
183
|
-
|
|
101
|
+
## Manual Triggering
|
|
184
102
|
|
|
185
|
-
|
|
186
|
-
const poller = new Poller(
|
|
187
|
-
async () => {
|
|
188
|
-
if (Math.random() < 0.5) throw new Error("Network fail");
|
|
189
|
-
return "ok";
|
|
190
|
-
},
|
|
191
|
-
(current) => console.log(current),
|
|
192
|
-
1000,
|
|
193
|
-
(error) => console.error("Fetch failed:", error)
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
await poller.start();
|
|
197
|
-
// Continues polling even after errors
|
|
198
|
-
```
|
|
103
|
+
### Debounced Triggers
|
|
199
104
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
- Errors during fetch are caught and passed to the optional `onError` callback.
|
|
203
|
-
- Polling continues after errors — no automatic retry logic is applied.
|
|
105
|
+
- `trigger(debounceMs?: number)`: Schedules a one-time poll after a debounce delay (default: 1000ms).
|
|
106
|
+
- Multiple rapid calls cancel previous timeouts—only the last one fires.
|
|
204
107
|
|
|
205
108
|
```typescript
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
5000,
|
|
210
|
-
(error) => {
|
|
211
|
-
console.error("Fetch failed:", error);
|
|
212
|
-
// Polling continues automatically
|
|
213
|
-
}
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
await poller.start();
|
|
109
|
+
poller.trigger(2000); // Polls in 2s
|
|
110
|
+
poller.trigger(2000); // Cancelled, re-schedule to 2s from now
|
|
111
|
+
poller.trigger(2000); // Cancelled again, final delay applies
|
|
217
112
|
```
|
|
218
113
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
### Polling Lifecycle
|
|
222
|
-
|
|
223
|
-
The `Poller` manages its lifecycle with `start()` and `stop()` methods.
|
|
114
|
+
- No-op if `poller` is not running.
|
|
224
115
|
|
|
225
|
-
|
|
226
|
-
- `stop()`: Clears the polling timer and any pending debounced triggers. No-op if already stopped.
|
|
116
|
+
## Overlap Handling
|
|
227
117
|
|
|
228
|
-
|
|
118
|
+
- Concurrent fetches are skipped—only one in-flight request is allowed at a time.
|
|
119
|
+
- Prevents resource waste and race conditions during slow network calls.
|
|
229
120
|
|
|
230
|
-
|
|
231
|
-
|
|
121
|
+
```typescript
|
|
122
|
+
// Interval fires every 5s; if fetch takes 6s:
|
|
123
|
+
// - Second interval fire is skipped
|
|
124
|
+
// - Third interval fire executes after first completes
|
|
125
|
+
```
|
|
232
126
|
|
|
233
127
|
## API Reference
|
|
234
128
|
|
|
235
|
-
### `Poller<T>`
|
|
129
|
+
### `Poller<T>`
|
|
236
130
|
|
|
237
|
-
| Parameter
|
|
238
|
-
|
|
239
|
-
| `fetchFn`
|
|
240
|
-
| `onChange`
|
|
241
|
-
| `intervalMs
|
|
242
|
-
| `onError?`
|
|
131
|
+
| Parameter | Type | Description |
|
|
132
|
+
|-----------|------|-------------|
|
|
133
|
+
| `fetchFn` | `() => Promise<T>` | Async function to fetch data |
|
|
134
|
+
| `onChange` | `(current: T, previous: T \| undefined) => void` | Callback on value change |
|
|
135
|
+
| `intervalMs` | `number` | Polling interval in milliseconds |
|
|
136
|
+
| `onError?` | `(error: unknown) => void` | Optional error handler |
|
|
243
137
|
|
|
244
138
|
### Methods
|
|
245
139
|
|
|
246
|
-
| Method
|
|
247
|
-
|
|
248
|
-
| `start
|
|
249
|
-
| `stop
|
|
250
|
-
| `trigger
|
|
251
|
-
|
|
252
|
-
### Behavior
|
|
253
|
-
|
|
254
|
-
- **Deep equality** — uses JSON serialization to detect structural changes
|
|
255
|
-
- **Overlap prevention** — skips a poll if the previous fetch is still running
|
|
256
|
-
- **Error resilience** — continues polling after fetch errors
|
|
257
|
-
- **Idempotent start** — calling `start()` multiple times is safe
|
|
258
|
-
- **Debounced triggers** — multiple `trigger()` calls coalesce into one poll
|
|
140
|
+
| Method | Signature | Description |
|
|
141
|
+
|--------|-----------|-------------|
|
|
142
|
+
| `start` | `(): Promise<void>` | Begin polling immediately and then at `intervalMs` |
|
|
143
|
+
| `stop` | `(): void` | Cancel all timers and pending triggers |
|
|
144
|
+
| `trigger` | `(debounceMs?: number) => void` | Trigger a one-time poll after debounce delay |
|