@okikio/observables 1.0.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/LICENSE +21 -0
- package/README.md +578 -0
- package/esm/_dnt.polyfills.d.ts +20 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +12 -0
- package/esm/_spec.d.ts +260 -0
- package/esm/_spec.d.ts.map +1 -0
- package/esm/_spec.js +1 -0
- package/esm/_types.d.ts +141 -0
- package/esm/_types.d.ts.map +1 -0
- package/esm/_types.js +20 -0
- package/esm/error.d.ts +331 -0
- package/esm/error.d.ts.map +1 -0
- package/esm/error.js +408 -0
- package/esm/events.d.ts +320 -0
- package/esm/events.d.ts.map +1 -0
- package/esm/events.js +451 -0
- package/esm/helpers/_types.d.ts +188 -0
- package/esm/helpers/_types.d.ts.map +1 -0
- package/esm/helpers/_types.js +1 -0
- package/esm/helpers/mod.d.ts +90 -0
- package/esm/helpers/mod.d.ts.map +1 -0
- package/esm/helpers/mod.js +90 -0
- package/esm/helpers/operations/batch.d.ts +109 -0
- package/esm/helpers/operations/batch.d.ts.map +1 -0
- package/esm/helpers/operations/batch.js +140 -0
- package/esm/helpers/operations/combination.d.ts +162 -0
- package/esm/helpers/operations/combination.d.ts.map +1 -0
- package/esm/helpers/operations/combination.js +350 -0
- package/esm/helpers/operations/conditional.d.ts +211 -0
- package/esm/helpers/operations/conditional.d.ts.map +1 -0
- package/esm/helpers/operations/conditional.js +280 -0
- package/esm/helpers/operations/core.d.ts +198 -0
- package/esm/helpers/operations/core.d.ts.map +1 -0
- package/esm/helpers/operations/core.js +264 -0
- package/esm/helpers/operations/errors.d.ts +277 -0
- package/esm/helpers/operations/errors.d.ts.map +1 -0
- package/esm/helpers/operations/errors.js +378 -0
- package/esm/helpers/operations/mod.d.ts +26 -0
- package/esm/helpers/operations/mod.d.ts.map +1 -0
- package/esm/helpers/operations/mod.js +25 -0
- package/esm/helpers/operations/timing.d.ts +206 -0
- package/esm/helpers/operations/timing.d.ts.map +1 -0
- package/esm/helpers/operations/timing.js +457 -0
- package/esm/helpers/operators.d.ts +520 -0
- package/esm/helpers/operators.d.ts.map +1 -0
- package/esm/helpers/operators.js +563 -0
- package/esm/helpers/pipe.d.ts +118 -0
- package/esm/helpers/pipe.d.ts.map +1 -0
- package/esm/helpers/pipe.js +129 -0
- package/esm/helpers/utils.d.ts +142 -0
- package/esm/helpers/utils.d.ts.map +1 -0
- package/esm/helpers/utils.js +193 -0
- package/esm/mod.d.ts +863 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +861 -0
- package/esm/observable.d.ts +1610 -0
- package/esm/observable.d.ts.map +1 -0
- package/esm/observable.js +1970 -0
- package/esm/package.json +3 -0
- package/esm/queue.d.ts +201 -0
- package/esm/queue.d.ts.map +1 -0
- package/esm/queue.js +273 -0
- package/esm/symbol.d.ts +60 -0
- package/esm/symbol.d.ts.map +1 -0
- package/esm/symbol.js +132 -0
- package/package.json +96 -0
- package/script/_dnt.polyfills.d.ts +20 -0
- package/script/_dnt.polyfills.d.ts.map +1 -0
- package/script/_dnt.polyfills.js +13 -0
- package/script/_spec.d.ts +260 -0
- package/script/_spec.d.ts.map +1 -0
- package/script/_spec.js +2 -0
- package/script/_types.d.ts +141 -0
- package/script/_types.d.ts.map +1 -0
- package/script/_types.js +22 -0
- package/script/error.d.ts +331 -0
- package/script/error.d.ts.map +1 -0
- package/script/error.js +414 -0
- package/script/events.d.ts +320 -0
- package/script/events.d.ts.map +1 -0
- package/script/events.js +458 -0
- package/script/helpers/_types.d.ts +188 -0
- package/script/helpers/_types.d.ts.map +1 -0
- package/script/helpers/_types.js +2 -0
- package/script/helpers/mod.d.ts +90 -0
- package/script/helpers/mod.d.ts.map +1 -0
- package/script/helpers/mod.js +106 -0
- package/script/helpers/operations/batch.d.ts +109 -0
- package/script/helpers/operations/batch.d.ts.map +1 -0
- package/script/helpers/operations/batch.js +144 -0
- package/script/helpers/operations/combination.d.ts +162 -0
- package/script/helpers/operations/combination.d.ts.map +1 -0
- package/script/helpers/operations/combination.js +355 -0
- package/script/helpers/operations/conditional.d.ts +211 -0
- package/script/helpers/operations/conditional.d.ts.map +1 -0
- package/script/helpers/operations/conditional.js +286 -0
- package/script/helpers/operations/core.d.ts +198 -0
- package/script/helpers/operations/core.d.ts.map +1 -0
- package/script/helpers/operations/core.js +272 -0
- package/script/helpers/operations/errors.d.ts +277 -0
- package/script/helpers/operations/errors.d.ts.map +1 -0
- package/script/helpers/operations/errors.js +387 -0
- package/script/helpers/operations/mod.d.ts +26 -0
- package/script/helpers/operations/mod.d.ts.map +1 -0
- package/script/helpers/operations/mod.js +41 -0
- package/script/helpers/operations/timing.d.ts +206 -0
- package/script/helpers/operations/timing.d.ts.map +1 -0
- package/script/helpers/operations/timing.js +464 -0
- package/script/helpers/operators.d.ts +520 -0
- package/script/helpers/operators.d.ts.map +1 -0
- package/script/helpers/operators.js +570 -0
- package/script/helpers/pipe.d.ts +118 -0
- package/script/helpers/pipe.d.ts.map +1 -0
- package/script/helpers/pipe.js +132 -0
- package/script/helpers/utils.d.ts +142 -0
- package/script/helpers/utils.d.ts.map +1 -0
- package/script/helpers/utils.js +200 -0
- package/script/mod.d.ts +863 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +877 -0
- package/script/observable.d.ts +1610 -0
- package/script/observable.d.ts.map +1 -0
- package/script/observable.js +1984 -0
- package/script/package.json +3 -0
- package/script/queue.d.ts +201 -0
- package/script/queue.d.ts.map +1 -0
- package/script/queue.js +286 -0
- package/script/symbol.d.ts +60 -0
- package/script/symbol.d.ts.map +1 -0
- package/script/symbol.js +135 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.delay = delay;
|
|
4
|
+
exports.delayEach = delayEach;
|
|
5
|
+
exports.debounce = debounce;
|
|
6
|
+
exports.throttle = throttle;
|
|
7
|
+
exports.timeout = timeout;
|
|
8
|
+
/**
|
|
9
|
+
* Time-based operators for spacing, delaying, and expiring stream values.
|
|
10
|
+
*
|
|
11
|
+
* This entrypoint groups the operators that make time part of your pipeline's
|
|
12
|
+
* behavior. Use it for UI patterns such as debouncing search input, throttling
|
|
13
|
+
* bursty events, delaying retries, or timing out work that takes too long.
|
|
14
|
+
*
|
|
15
|
+
* Timing operators do not just change values; they change when work is allowed
|
|
16
|
+
* to move downstream. That makes them especially important for coordinating
|
|
17
|
+
* async side effects without piling up stale requests or overwhelming slower
|
|
18
|
+
* consumers.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
require("../../_dnt.polyfills.js");
|
|
23
|
+
const operators_js_1 = require("../operators.js");
|
|
24
|
+
const error_js_1 = require("../../error.js");
|
|
25
|
+
/**
|
|
26
|
+
* Delays each item in the stream by a specified number of milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* This operator shifts the entire stream of events forward in time, preserving
|
|
29
|
+
* the relative time between them.
|
|
30
|
+
*
|
|
31
|
+
* > Note: This does not delay error emissions. If an error occurs, it will
|
|
32
|
+
* > be emitted immediately, regardless of the delay.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { pipe, delay, from } from "./helpers/mod.ts";
|
|
37
|
+
*
|
|
38
|
+
* // No direct Array equivalent, as it's about timing.
|
|
39
|
+
*
|
|
40
|
+
* // Stream behavior
|
|
41
|
+
* const sourceStream = from([1, 2, 3]);
|
|
42
|
+
* const delayedStream = pipe(sourceStream, delay(1000));
|
|
43
|
+
* // Emits 1 (after 1s), then 2 (immediately after), then 3 (immediately after).
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ## Practical Use Case
|
|
47
|
+
*
|
|
48
|
+
* Use `delay` to simulate network latency in tests, or to introduce a small
|
|
49
|
+
* pause in a UI animation sequence to make it feel more natural.
|
|
50
|
+
*
|
|
51
|
+
* ## Key Insight
|
|
52
|
+
*
|
|
53
|
+
* `delay` is about shifting the timeline of events, not about pausing between
|
|
54
|
+
* them. It's a simple way to control when a stream begins to emit its values.
|
|
55
|
+
*
|
|
56
|
+
* @typeParam T - Type of values from the source stream
|
|
57
|
+
* @param ms - The delay duration in milliseconds
|
|
58
|
+
* @returns A stream operator that delays each value
|
|
59
|
+
*/
|
|
60
|
+
function delay(ms) {
|
|
61
|
+
return (0, operators_js_1.createStatefulOperator)({
|
|
62
|
+
name: "delay",
|
|
63
|
+
errorMode: "pass-through", // Should be explicit
|
|
64
|
+
createState: () => ({
|
|
65
|
+
timeoutId: null,
|
|
66
|
+
buffer: [],
|
|
67
|
+
hasStarted: false,
|
|
68
|
+
delayOver: false,
|
|
69
|
+
pendingFlush: null,
|
|
70
|
+
}),
|
|
71
|
+
transform(chunk, state, controller) {
|
|
72
|
+
// If delay period is over, emit immediately
|
|
73
|
+
if (state.delayOver)
|
|
74
|
+
controller.enqueue(chunk);
|
|
75
|
+
// Buffer the item and start delay timer if not already started
|
|
76
|
+
else
|
|
77
|
+
state.buffer.push(chunk);
|
|
78
|
+
// Start the delay timer on the first item
|
|
79
|
+
if (!state.hasStarted) {
|
|
80
|
+
state.pendingFlush = Promise.withResolvers();
|
|
81
|
+
state.timeoutId = setTimeout(() => {
|
|
82
|
+
// Mark delay as complete
|
|
83
|
+
state.delayOver = true;
|
|
84
|
+
// Release all buffered items
|
|
85
|
+
for (const value of state.buffer) {
|
|
86
|
+
controller.enqueue(value);
|
|
87
|
+
}
|
|
88
|
+
state.buffer.length = 0; // Fast array clear
|
|
89
|
+
// Clean up timeout reference
|
|
90
|
+
clearTimeout(state.timeoutId);
|
|
91
|
+
state.timeoutId = null;
|
|
92
|
+
// If flush was waiting, complete it now
|
|
93
|
+
if (state.pendingFlush) {
|
|
94
|
+
state.pendingFlush.resolve();
|
|
95
|
+
state.pendingFlush = null;
|
|
96
|
+
}
|
|
97
|
+
}, ms);
|
|
98
|
+
state.hasStarted = true;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async flush(state, controller) {
|
|
102
|
+
// If delay hasn't completed yet, we need to wait
|
|
103
|
+
if (state.pendingFlush)
|
|
104
|
+
await state.pendingFlush.promise;
|
|
105
|
+
// Cancel timeout if stream ends during delay
|
|
106
|
+
if (state.timeoutId !== null) {
|
|
107
|
+
clearTimeout(state.timeoutId);
|
|
108
|
+
state.timeoutId = null;
|
|
109
|
+
}
|
|
110
|
+
// Emit any remaining buffered items
|
|
111
|
+
for (const item of state.buffer) {
|
|
112
|
+
controller.enqueue(item);
|
|
113
|
+
}
|
|
114
|
+
state.buffer.length = 0;
|
|
115
|
+
},
|
|
116
|
+
cancel(state) {
|
|
117
|
+
// Critical: clean up timeout to prevent memory leaks
|
|
118
|
+
if (state.timeoutId !== null) {
|
|
119
|
+
clearTimeout(state.timeoutId);
|
|
120
|
+
state.timeoutId = null;
|
|
121
|
+
}
|
|
122
|
+
state.buffer.length = 0;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Delays each individual item by a specified number of milliseconds.
|
|
128
|
+
*
|
|
129
|
+
* This operator delays every item independently, preserving the relative
|
|
130
|
+
* spacing between them while shifting each one forward by the same amount.
|
|
131
|
+
* Think of it as "adding X milliseconds to each item's timestamp."
|
|
132
|
+
*
|
|
133
|
+
* > Note: This does not delay error emissions. If an error occurs, it will
|
|
134
|
+
* > be emitted immediately, regardless of the delay.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* import { pipe, delayEach, from } from "./helpers/mod.ts";
|
|
139
|
+
*
|
|
140
|
+
* // Stream behavior
|
|
141
|
+
* const sourceStream = from([1, 2, 3]); // Items at T+0, T+100, T+200
|
|
142
|
+
* const delayedStream = pipe(sourceStream, delayEach(1000));
|
|
143
|
+
* // Emits 1 (at T+1000), 2 (at T+1100), 3 (at T+1200)
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* ## Practical Use Case
|
|
147
|
+
*
|
|
148
|
+
* Use `delayEach` to simulate processing time for each item, or to add
|
|
149
|
+
* consistent lag to every operation (useful for testing race conditions
|
|
150
|
+
* or simulating network latency per request).
|
|
151
|
+
*
|
|
152
|
+
* ## Key Insight
|
|
153
|
+
*
|
|
154
|
+
* `delayEach` maintains the original spacing between items while shifting
|
|
155
|
+
* each one. For delaying the entire stream timeline, use `delay` instead.
|
|
156
|
+
*
|
|
157
|
+
* @typeParam T - Type of values from the source stream
|
|
158
|
+
* @param ms - The delay duration in milliseconds for each item
|
|
159
|
+
* @returns A stream operator that delays each value individually
|
|
160
|
+
*/
|
|
161
|
+
function delayEach(ms) {
|
|
162
|
+
return (0, operators_js_1.createStatefulOperator)({
|
|
163
|
+
name: "delayEach",
|
|
164
|
+
errorMode: "pass-through",
|
|
165
|
+
createState: () => ({
|
|
166
|
+
pendingTimeouts: new Set(),
|
|
167
|
+
isComplete: false,
|
|
168
|
+
}),
|
|
169
|
+
transform(chunk, state, controller) {
|
|
170
|
+
// Schedule delayed emission for this specific item
|
|
171
|
+
const timeout = setTimeout((_chunk) => {
|
|
172
|
+
controller.enqueue(_chunk);
|
|
173
|
+
state.pendingTimeouts.delete(timeout);
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
// If stream ended and this was the last pending timeout, close stream
|
|
176
|
+
if (state.isComplete && state.pendingTimeouts.size === 0) {
|
|
177
|
+
controller.terminate();
|
|
178
|
+
}
|
|
179
|
+
}, ms, chunk);
|
|
180
|
+
state.pendingTimeouts.add(timeout);
|
|
181
|
+
},
|
|
182
|
+
flush(state, controller) {
|
|
183
|
+
state.isComplete = true;
|
|
184
|
+
// If no pending timeouts, stream can complete immediately
|
|
185
|
+
if (state.pendingTimeouts.size === 0) {
|
|
186
|
+
controller.terminate();
|
|
187
|
+
}
|
|
188
|
+
// Otherwise, wait for pending timeouts to finish (handled in transform)
|
|
189
|
+
},
|
|
190
|
+
cancel(state) {
|
|
191
|
+
// Critical: clean up all pending timeouts to prevent memory leaks
|
|
192
|
+
for (const timeout of state.pendingTimeouts) {
|
|
193
|
+
clearTimeout(timeout);
|
|
194
|
+
}
|
|
195
|
+
state.pendingTimeouts.clear();
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Emits only the latest item after a specified period of inactivity.
|
|
201
|
+
*
|
|
202
|
+
* This is the "search bar" operator. It waits for the user to stop typing
|
|
203
|
+
* before firing off a search query.
|
|
204
|
+
*
|
|
205
|
+
* > Note: This operator does not delay error emissions. If an error occurs,
|
|
206
|
+
* > it will be emitted immediately, regardless of the debounce period.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* import { pipe, debounce, from } from "./helpers/mod.ts";
|
|
211
|
+
*
|
|
212
|
+
* // No direct Array equivalent.
|
|
213
|
+
*
|
|
214
|
+
* // Stream behavior
|
|
215
|
+
* const inputStream = from(["a", "ab", "abc"]); // User typing quickly
|
|
216
|
+
* const debouncedStream = pipe(inputStream, debounce(300));
|
|
217
|
+
* // After 300ms of no new input, it will emit "abc".
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* ## Practical Use Case
|
|
221
|
+
*
|
|
222
|
+
* Use `debounce` for any event that fires rapidly, but you only care about the
|
|
223
|
+
* final value, such as search inputs, window resize events, or auto-saving
|
|
224
|
+
* form fields.
|
|
225
|
+
*
|
|
226
|
+
* ## Key Insight
|
|
227
|
+
*
|
|
228
|
+
* `debounce` filters out noise from rapid-fire events, ensuring that expensive
|
|
229
|
+
* operations (like API calls) are only triggered when necessary.
|
|
230
|
+
*
|
|
231
|
+
* @typeParam T - Type of values from the source stream
|
|
232
|
+
* @param ms - The debounce duration in milliseconds
|
|
233
|
+
* @returns A stream operator that debounces values
|
|
234
|
+
*/
|
|
235
|
+
function debounce(ms) {
|
|
236
|
+
return (0, operators_js_1.createStatefulOperator)({
|
|
237
|
+
name: "debounce",
|
|
238
|
+
errorMode: "pass-through",
|
|
239
|
+
createState: () => ({
|
|
240
|
+
timeout: null,
|
|
241
|
+
lastValue: null,
|
|
242
|
+
hasValue: false,
|
|
243
|
+
}),
|
|
244
|
+
transform(chunk, state, controller) {
|
|
245
|
+
// Cancel any pending timeout
|
|
246
|
+
if (state.timeout !== null) {
|
|
247
|
+
clearTimeout(state.timeout);
|
|
248
|
+
state.timeout = null;
|
|
249
|
+
}
|
|
250
|
+
// Store the latest value
|
|
251
|
+
state.lastValue = chunk;
|
|
252
|
+
state.hasValue = true;
|
|
253
|
+
// Set up a new timeout to emit the latest value
|
|
254
|
+
state.timeout = setTimeout(() => {
|
|
255
|
+
if (state.hasValue) {
|
|
256
|
+
controller.enqueue(state.lastValue);
|
|
257
|
+
}
|
|
258
|
+
state.timeout = null;
|
|
259
|
+
state.lastValue = null;
|
|
260
|
+
state.hasValue = false;
|
|
261
|
+
}, ms);
|
|
262
|
+
},
|
|
263
|
+
flush(state, controller) {
|
|
264
|
+
// Clear any pending timeout
|
|
265
|
+
if (state.timeout !== null) {
|
|
266
|
+
clearTimeout(state.timeout);
|
|
267
|
+
state.timeout = null;
|
|
268
|
+
}
|
|
269
|
+
// Emit the last value if we have one
|
|
270
|
+
if (state.hasValue) {
|
|
271
|
+
controller.enqueue(state.lastValue);
|
|
272
|
+
state.lastValue = null;
|
|
273
|
+
state.hasValue = false;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
cancel(state) {
|
|
277
|
+
// Clean up on cancellation
|
|
278
|
+
if (state.timeout !== null) {
|
|
279
|
+
clearTimeout(state.timeout);
|
|
280
|
+
state.timeout = null;
|
|
281
|
+
}
|
|
282
|
+
state.lastValue = null;
|
|
283
|
+
state.hasValue = false;
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Limits the stream to emit at most one item per specified time interval.
|
|
289
|
+
*
|
|
290
|
+
* This is the "scroll event" operator. It ensures that even if an event fires
|
|
291
|
+
* hundreds of times per second, you only handle it at a manageable rate.
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```ts
|
|
295
|
+
* import { pipe, throttle, from } from "./helpers/mod.ts";
|
|
296
|
+
*
|
|
297
|
+
* // No direct Array equivalent.
|
|
298
|
+
*
|
|
299
|
+
* // Stream behavior
|
|
300
|
+
* const scrollStream = from([10, 20, 50, 100, 150]); // Scroll events
|
|
301
|
+
* const throttledStream = pipe(scrollStream, throttle(100));
|
|
302
|
+
* // Emits 10 immediately, then waits 100ms before being able to emit again.
|
|
303
|
+
* // If 150 is the last value, it will be emitted after the throttle window.
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* ## Practical Use Case
|
|
307
|
+
*
|
|
308
|
+
* Use `throttle` for high-frequency events where you need to guarantee a
|
|
309
|
+
* regular sampling of the data, such as scroll position tracking, mouse
|
|
310
|
+
* movement, or real-time data visualization.
|
|
311
|
+
*
|
|
312
|
+
* ## Key Insight
|
|
313
|
+
*
|
|
314
|
+
* `throttle` guarantees a steady flow of data, unlike `debounce` which waits
|
|
315
|
+
* for silence. It's about rate-limiting, not just handling the final value.
|
|
316
|
+
*
|
|
317
|
+
* @typeParam T - Type of values from the source stream
|
|
318
|
+
* @param ms - The throttle duration in milliseconds
|
|
319
|
+
* @returns A stream operator that throttles values
|
|
320
|
+
*/
|
|
321
|
+
function throttle(ms) {
|
|
322
|
+
return (0, operators_js_1.createStatefulOperator)({
|
|
323
|
+
name: "throttle",
|
|
324
|
+
errorMode: "pass-through",
|
|
325
|
+
createState: () => ({
|
|
326
|
+
lastEmitTime: 0,
|
|
327
|
+
nextValue: null,
|
|
328
|
+
hasNextValue: false,
|
|
329
|
+
timeoutId: null,
|
|
330
|
+
}),
|
|
331
|
+
transform(chunk, state, controller) {
|
|
332
|
+
if ((0, error_js_1.isObservableError)(chunk)) {
|
|
333
|
+
// If the chunk is an error, we can immediately emit it
|
|
334
|
+
controller.enqueue(chunk);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
const timeSinceLastEmit = now - state.lastEmitTime;
|
|
339
|
+
// If we haven't emitted for the throttle duration, emit immediately
|
|
340
|
+
if (timeSinceLastEmit >= ms) {
|
|
341
|
+
state.lastEmitTime = now;
|
|
342
|
+
controller.enqueue(chunk);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Otherwise, store this value to emit later
|
|
346
|
+
state.nextValue = chunk;
|
|
347
|
+
state.hasNextValue = true;
|
|
348
|
+
// If we don't have a timeout scheduled, schedule one
|
|
349
|
+
if (state.timeoutId === null) {
|
|
350
|
+
const remainingTime = ms - timeSinceLastEmit;
|
|
351
|
+
state.timeoutId = setTimeout(() => {
|
|
352
|
+
if (state.hasNextValue) {
|
|
353
|
+
state.lastEmitTime = Date.now();
|
|
354
|
+
controller.enqueue(state.nextValue);
|
|
355
|
+
state.nextValue = null;
|
|
356
|
+
state.hasNextValue = false;
|
|
357
|
+
}
|
|
358
|
+
state.timeoutId = null;
|
|
359
|
+
}, remainingTime);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
flush(state, controller) {
|
|
363
|
+
// Clean up any scheduled timeout
|
|
364
|
+
if (state.timeoutId !== null) {
|
|
365
|
+
clearTimeout(state.timeoutId);
|
|
366
|
+
state.timeoutId = null;
|
|
367
|
+
}
|
|
368
|
+
// Emit the last value if we have one
|
|
369
|
+
if (state.hasNextValue) {
|
|
370
|
+
controller.enqueue(state.nextValue);
|
|
371
|
+
state.nextValue = null;
|
|
372
|
+
state.hasNextValue = false;
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
cancel(state) {
|
|
376
|
+
// Clean up on cancellation
|
|
377
|
+
if (state.timeoutId !== null) {
|
|
378
|
+
clearTimeout(state.timeoutId);
|
|
379
|
+
state.timeoutId = null;
|
|
380
|
+
}
|
|
381
|
+
state.nextValue = null;
|
|
382
|
+
state.hasNextValue = false;
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Errors if an item takes too long to be processed.
|
|
388
|
+
*
|
|
389
|
+
* This operator passes normal synchronous values through immediately, but when
|
|
390
|
+
* an upstream operator emits a promise-like chunk it waits for that chunk to
|
|
391
|
+
* settle and races it against a timer. If the promise-like chunk does not
|
|
392
|
+
* resolve before the timer finishes, it emits an `ObservableError` instead.
|
|
393
|
+
*
|
|
394
|
+
* The implementation uses an async transform so Web Streams backpressure keeps
|
|
395
|
+
* later chunks from overtaking the timed chunk. In practice that means operator
|
|
396
|
+
* chains still stay ordered: each promise-like chunk either resolves to a value
|
|
397
|
+
* or times out before the next chunk is processed.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```ts
|
|
401
|
+
* import { pipe, timeout, from } from "./helpers/mod.ts";
|
|
402
|
+
*
|
|
403
|
+
* // Stream behavior
|
|
404
|
+
* const sourceStream = from([
|
|
405
|
+
* new Promise(res => setTimeout(() => res(1), 100)),
|
|
406
|
+
* new Promise(res => setTimeout(() => res(2), 2000))
|
|
407
|
+
* ]);
|
|
408
|
+
*
|
|
409
|
+
* const timedStream = pipe(sourceStream, timeout(1000));
|
|
410
|
+
* // Emits 1, then emits an error because the second promise took too long.
|
|
411
|
+
* ```
|
|
412
|
+
*
|
|
413
|
+
* ## Practical Use Case
|
|
414
|
+
*
|
|
415
|
+
* Use `timeout` to enforce Service Level Agreements (SLAs) on asynchronous
|
|
416
|
+
* operations, such as API calls. If a request takes too long, you can gracefully
|
|
417
|
+
* handle the timeout instead of letting your application hang.
|
|
418
|
+
*
|
|
419
|
+
* ## Key Insight
|
|
420
|
+
*
|
|
421
|
+
* `timeout` is a crucial tool for building resilient systems that can handle
|
|
422
|
+
* slow or unresponsive dependencies. It turns an indefinite wait into a
|
|
423
|
+
* predictable failure.
|
|
424
|
+
*
|
|
425
|
+
* @typeParam T The type of data in the stream.
|
|
426
|
+
* @param ms The timeout duration in milliseconds.
|
|
427
|
+
* @returns An operator that enforces a timeout on each item.
|
|
428
|
+
*/
|
|
429
|
+
function timeout(ms) {
|
|
430
|
+
return (0, operators_js_1.createOperator)({
|
|
431
|
+
name: "timeout",
|
|
432
|
+
errorMode: "pass-through",
|
|
433
|
+
async transform(chunk, controller) {
|
|
434
|
+
// If the chunk is an error, we can immediately emit it
|
|
435
|
+
if ((0, error_js_1.isObservableError)(chunk)) {
|
|
436
|
+
controller.enqueue(chunk);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (chunk !== null &&
|
|
440
|
+
(typeof chunk === "object" || typeof chunk === "function") &&
|
|
441
|
+
"then" in chunk &&
|
|
442
|
+
typeof chunk.then === "function") {
|
|
443
|
+
const timeoutResult = Symbol("timeout");
|
|
444
|
+
let timeoutId;
|
|
445
|
+
const result = await Promise.race([
|
|
446
|
+
chunk,
|
|
447
|
+
new Promise((resolve) => {
|
|
448
|
+
timeoutId = setTimeout(() => resolve(timeoutResult), ms);
|
|
449
|
+
}),
|
|
450
|
+
]);
|
|
451
|
+
if (timeoutId !== undefined) {
|
|
452
|
+
clearTimeout(timeoutId);
|
|
453
|
+
}
|
|
454
|
+
if (result === timeoutResult) {
|
|
455
|
+
controller.enqueue(error_js_1.ObservableError.from(new Error(`Operation timed out after ${ms}ms`), "operator:timeout", { timeoutMs: ms, chunk }));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
controller.enqueue(result);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
controller.enqueue(chunk);
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|