async-lube 2.0.0 → 2.1.0

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 (70) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/docs/http.md +1 -1
  4. package/docs/reference.md +4 -2
  5. package/docs/tasks.md +19 -2
  6. package/package.json +4 -5
  7. package/public.d.ts +112 -0
  8. package/src/attempt.js +17 -7
  9. package/src/durable/context.js +107 -64
  10. package/src/durable/error.js +39 -15
  11. package/src/durable/index.js +18 -10
  12. package/src/durable/journal.js +29 -14
  13. package/src/durable/json.js +26 -9
  14. package/src/durable/lease.js +15 -5
  15. package/src/durable/persist.js +60 -22
  16. package/src/durable/run.js +103 -41
  17. package/src/durable/send.js +30 -13
  18. package/src/durable/serve.js +30 -24
  19. package/src/durable/state.js +28 -13
  20. package/src/durable/store.js +19 -10
  21. package/src/durable/watch.js +31 -11
  22. package/src/errors.js +9 -3
  23. package/src/flow/attempts.js +49 -29
  24. package/src/flow/builder.js +260 -161
  25. package/src/flow/compile.js +86 -66
  26. package/src/flow/graph.js +108 -42
  27. package/src/flow/index.js +13 -7
  28. package/src/flow/items.js +79 -46
  29. package/src/flow/nodes.js +267 -117
  30. package/src/flow/refs.js +27 -13
  31. package/src/flow/resources.js +9 -3
  32. package/src/flow/run.js +287 -114
  33. package/src/flow/streams.js +44 -19
  34. package/src/flow/subflows.js +93 -53
  35. package/src/http/config.js +11 -5
  36. package/src/http/index.js +25 -14
  37. package/src/http/refresh.js +29 -12
  38. package/src/http/request.js +246 -151
  39. package/src/http/response.js +105 -50
  40. package/src/http/retry.js +52 -27
  41. package/src/http/socket.js +220 -172
  42. package/src/http/sse.js +113 -74
  43. package/src/http/throttle.js +9 -4
  44. package/src/http/url.js +58 -35
  45. package/src/http/xhr.js +29 -24
  46. package/src/index.js +1 -0
  47. package/src/key.js +3 -5
  48. package/src/limiter.js +146 -65
  49. package/src/offload.js +116 -85
  50. package/src/offload_module.js +594 -0
  51. package/src/options.js +8 -4
  52. package/src/redial.js +129 -0
  53. package/src/signal.js +14 -4
  54. package/src/stream.js +353 -198
  55. package/types/attempt.d.ts.map +1 -1
  56. package/types/durable/index.d.ts.map +1 -1
  57. package/types/durable/state.d.ts.map +1 -1
  58. package/types/durable/store.d.ts.map +1 -1
  59. package/types/errors.d.ts.map +1 -1
  60. package/types/http/index.d.ts.map +1 -1
  61. package/types/index.d.ts +1 -0
  62. package/types/index.d.ts.map +1 -1
  63. package/types/limiter.d.ts +5 -1
  64. package/types/limiter.d.ts.map +1 -1
  65. package/types/offload.d.ts +2 -16
  66. package/types/offload.d.ts.map +1 -1
  67. package/types/redial.d.ts +28 -0
  68. package/types/redial.d.ts.map +1 -0
  69. package/types/stream.d.ts +2 -0
  70. package/types/stream.d.ts.map +1 -1
@@ -0,0 +1,594 @@
1
+ /** @import { NodeWorker, OffloadReply, OffloadTransport } from "../private.js" */
2
+ import { CancelError } from "./errors.js"
3
+ import { check_options } from "./options.js"
4
+ import { noop } from "./signal.js"
5
+ /**
6
+ * @template Q, R, D, B
7
+ * @param {string | URL} target
8
+ * @param {import("../public.js").OffloadModuleOptions<D, B>=} options
9
+ * @returns {import("../public.js").OffloadModule<Q, R>}
10
+ */
11
+ export function offload_module(target, options = {}) {
12
+ const url = typeof target == "string" ? new URL(target) : target
13
+ check_options(
14
+ options,
15
+ [ "data", "delay", "onBatch" ],
16
+ "offload"
17
+ )
18
+ const { data, delay = 0, onBatch } = options
19
+ if (typeof delay != "number" || !(Number.isFinite(delay) && delay >= 0)) {
20
+ throw TypeError(
21
+ "The delay of offload() must be a non-negative number of milliseconds"
22
+ )
23
+ }
24
+ if (onBatch !== void 0 && typeof onBatch != "function") {
25
+ throw TypeError(
26
+ "The onBatch of offload() must be a function"
27
+ )
28
+ }
29
+ /** @type {Map<number, { reject: (error: unknown) => void, resolve: (value: R) => void }>} */
30
+ const pending = new Map()
31
+ /** @type {[number, Q][]} */
32
+ let outbox = []
33
+ /** @type {OffloadTransport} */
34
+ let transport = {
35
+ post: noop,
36
+ ref: noop,
37
+ stop: () => Promise.resolve()
38
+ }
39
+ /** @type {{ error: unknown } | undefined} */
40
+ let failure
41
+ /** @type {Promise<void> | undefined} */
42
+ let closing
43
+ /** @type {((outcome: { error: unknown } | undefined) => void) | undefined} */
44
+ let finish_closing
45
+ let chain = Promise.resolve()
46
+ let count = 0
47
+ let closed = false
48
+ let flushing = false
49
+ let is_ready = false
50
+ let resolve_ready = noop
51
+ /** @type {(error: unknown) => void} */
52
+ let reject_ready = noop
53
+ const ready = new Promise(
54
+ (resolve, reject) => {
55
+ resolve_ready = () => resolve(void 0)
56
+ reject_ready = reject
57
+ }
58
+ )
59
+ ready.catch(noop)
60
+ try {
61
+ transport = open_thread(
62
+ url,
63
+ data,
64
+ delay,
65
+ receive,
66
+ error => fail(to_error(error))
67
+ )
68
+ } catch (error) {
69
+ fail(error)
70
+ }
71
+ update_ref()
72
+ return {
73
+ call,
74
+ close: () => closing ??= shutdown(),
75
+ ready: /** @type {Promise<void>} */(ready)/**/
76
+ }
77
+ /**
78
+ * @param {OffloadReply[]} entries
79
+ * @returns {void}
80
+ */
81
+ function apply(entries) {
82
+ for (const [ id, ok, value ] of entries) {
83
+ settle(
84
+ id,
85
+ ok,
86
+ ok ? value : to_error(value)
87
+ )
88
+ }
89
+ }
90
+ /**
91
+ * @param {Q} request
92
+ * @returns {Promise<R>}
93
+ */
94
+ function call(request) {
95
+ if (failure) {
96
+ return Promise.reject(failure.error)
97
+ }
98
+ if (closed) {
99
+ return Promise.reject(
100
+ new CancelError("The offload was closed")
101
+ )
102
+ }
103
+ return new Promise(
104
+ (resolve, reject) => {
105
+ const id = ++count
106
+ pending.set(id, { reject, resolve })
107
+ outbox.push([ id, request ])
108
+ update_ref()
109
+ if (!flushing) {
110
+ flushing = true
111
+ queueMicrotask(flush)
112
+ }
113
+ }
114
+ )
115
+ }
116
+ /**
117
+ * @param {unknown} error
118
+ * @returns {void}
119
+ */
120
+ function fail(error) {
121
+ if (failure) {
122
+ return
123
+ }
124
+ failure = { error }
125
+ reject_ready(error)
126
+ const jobs = [ ...pending.values() ]
127
+ pending.clear()
128
+ outbox = []
129
+ for (const job of jobs) {
130
+ job.reject(error)
131
+ }
132
+ void transport.stop()
133
+ update_ref()
134
+ finish_closing?.({ error })
135
+ }
136
+ /** @returns {void} */
137
+ function flush() {
138
+ flushing = false
139
+ if (!is_ready || failure || !outbox.length) {
140
+ return
141
+ }
142
+ const calls = outbox
143
+ outbox = []
144
+ try {
145
+ transport.post([ "calls", calls ])
146
+ } catch {
147
+ for (const entry of calls) {
148
+ try {
149
+ transport.post([ "calls", [ entry ] ])
150
+ } catch (error) {
151
+ settle(entry[0], false, error)
152
+ }
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * @param {unknown[]} message
158
+ * @returns {void}
159
+ */
160
+ function receive(message) {
161
+ const [ kind, first, second ] = message
162
+ if (kind == "ready") {
163
+ is_ready = true
164
+ resolve_ready()
165
+ update_ref()
166
+ flush()
167
+ } else if (kind == "failed") {
168
+ fail(to_error(first))
169
+ } else if (kind == "replies") {
170
+ replied(
171
+ /** @type {OffloadReply[]} */(first)/**/,
172
+ second
173
+ )
174
+ } else if (kind == "closed") {
175
+ finish_closing?.(
176
+ first ? { error: to_error(second) } : void 0
177
+ )
178
+ }
179
+ }
180
+ /**
181
+ * @param {OffloadReply[]} entries
182
+ * @param {unknown} value
183
+ * @returns {void}
184
+ */
185
+ function replied(entries, value) {
186
+ if (!onBatch) {
187
+ apply(entries)
188
+ return
189
+ }
190
+ chain = chain.then(
191
+ async () => {
192
+ if (value !== void 0) {
193
+ try {
194
+ await onBatch(/** @type {B} */(value)/**/)
195
+ } catch (error) {
196
+ for (const [ id ] of entries) {
197
+ settle(id, false, error)
198
+ }
199
+ return
200
+ }
201
+ }
202
+ apply(entries)
203
+ }
204
+ )
205
+ }
206
+ /**
207
+ * @param {number} id
208
+ * @param {boolean} ok
209
+ * @param {unknown} value
210
+ * @returns {void}
211
+ */
212
+ function settle(id, ok, value) {
213
+ const job = pending.get(id)
214
+ if (!job) {
215
+ return
216
+ }
217
+ pending.delete(id)
218
+ update_ref()
219
+ if (ok) {
220
+ job.resolve(/** @type {R} */(value)/**/)
221
+ } else {
222
+ job.reject(value)
223
+ }
224
+ }
225
+ /** @returns {Promise<void>} */
226
+ async function shutdown() {
227
+ closed = true
228
+ update_ref()
229
+ await ready.catch(noop)
230
+ /** @type {{ error: unknown } | undefined} */
231
+ let outcome
232
+ if (!failure) {
233
+ flush()
234
+ outcome = await new Promise(
235
+ resolve => {
236
+ finish_closing = resolve
237
+ try {
238
+ transport.post([ "close" ])
239
+ } catch (error) {
240
+ resolve({ error })
241
+ }
242
+ }
243
+ )
244
+ }
245
+ await chain
246
+ await transport.stop()
247
+ if (outcome) {
248
+ throw outcome.error
249
+ }
250
+ }
251
+ /** @returns {void} */
252
+ function update_ref() {
253
+ transport.ref(
254
+ pending.size > 0 || closed || !is_ready && !failure
255
+ )
256
+ }
257
+ }
258
+ /**
259
+ * @param {URL} url
260
+ * @param {unknown} data
261
+ * @param {number} delay
262
+ * @param {(message: unknown[]) => void} on_message
263
+ * @param {(error: unknown) => void} on_failure
264
+ * @returns {OffloadTransport}
265
+ */
266
+ function open_thread(
267
+ url,
268
+ data,
269
+ delay,
270
+ on_message,
271
+ on_failure
272
+ ) {
273
+ const threads = /** @type {{ process?: { getBuiltinModule?: (name: string) => unknown } }} */(globalThis)/**/.process?.getBuiltinModule?.("node:worker_threads")
274
+ const rest = `() => import(${JSON.stringify(url.href)}), ${delay}`
275
+ if (threads) {
276
+ const { Worker } = /** @type {{ Worker: new (code: string, options: { eval: true, workerData: unknown }) => NodeWorker }} */(threads)/**/
277
+ const thread = new Worker(
278
+ `const { parentPort, workerData } = process.getBuiltinModule("node:worker_threads");\n(${worker_main})(message => parentPort.postMessage(message), listener => parentPort.on("message", listener), fn => setImmediate(fn), ${rest}, workerData)`,
279
+ { eval: true, workerData: data }
280
+ )
281
+ let stopped = false
282
+ thread.on("message", on_message)
283
+ thread.on("error", on_failure)
284
+ thread.on(
285
+ "exit",
286
+ code => {
287
+ if (!stopped) {
288
+ on_failure(
289
+ Error(
290
+ `The worker of offload() exited with code ${code}`
291
+ )
292
+ )
293
+ }
294
+ }
295
+ )
296
+ thread.unref()
297
+ return {
298
+ post: message => thread.postMessage(message),
299
+ ref: on => on ? thread.ref() : thread.unref(),
300
+ stop: () => {
301
+ stopped = true
302
+ return thread.terminate().then(noop)
303
+ }
304
+ }
305
+ }
306
+ if (typeof Worker == "function") {
307
+ const source = URL.createObjectURL(
308
+ new Blob(
309
+ [
310
+ `self.onmessage = ({ data: [ , data ] }) => (${worker_main})(message => self.postMessage(message), listener => { self.onmessage = ({ data }) => listener(data) }, fn => setTimeout(fn, 0), ${rest}, data)`
311
+ ],
312
+ { type: "text/javascript" }
313
+ )
314
+ )
315
+ const thread = new Worker(source, { type: "module" })
316
+ thread.onmessage = event => on_message(event.data)
317
+ thread.onerror = event => {
318
+ event.preventDefault()
319
+ on_failure(
320
+ event.error ?? Error(event.message)
321
+ )
322
+ }
323
+ try {
324
+ thread.postMessage([ "init", data ])
325
+ } catch (error) {
326
+ thread.terminate()
327
+ URL.revokeObjectURL(source)
328
+ throw error
329
+ }
330
+ return {
331
+ post: message => thread.postMessage(message),
332
+ ref: noop,
333
+ stop: () => {
334
+ thread.terminate()
335
+ URL.revokeObjectURL(source)
336
+ return Promise.resolve()
337
+ }
338
+ }
339
+ }
340
+ throw TypeError(
341
+ "offload() needs Worker, or worker_threads of Node.js 20.16 or later"
342
+ )
343
+ }
344
+ /**
345
+ * @param {unknown} value
346
+ * @returns {unknown}
347
+ */
348
+ export function to_error(value) {
349
+ if (value instanceof Error || value == null || typeof value != "object" || !("message" in value)) {
350
+ return value
351
+ }
352
+ const error = Error(String(value.message))
353
+ error.name = String(
354
+ /** @type {{ name?: unknown }} */(value)/**/.name ?? "Error"
355
+ )
356
+ return error
357
+ }
358
+ /**
359
+ * @param {(message: unknown[]) => void} post
360
+ * @param {(listener: (message: unknown[]) => void) => void} listen
361
+ * @param {(fn: () => void) => void} defer
362
+ * @param {() => Promise<{ default?: unknown }>} import_module
363
+ * @param {number} delay
364
+ * @param {unknown} data
365
+ * @returns {void}
366
+ */
367
+ function worker_main(
368
+ post,
369
+ listen,
370
+ defer,
371
+ import_module,
372
+ delay,
373
+ data
374
+ ) {
375
+ /** @type {import("../public.js").OffloadHandlers<unknown, unknown>} */
376
+ let handlers = { call: () => void 0 }
377
+ /** @type {[number, unknown][]} */
378
+ const queue = []
379
+ let waiting = false
380
+ let running = false
381
+ let closing = false
382
+ listen(
383
+ message => {
384
+ if (message[0] == "close") {
385
+ closing = true
386
+ settle()
387
+ } else {
388
+ queue.push(
389
+ .../** @type {[number, unknown][]} */(message[1])/**/
390
+ )
391
+ wait()
392
+ }
393
+ }
394
+ )
395
+ load().then(
396
+ () => post([ "ready" ]),
397
+ error => send("failed", error)
398
+ )
399
+ /**
400
+ * @param {[number, unknown][]} batch
401
+ * @returns {Promise<void>}
402
+ */
403
+ async function execute(batch) {
404
+ /** @type {Promise<OffloadReply>[]} */
405
+ const outcomes = []
406
+ /** @type {Promise<void> | undefined} */
407
+ let started
408
+ let failed = false
409
+ /** @type {unknown} */
410
+ let failure
411
+ /** @type {unknown} */
412
+ let value
413
+ try {
414
+ value = handlers.batch ? await handlers.batch(run) : await run()
415
+ } catch (error) {
416
+ failed = true
417
+ failure = error
418
+ }
419
+ if (started) {
420
+ await started
421
+ } else if (!failed) {
422
+ failed = true
423
+ failure = Error(
424
+ "The batch function of offload() did not call run()"
425
+ )
426
+ }
427
+ /** @type {OffloadReply[]} */
428
+ const entries = failed
429
+ ? batch.map(
430
+ ([ id ]) => [ id, false, failure ]
431
+ )
432
+ : await Promise.all(outcomes)
433
+ reply(entries, failed ? void 0 : value)
434
+ /**
435
+ * @param {number} id
436
+ * @param {unknown} request
437
+ * @returns {Promise<OffloadReply>}
438
+ */
439
+ function invoke(id, request) {
440
+ try {
441
+ return Promise.resolve(handlers.call(request)).then(
442
+ result => [ id, true, result ],
443
+ error => [ id, false, error ]
444
+ )
445
+ } catch (error) {
446
+ return Promise.resolve([ id, false, error ])
447
+ }
448
+ }
449
+ /** @returns {Promise<void>} */
450
+ function run() {
451
+ started ??= start()
452
+ return started
453
+ }
454
+ /** @returns {Promise<void>} */
455
+ function start() {
456
+ for (const [ id, request ] of batch) {
457
+ outcomes.push(invoke(id, request))
458
+ }
459
+ return Promise.all(outcomes).then(() => void 0)
460
+ }
461
+ }
462
+ /** @returns {Promise<void>} */
463
+ async function finish() {
464
+ try {
465
+ await handlers.close?.()
466
+ } catch (error) {
467
+ try {
468
+ post([ "closed", true, error ])
469
+ } catch {
470
+ post(
471
+ [ "closed", true, plain(error) ]
472
+ )
473
+ }
474
+ return
475
+ }
476
+ post([ "closed", false ])
477
+ }
478
+ /** @returns {void} */
479
+ function fire() {
480
+ waiting = false
481
+ running = true
482
+ execute(queue.splice(0)).then(
483
+ () => {
484
+ running = false
485
+ settle()
486
+ }
487
+ )
488
+ }
489
+ /** @returns {Promise<void>} */
490
+ async function load() {
491
+ const setup = (await import_module()).default
492
+ if (typeof setup != "function") {
493
+ throw TypeError(
494
+ "The module of offload() must export a setup function by default"
495
+ )
496
+ }
497
+ handlers = await setup(data)
498
+ if (typeof handlers?.call != "function") {
499
+ throw TypeError(
500
+ "The setup function of the module of offload() must return an object with a call function"
501
+ )
502
+ }
503
+ }
504
+ /**
505
+ * @param {unknown} error
506
+ * @returns {{ message: string, name: string }}
507
+ */
508
+ function plain(error) {
509
+ /** @type {{ message?: unknown, name?: unknown }} */
510
+ const { message, name } = Object(error)
511
+ return {
512
+ message: String(message ?? error),
513
+ name: String(name ?? "Error")
514
+ }
515
+ }
516
+ /**
517
+ * @param {OffloadReply[]} entries
518
+ * @param {unknown} value
519
+ * @returns {void}
520
+ */
521
+ function reply(entries, value) {
522
+ try {
523
+ post([ "replies", entries, value ])
524
+ } catch {
525
+ let shipped = value
526
+ try {
527
+ structuredClone(value)
528
+ } catch (clone_error) {
529
+ shipped = void 0
530
+ entries = entries.map(
531
+ ([ id ]) => [ id, false, plain(clone_error) ]
532
+ )
533
+ }
534
+ post(
535
+ [
536
+ "replies",
537
+ entries.map(
538
+ entry => {
539
+ try {
540
+ structuredClone(entry)
541
+ return entry
542
+ } catch (clone_error) {
543
+ return [
544
+ entry[0],
545
+ false,
546
+ plain(
547
+ entry[1] ? clone_error : entry[2]
548
+ )
549
+ ]
550
+ }
551
+ }
552
+ ),
553
+ shipped
554
+ ]
555
+ )
556
+ }
557
+ }
558
+ /**
559
+ * @param {string} kind
560
+ * @param {unknown} error
561
+ * @returns {void}
562
+ */
563
+ function send(kind, error) {
564
+ try {
565
+ post([ kind, error ])
566
+ } catch {
567
+ post([ kind, plain(error) ])
568
+ }
569
+ }
570
+ /** @returns {void} */
571
+ function settle() {
572
+ if (running || waiting) {
573
+ return
574
+ }
575
+ if (queue.length) {
576
+ wait()
577
+ } else if (closing) {
578
+ closing = false
579
+ finish()
580
+ }
581
+ }
582
+ /** @returns {void} */
583
+ function wait() {
584
+ if (waiting || running || !queue.length) {
585
+ return
586
+ }
587
+ waiting = true
588
+ if (delay > 0) {
589
+ setTimeout(fire, delay)
590
+ } else {
591
+ defer(fire)
592
+ }
593
+ }
594
+ }
package/src/options.js CHANGED
@@ -6,10 +6,14 @@
6
6
  * @returns {void}
7
7
  */
8
8
  export function check_options(options, keys, kind, prefix = "") {
9
- if (options == null || typeof options != "object") return
9
+ if (options == null || typeof options != "object") {
10
+ return
11
+ }
10
12
  for (const key of Object.keys(options)) {
11
- if (!keys.includes(key)) throw TypeError(
12
- `Unknown ${kind} option "${prefix}${key}"`
13
- )
13
+ if (!keys.includes(key)) {
14
+ throw TypeError(
15
+ `Unknown ${kind} option "${prefix}${key}"`
16
+ )
17
+ }
14
18
  }
15
19
  }