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
package/src/http/sse.js CHANGED
@@ -63,11 +63,15 @@ const stable_delay = 5000
63
63
  */
64
64
  function check_sse_options(options) {
65
65
  for (const key of Object.keys(options)) {
66
- if (request_only_keys.has(key)) throw TypeError(`Unknown sse option "${key}"`)
66
+ if (request_only_keys.has(key)) {
67
+ throw TypeError(`Unknown sse option "${key}"`)
68
+ }
69
+ }
70
+ if (options.as != null && options.as != "json" && options.as != "text") {
71
+ throw TypeError(
72
+ "The as of sse() must be \"json\" or \"text\""
73
+ )
67
74
  }
68
- if (options.as != null && options.as != "json" && options.as != "text") throw TypeError(
69
- "The as of sse() must be \"json\" or \"text\""
70
- )
71
75
  check_reconnect(options.reconnect, "sse")
72
76
  }
73
77
  /**
@@ -76,8 +80,8 @@ function check_sse_options(options) {
76
80
  */
77
81
  function is_reconnectable(error) {
78
82
  return error instanceof NetworkError
79
- || error instanceof TimeoutError
80
- || error instanceof HttpError && (error.status < 400 || error.status >= 500 || sse_retry_statuses.has(error.status))
83
+ || error instanceof TimeoutError
84
+ || error instanceof HttpError && (error.status < 400 || error.status >= 500 || sse_retry_statuses.has(error.status))
81
85
  }
82
86
  /**
83
87
  * @param {string} data
@@ -121,6 +125,45 @@ export function sse(c, path, params, options) {
121
125
  }
122
126
  )
123
127
  : noop
128
+ return {
129
+ [Symbol.asyncIterator]() {
130
+ const controller = new AbortController()
131
+ current = controller
132
+ return watch_records(
133
+ connect(controller),
134
+ left => {
135
+ if (!left) {
136
+ return
137
+ }
138
+ controller.abort(
139
+ new CancelError("The event stream was closed")
140
+ )
141
+ if (current == controller) {
142
+ set_status(
143
+ closed.signal.aborted ? "closed" : "idle"
144
+ )
145
+ }
146
+ }
147
+ )
148
+ },
149
+ /**
150
+ * @param {unknown} reason
151
+ * @returns {void}
152
+ */
153
+ cancel(reason) {
154
+ unwatch()
155
+ closed.abort(
156
+ new CancelError(reason ?? "Cancelled")
157
+ )
158
+ set_status("closed")
159
+ },
160
+ get lastEventId() {
161
+ return last_event_id
162
+ },
163
+ get status() {
164
+ return options.signal?.aborted ? "closed" : stream_status
165
+ }
166
+ }
124
167
  /**
125
168
  * @param {AbortController} controller
126
169
  * @returns {AsyncGenerator<import("../../public.js").ServerEvent<unknown>, void, undefined>}
@@ -133,19 +176,16 @@ export function sse(c, path, params, options) {
133
176
  let last_error
134
177
  /** @type {number | undefined} */
135
178
  let server_retry
136
- /**
137
- * @param {import("../../public.js").ConnectionStatus} next
138
- * @returns {void}
139
- */
140
- function show(next) {
141
- if (current == controller && !controller.signal.aborted) set_status(next)
142
- }
143
179
  try {
144
- if (controller.signal.aborted) return
180
+ if (controller.signal.aborted) {
181
+ return
182
+ }
145
183
  show("connecting")
146
- if (reconnect && options.body instanceof ReadableStream) throw TypeError(
147
- "An event stream with a ReadableStream body cannot reconnect: set reconnect to false"
148
- )
184
+ if (reconnect && options.body instanceof ReadableStream) {
185
+ throw TypeError(
186
+ "An event stream with a ReadableStream body cannot reconnect: set reconnect to false"
187
+ )
188
+ }
149
189
  for (;;) {
150
190
  const connection = new AbortController()
151
191
  let opened_at = 0
@@ -155,10 +195,12 @@ export function sse(c, path, params, options) {
155
195
  try {
156
196
  const headers = new Headers(options.headers)
157
197
  headers.set("Accept", "text/event-stream")
158
- if (last_event_id) headers.set(
159
- "Last-Event-ID",
160
- to_byte_string(last_event_id)
161
- )
198
+ if (last_event_id) {
199
+ headers.set(
200
+ "Last-Event-ID",
201
+ to_byte_string(last_event_id)
202
+ )
203
+ }
162
204
  /** @type {import("../../public.js").RawResponse<unknown>} */
163
205
  const {
164
206
  data,
@@ -183,7 +225,9 @@ export function sse(c, path, params, options) {
183
225
  true
184
226
  )
185
227
  .raw()
186
- if (status == 204) return
228
+ if (status == 204) {
229
+ return
230
+ }
187
231
  if (!event_stream_regex.test(
188
232
  response_headers.get("Content-Type") ?? ""
189
233
  )) {
@@ -196,9 +240,11 @@ export function sse(c, path, params, options) {
196
240
  show("open")
197
241
  const records = /** @type {AsyncIterable<StreamRecord>} */(data)/**/
198
242
  for await (const record of records) {
199
- if (record.type == "retry") server_retry = record.retry
200
- else if (record.type == "id") last_event_id = record.id
201
- else {
243
+ if (record.type == "retry") {
244
+ server_retry = record.retry
245
+ } else if (record.type == "id") {
246
+ last_event_id = record.id
247
+ } else {
202
248
  last_event_id = record.event.id
203
249
  const event_data = as_json ? read_json(record.event.data) : record.event.data
204
250
  if (!parse) {
@@ -226,11 +272,17 @@ export function sse(c, path, params, options) {
226
272
  }
227
273
  failures = 0
228
274
  } catch (error) {
229
- if (controller.signal.aborted) return
275
+ if (controller.signal.aborted) {
276
+ return
277
+ }
230
278
  last_error = error
231
279
  report(c, error)
232
- if (opened_at && Date.now() - opened_at >= stable_delay) failures = 0
233
- if (error === fatal || !reconnect || !is_reconnectable(error) || ++failures > reconnect_count) throw error
280
+ if (opened_at && Date.now() - opened_at >= stable_delay) {
281
+ failures = 0
282
+ }
283
+ if (error === fatal || !reconnect || !is_reconnectable(error) || ++failures > reconnect_count) {
284
+ throw error
285
+ }
234
286
  } finally {
235
287
  unlink_connection()
236
288
  connection.abort(
@@ -239,79 +291,65 @@ export function sse(c, path, params, options) {
239
291
  )
240
292
  )
241
293
  }
242
- if (!reconnect || controller.signal.aborted) return
294
+ if (!reconnect || controller.signal.aborted) {
295
+ return
296
+ }
243
297
  show("connecting")
244
298
  const delay = failures
245
299
  ? get_reconnect_wait(reconnect, failures, last_error)
246
300
  : server_retry ?? 3000
247
- if (delay == null) throw last_error
301
+ if (delay == null) {
302
+ throw last_error
303
+ }
248
304
  await wait(
249
305
  delay,
250
306
  controller.signal,
251
307
  !!failures && read_retry_after(last_error) == null
252
308
  )
253
- if (controller.signal.aborted) return
309
+ if (controller.signal.aborted) {
310
+ return
311
+ }
254
312
  }
255
313
  } catch (error) {
256
- if (error !== last_error) report(c, error)
314
+ if (error !== last_error) {
315
+ report(c, error)
316
+ }
257
317
  throw error
258
318
  } finally {
259
- if (current == controller) set_status(
260
- closed.signal.aborted ? "closed" : "idle"
261
- )
319
+ if (current == controller) {
320
+ set_status(
321
+ closed.signal.aborted ? "closed" : "idle"
322
+ )
323
+ }
262
324
  unlink()
263
325
  unlink_closed()
264
326
  controller.abort(
265
327
  new CancelError("The event stream was closed")
266
328
  )
267
329
  }
330
+ /**
331
+ * @param {import("../../public.js").ConnectionStatus} next
332
+ * @returns {void}
333
+ */
334
+ function show(next) {
335
+ if (current == controller && !controller.signal.aborted) {
336
+ set_status(next)
337
+ }
338
+ }
268
339
  }
269
340
  /**
270
341
  * @param {import("../../public.js").ConnectionStatus} next
271
342
  * @returns {void}
272
343
  */
273
344
  function set_status(next) {
274
- if (stream_status == next) return
345
+ if (stream_status == next) {
346
+ return
347
+ }
275
348
  stream_status = next
276
349
  try {
277
350
  options.onStatus?.(next)
278
351
  } catch {}
279
352
  }
280
- return {
281
- [Symbol.asyncIterator]() {
282
- const controller = new AbortController()
283
- current = controller
284
- return watch_records(
285
- connect(controller),
286
- left => {
287
- if (!left) return
288
- controller.abort(
289
- new CancelError("The event stream was closed")
290
- )
291
- if (current == controller) set_status(
292
- closed.signal.aborted ? "closed" : "idle"
293
- )
294
- }
295
- )
296
- },
297
- /**
298
- * @param {unknown} reason
299
- * @returns {void}
300
- */
301
- cancel(reason) {
302
- unwatch()
303
- closed.abort(
304
- new CancelError(reason ?? "Cancelled")
305
- )
306
- set_status("closed")
307
- },
308
- get lastEventId() {
309
- return last_event_id
310
- },
311
- get status() {
312
- return options.signal?.aborted ? "closed" : stream_status
313
- }
314
- }
315
353
  }
316
354
  /**
317
355
  * @param {number} delay
@@ -330,15 +368,16 @@ function wait(delay, signal, online) {
330
368
  resolve()
331
369
  return
332
370
  }
371
+ const timer = setTimeout(done, clamp_delay(delay))
372
+ const unwatch = on_abort(signal, done)
373
+ globalThis.addEventListener("online", done)
374
+ /** @returns {void} */
333
375
  function done() {
334
376
  clearTimeout(timer)
335
377
  unwatch()
336
378
  globalThis.removeEventListener("online", done)
337
379
  resolve()
338
380
  }
339
- const timer = setTimeout(done, clamp_delay(delay))
340
- const unwatch = on_abort(signal, done)
341
- globalThis.addEventListener("online", done)
342
381
  }
343
382
  )
344
383
  }
@@ -24,15 +24,20 @@ export async function throttle(c, key, ms, cancel, signal) {
24
24
  try {
25
25
  await sleep(Math.max(wait, 0), signal)
26
26
  } finally {
27
- if (current.cancel == cancel) current.cancel = void 0
27
+ if (current.cancel == cancel) {
28
+ current.cancel = void 0
29
+ }
28
30
  }
29
31
  }
30
32
  current.next = Date.now() + ms
31
- const sweeper = /** @type {{ unref?: () => void }} */(/** @type {unknown} */(setTimeout(
33
+ /** @type {{ unref?: () => void }} */
34
+ const sweeper = setTimeout(
32
35
  () => {
33
- if (!current.cancel && Date.now() >= current.next && c.shared.throttles.get(key) == current) c.shared.throttles.delete(key)
36
+ if (!current.cancel && Date.now() >= current.next && c.shared.throttles.get(key) == current) {
37
+ c.shared.throttles.delete(key)
38
+ }
34
39
  },
35
40
  clamp_delay(ms)
36
- )))/**/
41
+ )
37
42
  sweeper.unref?.()
38
43
  }
package/src/http/url.js CHANGED
@@ -10,13 +10,19 @@ const trailing_slash_regex = /\/+$/
10
10
  * @returns {string}
11
11
  */
12
12
  function add_query(url, query, first) {
13
- if (!query) return url
13
+ if (!query) {
14
+ return url
15
+ }
14
16
  const hash_index = url.indexOf("#")
15
17
  const without_hash = hash_index < 0 ? url : url.slice(0, hash_index)
16
18
  const hash = hash_index < 0 ? "" : url.slice(hash_index)
17
19
  const query_index = without_hash.indexOf("?")
18
- if (query_index < 0) return without_hash + "?" + query + hash
19
- if (!first) return without_hash + "&" + query + hash
20
+ if (query_index < 0) {
21
+ return without_hash + "?" + query + hash
22
+ }
23
+ if (!first) {
24
+ return without_hash + "&" + query + hash
25
+ }
20
26
  const existing = without_hash.slice(query_index + 1)
21
27
  return without_hash.slice(0, query_index + 1) + query + (existing ? "&" + existing : "") + hash
22
28
  }
@@ -27,17 +33,26 @@ function add_query(url, query, first) {
27
33
  * @returns {void}
28
34
  */
29
35
  function append_query(pairs, key, value) {
30
- if (value == null) return
36
+ if (value == null) {
37
+ return
38
+ }
31
39
  if (Array.isArray(value)) {
32
- for (const item of /** @type {readonly import("../../public.js").QueryValue[]} */(value)/**/) append_query(pairs, key, item)
33
- } else if (value instanceof Date) pairs.push([ key, value.toISOString() ])
34
- else if (typeof value == "object") {
35
- for (const name of Object.keys(value)) append_query(
36
- pairs,
37
- `${key}[${name}]`,
38
- /** @type {{ readonly [key: string]: import("../../public.js").QueryValue }} */(value)/**/[name]
39
- )
40
- } else pairs.push([ key, String(value) ])
40
+ for (const item of /** @type {readonly import("../../public.js").QueryValue[]} */(value)/**/) {
41
+ append_query(pairs, key, item)
42
+ }
43
+ } else if (value instanceof Date) {
44
+ pairs.push([ key, value.toISOString() ])
45
+ } else if (typeof value == "object") {
46
+ for (const name of Object.keys(value)) {
47
+ append_query(
48
+ pairs,
49
+ `${key}[${name}]`,
50
+ /** @type {{ readonly [key: string]: import("../../public.js").QueryValue }} */(value)/**/[name]
51
+ )
52
+ }
53
+ } else {
54
+ pairs.push([ key, String(value) ])
55
+ }
41
56
  }
42
57
  /**
43
58
  * @param {string | undefined} base
@@ -57,6 +72,23 @@ export function build_url(
57
72
  const rest = { ...source }
58
73
  /** @type {string[]} */
59
74
  const used = []
75
+ let url = fill(path)
76
+ if (base && !absolute_regex.test(url)) {
77
+ const split_index = base.search(/[?#]/)
78
+ const base_path = fill(
79
+ split_index < 0 ? base : base.slice(0, split_index)
80
+ )
81
+ const base_query = split_index < 0 || base[split_index] != "?"
82
+ ? ""
83
+ : /** @type {string} */(base.slice(split_index + 1).split("#")[0])/**/
84
+ const trimmed = url.replace(leading_slash_regex, "")
85
+ url = base_path.replace(trailing_slash_regex, "") + (trimmed && !trimmed.startsWith("?") ? "/" : "") + trimmed
86
+ url = add_query(url, base_query, true)
87
+ }
88
+ for (const name of used) {
89
+ delete rest[name]
90
+ }
91
+ return add_query(url, serialize(rest), false)
60
92
  /**
61
93
  * @param {string} text
62
94
  * @returns {string}
@@ -73,33 +105,22 @@ export function build_url(
73
105
  : value instanceof Date
74
106
  ? value.toISOString()
75
107
  : String(value)
76
- if (!value_text) throw TypeError(
77
- `Missing path parameter "${name}" for ${text}`
78
- )
79
- if (dot_segment_regex.test(value_text)) throw TypeError(
80
- `Path parameter "${name}" cannot be "${value_text}"`
81
- )
108
+ if (!value_text) {
109
+ throw TypeError(
110
+ `Missing path parameter "${name}" for ${text}`
111
+ )
112
+ }
113
+ if (dot_segment_regex.test(value_text)) {
114
+ throw TypeError(
115
+ `Path parameter "${name}" cannot be "${value_text}"`
116
+ )
117
+ }
82
118
  used.push(name)
83
119
  return "/" + encodeURIComponent(value_text)
84
120
  }
85
121
  )
86
122
  return split_index < 0 ? filled : filled + text.slice(split_index)
87
123
  }
88
- let url = fill(path)
89
- if (base && !absolute_regex.test(url)) {
90
- const split_index = base.search(/[?#]/)
91
- const base_path = fill(
92
- split_index < 0 ? base : base.slice(0, split_index)
93
- )
94
- const base_query = split_index < 0 || base[split_index] != "?"
95
- ? ""
96
- : base.slice(split_index + 1).split("#")[0] ?? ""
97
- const trimmed = url.replace(leading_slash_regex, "")
98
- url = base_path.replace(trailing_slash_regex, "") + (trimmed && !trimmed.startsWith("?") ? "/" : "") + trimmed
99
- url = add_query(url, base_query, true)
100
- }
101
- for (const name of used) delete rest[name]
102
- return add_query(url, serialize(rest), false)
103
124
  }
104
125
  /**
105
126
  * @param {import("../../public.js").QueryParams} params
@@ -108,7 +129,9 @@ export function build_url(
108
129
  export function serialize_query(params) {
109
130
  /** @type {[string, string][]} */
110
131
  const pairs = []
111
- for (const key of Object.keys(params)) append_query(pairs, key, params[key])
132
+ for (const key of Object.keys(params)) {
133
+ append_query(pairs, key, params[key])
134
+ }
112
135
  return pairs.map(
113
136
  ([ key, value ]) => encodeURIComponent(key) + "=" + encodeURIComponent(value)
114
137
  )
package/src/http/xhr.js CHANGED
@@ -3,7 +3,7 @@ const header_line_regex = /\r?\n/
3
3
  const null_body_statuses = new Set([ 101, 103, 204, 205, 304 ])
4
4
  /**
5
5
  * @param {string} url
6
- * @param {RequestInit & { headers: Headers }} init
6
+ * @param {RequestInit & { headers: Headers, method: string }} init
7
7
  * @param {(progress: import("../../public.js").Progress) => void} on_upload
8
8
  * @returns {Promise<Response>}
9
9
  */
@@ -12,24 +12,7 @@ export function send_with_xhr(url, init, on_upload) {
12
12
  (resolve, reject) => {
13
13
  const xhr = new XMLHttpRequest()
14
14
  const signal = init.signal
15
- function abort() {
16
- return xhr.abort()
17
- }
18
- /**
19
- * @param {() => void} settle
20
- * @returns {() => void}
21
- */
22
- function finish(settle) {
23
- return () => {
24
- signal?.removeEventListener("abort", abort)
25
- try {
26
- settle()
27
- } catch (error) {
28
- reject(error)
29
- }
30
- }
31
- }
32
- xhr.open(init.method ?? "GET", url)
15
+ xhr.open(init.method, url)
33
16
  init.headers.forEach(
34
17
  (value, key) => xhr.setRequestHeader(key, value)
35
18
  )
@@ -51,15 +34,19 @@ export function send_with_xhr(url, init, on_upload) {
51
34
  xhr.ontimeout = xhr.onerror
52
35
  xhr.onload = finish(
53
36
  () => {
54
- if (!xhr.status) throw TypeError("Network request failed")
37
+ if (!xhr.status) {
38
+ throw TypeError("Network request failed")
39
+ }
55
40
  /** @type {Headers} */
56
41
  const headers = new Headers()
57
42
  for (const line of xhr.getAllResponseHeaders().split(header_line_regex)) {
58
43
  const index = line.indexOf(":")
59
- if (index > 0) headers.append(
60
- line.slice(0, index).trim(),
61
- line.slice(index + 1).trim()
62
- )
44
+ if (index > 0) {
45
+ headers.append(
46
+ line.slice(0, index).trim(),
47
+ line.slice(index + 1).trim()
48
+ )
49
+ }
63
50
  }
64
51
  resolve(
65
52
  new Response(
@@ -81,6 +68,24 @@ export function send_with_xhr(url, init, on_upload) {
81
68
  xhr.send(
82
69
  /** @type {XMLHttpRequestBodyInit | null} */(init.body ?? null)/**/
83
70
  )
71
+ /** @returns {void} */
72
+ function abort() {
73
+ return xhr.abort()
74
+ }
75
+ /**
76
+ * @param {() => void} settle
77
+ * @returns {() => void}
78
+ */
79
+ function finish(settle) {
80
+ return () => {
81
+ signal?.removeEventListener("abort", abort)
82
+ try {
83
+ settle()
84
+ } catch (error) {
85
+ reject(error)
86
+ }
87
+ }
88
+ }
84
89
  }
85
90
  )
86
91
  }
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ export { default as flow } from "./flow/index.js"
12
12
  export { offload } from "./offload.js"
13
13
  export { attempt } from "./attempt.js"
14
14
  export { limiter } from "./limiter.js"
15
+ export { redial } from "./redial.js"
15
16
  export {
16
17
  buffer,
17
18
  channel,
package/src/key.js CHANGED
@@ -1,8 +1,6 @@
1
- /**
2
- * @returns {string}
3
- */
1
+ /** @returns {string} */
4
2
  export function random_key() {
5
3
  return globalThis.crypto?.randomUUID?.()
6
- ?? Date.now().toString(36) + Math.random().toString(36)
7
- .slice(2)
4
+ ?? Date.now().toString(36) + Math.random().toString(36)
5
+ .slice(2)
8
6
  }