@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.
Files changed (2) hide show
  1. package/README.md +73 -187
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/poller
2
2
 
3
- A generic polling utility that periodically fetches data, detects changes via deep equality comparison, and supports debounced manual triggers with overlap handling.
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
- async () => {
18
- const res = await fetch("https://api.example.com/status");
19
- return res.json();
20
- },
24
+ fetchUser,
21
25
  (current, previous) => {
22
- console.log("Status changed:", current);
26
+ console.log("User updated:", current);
23
27
  },
24
- 5000 // poll every 5 seconds
28
+ 5000 // Poll every 5 seconds
25
29
  );
26
30
 
27
31
  await poller.start();
28
- // Polls start immediately
32
+ // Polls every 5s, fires onChange only when JSON data changes
29
33
 
30
- // Optionally, manually trigger a debounced poll
31
- poller.trigger(1000); // fires once after 1s of inactivity
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 done
37
+ // Stop polling when no longer needed
34
38
  poller.stop();
35
39
  ```
36
40
 
37
- ### Basic Usage
41
+ ## Polling Lifecycle
38
42
 
39
- ```typescript
40
- import { Poller } from "@hardlydifficult/poller";
43
+ ### Start and Stop
41
44
 
42
- const poller = new Poller(
43
- async () => await fetchCurrentState(),
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
- Changes are detected using deep equality via `JSON.stringify`. Structural equality means that objects with identical content—even different references—won’t trigger unnecessary callbacks.
58
+ ### Deep Equality via JSON
77
59
 
78
- ```typescript
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 poller = new Poller(
95
- async () => ({ count: 5, items: [1, 2, 3] }),
96
- (current, previous) => {
97
- // Only fires when the JSON representation changes
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
- await poller.start();
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
- ### `stop()`
125
-
126
- Stops polling and cleans up all timers.
72
+ ### On Change Callback
127
73
 
128
74
  ```typescript
129
- poller.stop();
130
- // No more polls will fire
75
+ onChange(current: T, previous: T | undefined): void
131
76
  ```
132
77
 
133
- ## Manual Triggers
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
- // If multiple triggers occur within debounceMs, only the last one fires
149
- poller.trigger();
150
- poller.trigger();
151
- poller.trigger(); // only this one will poll after 1s
152
- ```
81
+ ## Error Handling
153
82
 
154
- ### Behavior
83
+ ### Optional Error Callback
155
84
 
156
- - Accepts a `debounceMs` parameter (default: `1000`).
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
- async () => await fetchData(),
163
- (current, previous) => console.log("Updated:", current),
164
- 60000
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
- ## Error Handling
98
+ - Errors do not halt the polling interval.
99
+ - If `onError` is omitted, errors are silently suppressed.
182
100
 
183
- Fetch errors do not halt polling. An optional `onError` callback receives any errors, and polling continues at the next interval.
101
+ ## Manual Triggering
184
102
 
185
- ```typescript
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
- ### Error Handling Details
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
- const poller = new Poller(
207
- async () => await fetchData(),
208
- (current, previous) => console.log("Updated:", current),
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
- ## Core Features
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
- - `start()`: Begins polling immediately and then at the configured interval. Idempotent — calling it multiple times has no effect after the first call.
226
- - `stop()`: Clears the polling timer and any pending debounced triggers. No-op if already stopped.
116
+ ## Overlap Handling
227
117
 
228
- ### Concurrent Fetch Handling
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
- Overlapping fetches (e.g., due to slow network) are automatically skipped:
231
- - If `poll()` is already running when the interval fires, the next poll is skipped until the current one completes.
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>` Constructor
129
+ ### `Poller<T>`
236
130
 
237
- | Parameter | Type | Description |
238
- |-------------|---------------------------------------|------------------------------------------|
239
- | `fetchFn` | `() => Promise<T>` | Async function returning the current state |
240
- | `onChange` | `(current: T, previous: T | undefined) => void` | Callback invoked on state changes |
241
- | `intervalMs`| `number` | Polling interval in milliseconds |
242
- | `onError?` | `(error: unknown) => void` | Optional callback for fetch errors |
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 | Signature | Description |
247
- |--------------|----------------------------------------|----------------------------------------------|
248
- | `start()` | `(): Promise<void>` | Begins polling (immediate + interval-based) |
249
- | `stop()` | `(): void` | Stops polling and clears timers |
250
- | `trigger()` | `(debounceMs?: number) => void` | Triggers a debounced manual poll |
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/poller",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [