styled-map-package-api 5.0.0-pre.3 → 5.0.0-pre.4

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 (94) hide show
  1. package/README.md +94 -0
  2. package/dist/download.d.ts +11 -21
  3. package/dist/fallbacks.d.ts +32 -0
  4. package/dist/from-mbtiles.d.ts +1 -3
  5. package/dist/index.d.ts +11 -24
  6. package/dist/reader.d.ts +28 -12
  7. package/dist/server.d.ts +23 -14
  8. package/dist/style-downloader.d.ts +13 -19
  9. package/dist/tile-downloader.d.ts +13 -23
  10. package/dist/types.d.ts +61 -0
  11. package/dist/utils/errors.d.ts +2 -4
  12. package/dist/utils/fetch.d.ts +3 -8
  13. package/dist/utils/file-formats.d.ts +3 -10
  14. package/dist/utils/geo.d.ts +17 -9
  15. package/dist/utils/mapbox.d.ts +8 -10
  16. package/dist/utils/misc.d.ts +3 -5
  17. package/dist/utils/streams.d.ts +6 -10
  18. package/dist/utils/style.d.ts +27 -16
  19. package/dist/utils/templates.d.ts +30 -25
  20. package/dist/validator.d.ts +66 -0
  21. package/dist/writer.d.ts +157 -4
  22. package/lib/download.js +125 -0
  23. package/lib/fallbacks.js +157 -0
  24. package/lib/from-mbtiles.js +131 -0
  25. package/lib/index.js +12 -0
  26. package/lib/reader.js +360 -0
  27. package/lib/server.js +222 -0
  28. package/lib/style-downloader.js +369 -0
  29. package/lib/tile-downloader.js +189 -0
  30. package/lib/types.ts +99 -0
  31. package/lib/utils/errors.js +24 -0
  32. package/lib/utils/fetch.js +104 -0
  33. package/lib/utils/file-formats.js +92 -0
  34. package/lib/utils/geo.js +97 -0
  35. package/lib/utils/mapbox.js +155 -0
  36. package/{dist/utils/misc.d.cts → lib/utils/misc.js} +9 -5
  37. package/lib/utils/streams.js +101 -0
  38. package/lib/utils/style.js +206 -0
  39. package/lib/utils/templates.js +165 -0
  40. package/lib/validator.js +789 -0
  41. package/lib/writer.js +652 -0
  42. package/package.json +30 -78
  43. package/dist/download.cjs +0 -100
  44. package/dist/download.d.cts +0 -63
  45. package/dist/download.js +0 -76
  46. package/dist/from-mbtiles.cjs +0 -127
  47. package/dist/from-mbtiles.d.cts +0 -14
  48. package/dist/from-mbtiles.js +0 -103
  49. package/dist/index.cjs +0 -46
  50. package/dist/index.d.cts +0 -24
  51. package/dist/index.js +0 -16
  52. package/dist/reader.cjs +0 -287
  53. package/dist/reader.d.cts +0 -67
  54. package/dist/reader.js +0 -259
  55. package/dist/server.cjs +0 -73
  56. package/dist/server.d.cts +0 -45
  57. package/dist/server.js +0 -49
  58. package/dist/style-downloader.cjs +0 -314
  59. package/dist/style-downloader.d.cts +0 -118
  60. package/dist/style-downloader.js +0 -290
  61. package/dist/tile-downloader.cjs +0 -156
  62. package/dist/tile-downloader.d.cts +0 -82
  63. package/dist/tile-downloader.js +0 -124
  64. package/dist/types-Bhn0-Ldk.d.cts +0 -201
  65. package/dist/types-Bhn0-Ldk.d.ts +0 -201
  66. package/dist/utils/errors.cjs +0 -41
  67. package/dist/utils/errors.d.cts +0 -18
  68. package/dist/utils/errors.js +0 -16
  69. package/dist/utils/fetch.cjs +0 -97
  70. package/dist/utils/fetch.d.cts +0 -50
  71. package/dist/utils/fetch.js +0 -63
  72. package/dist/utils/file-formats.cjs +0 -96
  73. package/dist/utils/file-formats.d.cts +0 -32
  74. package/dist/utils/file-formats.js +0 -70
  75. package/dist/utils/geo.cjs +0 -84
  76. package/dist/utils/geo.d.cts +0 -46
  77. package/dist/utils/geo.js +0 -56
  78. package/dist/utils/mapbox.cjs +0 -121
  79. package/dist/utils/mapbox.d.cts +0 -43
  80. package/dist/utils/mapbox.js +0 -91
  81. package/dist/utils/misc.cjs +0 -39
  82. package/dist/utils/misc.js +0 -13
  83. package/dist/utils/streams.cjs +0 -99
  84. package/dist/utils/streams.d.cts +0 -49
  85. package/dist/utils/streams.js +0 -73
  86. package/dist/utils/style.cjs +0 -126
  87. package/dist/utils/style.d.cts +0 -66
  88. package/dist/utils/style.js +0 -98
  89. package/dist/utils/templates.cjs +0 -124
  90. package/dist/utils/templates.d.cts +0 -79
  91. package/dist/utils/templates.js +0 -85
  92. package/dist/writer.cjs +0 -539
  93. package/dist/writer.d.cts +0 -4
  94. package/dist/writer.js +0 -516
@@ -0,0 +1,104 @@
1
+ import ky from 'ky'
2
+ import pLimit from 'p-limit'
3
+
4
+ import { ProgressStream } from './streams.js'
5
+
6
+ /**
7
+ * @typedef {object} DownloadResponse
8
+ * @property {ReadableStream<Uint8Array>} body Web ReadableStream of the response body
9
+ * @property {string | null} mimeType Content mime-type (from http content-type header)
10
+ * @property {number | null} contentLength Content length in bytes (from http content-length header)
11
+ */
12
+
13
+ /**
14
+ * A wrapper for fetch that limits the number of concurrent downloads.
15
+ */
16
+ export class FetchQueue {
17
+ /** @type {import('p-limit').LimitFunction} */
18
+ #limit
19
+ /** @param {number} concurrency */
20
+ constructor(concurrency) {
21
+ this.#limit = pLimit(concurrency)
22
+ }
23
+
24
+ get activeCount() {
25
+ return this.#limit.activeCount
26
+ }
27
+
28
+ /**
29
+ * Fetch a URL, limiting the number of concurrent downloads. Resolves with a
30
+ * `DownloadResponse`, which is a parsed from the Fetch `Response` objects,
31
+ * with `body` as a web ReadableStream, and the MIME type and content length
32
+ * of the response.
33
+ *
34
+ * NB: The response body stream must be consumed to the end, otherwise the
35
+ * queue will never be emptied.
36
+ *
37
+ * @param {string} url
38
+ * @param {{ onprogress?: import('./streams.js').ProgressCallback }} opts
39
+ * @returns {Promise<DownloadResponse>}
40
+ */
41
+ fetch(url, { onprogress } = {}) {
42
+ // This is wrapped like this so that pLimit limits concurrent `fetchStream`
43
+ // calls, which only resolve when the body is completely downloaded, but
44
+ // this method will return a response as soon as it is available. NB: If the
45
+ // body of a response is never "consumed" (e.g. by reading it to the end),
46
+ // the fetchStream function will never resolve, and the limit will never be
47
+ // released.
48
+ return new Promise((resolveResponse, rejectResponse) => {
49
+ this.#limit(fetchStream, {
50
+ url,
51
+ onresponse: resolveResponse,
52
+ onerror: rejectResponse,
53
+ onprogress,
54
+ })
55
+ })
56
+ }
57
+ }
58
+
59
+ /**
60
+ * This will resolve when the download is complete, regardless of success or
61
+ * failure, but a readable stream is available before download via the
62
+ * onReadStream param. This strange function signature is used for limiting the
63
+ * number of simultaneous downloads, but still being able to expose the Response
64
+ * as soon as it is available. This is implmented this way to avoid creating
65
+ * unnecessary closures, which is important here because we can have thousands
66
+ * of tile requests.
67
+ *
68
+ * @param {object} opts
69
+ * @param {string} opts.url
70
+ * @param {(response: DownloadResponse) => void} opts.onresponse
71
+ * @param {(err: Error) => void} opts.onerror
72
+ * @param {import('./streams.js').ProgressCallback} [opts.onprogress]
73
+ * @returns {Promise<void>}
74
+ */
75
+ async function fetchStream({ url, onresponse, onerror, onprogress }) {
76
+ try {
77
+ const response = await ky(url, { retry: 3 })
78
+ if (!response.body) {
79
+ throw new Error('No body in response')
80
+ }
81
+ const contentType = response.headers.get('content-type')
82
+ const mimeType =
83
+ typeof contentType === 'string' ? contentType.split(';')[0] : null
84
+ const contentLengthHeader = response.headers.get('content-length')
85
+ const contentLength =
86
+ contentLengthHeader === null ? null : parseInt(contentLengthHeader, 10)
87
+
88
+ const passthrough = new TransformStream()
89
+ // pipeTo resolves when the body is fully consumed (respects backpressure),
90
+ // which releases the pLimit slot for the next download
91
+ const pipePromise = response.body.pipeTo(passthrough.writable)
92
+
93
+ let body = passthrough.readable
94
+ if (onprogress) {
95
+ const progress = new ProgressStream({ onprogress })
96
+ body = body.pipeThrough(progress)
97
+ }
98
+
99
+ onresponse({ body, mimeType, contentLength })
100
+ await pipePromise
101
+ } catch (err) {
102
+ onerror(err instanceof Error ? err : new Error('Unknown error'))
103
+ }
104
+ }
@@ -0,0 +1,92 @@
1
+ import { hasOwn } from './misc.js'
2
+
3
+ const MAGIC_BYTES = /** @type {const} */ ({
4
+ png: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
5
+ jpg: [0xff, 0xd8, 0xff],
6
+ // eslint-disable-next-line no-sparse-arrays
7
+ webp: [0x52, 0x49, 0x46, 0x46, , , , , 0x57, 0x45, 0x42, 0x50],
8
+ // Include the compression-type byte, which is always 0x08 (DEFLATE) for gzip
9
+ gz: [0x1f, 0x8b, 0x08],
10
+ })
11
+
12
+ const MIME_TYPES = /** @type {const} */ ({
13
+ 'image/png': 'png',
14
+ 'image/jpeg': 'jpg',
15
+ 'image/webp': 'webp',
16
+ })
17
+
18
+ /** @type {Map<number, keyof typeof MAGIC_BYTES>} */
19
+ const magicByteMap = new Map()
20
+ for (const [ext, bytes] of Object.entries(MAGIC_BYTES)) {
21
+ magicByteMap.set(
22
+ bytes[0],
23
+ // @ts-ignore
24
+ ext,
25
+ )
26
+ }
27
+ /**
28
+ * For a given buffer, determine the tile format based on the magic bytes.
29
+ * Will throw for unknown file types.
30
+ * Smaller and faster version of magic-bytes.js due to the limited use case.
31
+ *
32
+ * @param {Buffer | Uint8Array} buf
33
+ * @returns {import("../writer.js").TileFormat}
34
+ */
35
+ export function getTileFormatFromBuffer(buf) {
36
+ const ext = magicByteMap.get(buf[0])
37
+ if (!ext) {
38
+ throw new Error('Unknown file type')
39
+ }
40
+ const sig = MAGIC_BYTES[ext]
41
+ for (let i = 1; i < sig.length; i++) {
42
+ if (typeof sig[i] !== 'undefined' && sig[i] !== buf[i]) {
43
+ throw new Error('Unknown file type')
44
+ }
45
+ }
46
+ if (ext === 'gz') {
47
+ // Gzipped tiles are always MVT
48
+ return 'mvt'
49
+ }
50
+ return ext
51
+ }
52
+
53
+ /**
54
+ * Determine the tile format from a readable stream from the magic bytes at the
55
+ * start of the file. Used if data is served without a content-type header.
56
+ * Returns the format and a new readable stream that includes all original data.
57
+ *
58
+ * @param {ReadableStream<Uint8Array>} tileData Web ReadableStream
59
+ * @returns {Promise<[import("../writer.js").TileFormat, ReadableStream<Uint8Array>]>}
60
+ */
61
+ export async function getTileFormatFromStream(tileData) {
62
+ const [stream1, stream2] = tileData.tee()
63
+
64
+ // Read enough bytes to detect the format from the first branch
65
+ // 12 bytes is enough for all magic byte signatures (WEBP needs 12)
66
+ const reader = stream1.getReader()
67
+ let buffer = new Uint8Array(0)
68
+ while (buffer.length < 12) {
69
+ const { done, value } = await reader.read()
70
+ if (done) break
71
+ const newBuffer = new Uint8Array(buffer.length + value.length)
72
+ newBuffer.set(buffer)
73
+ newBuffer.set(value, buffer.length)
74
+ buffer = newBuffer
75
+ }
76
+ reader.cancel()
77
+
78
+ const format = getTileFormatFromBuffer(buffer)
79
+ return [format, stream2]
80
+ }
81
+
82
+ /**
83
+ * Get the tile format from a MIME type. Throws for unsupported types.
84
+ *
85
+ * @param {string} mimeType
86
+ * @returns {import("../writer.js").TileFormat}
87
+ */
88
+ export function getFormatFromMimeType(mimeType) {
89
+ if (mimeType.startsWith('application/')) return 'mvt'
90
+ if (hasOwn(MIME_TYPES, mimeType)) return MIME_TYPES[mimeType]
91
+ throw new Error('Unsupported MIME type ' + mimeType)
92
+ }
@@ -0,0 +1,97 @@
1
+ // Adapted from https://github.com/mapbox/tilebelt
2
+
3
+ const r2d = 180 / Math.PI
4
+
5
+ /** Spherical Mercator max bounds, rounded to 6 decimal places */
6
+ export const MAX_BOUNDS = /** @type {BBox} */ ([
7
+ -180, -85.051129, 180, 85.051129,
8
+ ])
9
+
10
+ /**
11
+ * @typedef {[number, number, number, number]} BBox
12
+ */
13
+
14
+ /**
15
+ * Return the bounding box for the given tile.
16
+ *
17
+ * @param {{ x: number, y: number, z: number }} tile
18
+ * @returns {BBox} Bounding Box [w, s, e, n]
19
+ */
20
+ export function tileToBBox({ x, y, z }) {
21
+ const e = tile2lon({ x: x + 1, z })
22
+ const w = tile2lon({ x, z })
23
+ const s = tile2lat({ y: y + 1, z })
24
+ const n = tile2lat({ y, z })
25
+ return [w, s, e, n]
26
+ }
27
+
28
+ /**
29
+ * @param {{ x: number, y: number, z: number }} tile
30
+ */
31
+ export function getQuadkey({ x, y, z }) {
32
+ let quadkey = ''
33
+ let mask
34
+ for (let i = z; i > 0; i--) {
35
+ mask = 1 << (i - 1)
36
+ quadkey += (x & mask ? 1 : 0) + (y & mask ? 2 : 0)
37
+ }
38
+ return quadkey
39
+ }
40
+
41
+ /**
42
+ * From an array of tile URL templates, get the URL for the given tile.
43
+ *
44
+ * @param {string[]} urls
45
+ * @param {{ x: number, y: number, z: number, scheme?: 'xyz' | 'tms' }} opts
46
+ */
47
+ export function getTileUrl(urls, { x, y, z, scheme = 'xyz' }) {
48
+ const bboxEspg3857 = tileToBBox({ x, y: Math.pow(2, z) - y - 1, z })
49
+ const quadkey = getQuadkey({ x, y, z })
50
+
51
+ return urls[(x + y) % urls.length]
52
+ .replace('{prefix}', (x % 16).toString(16) + (y % 16).toString(16))
53
+ .replace(/{z}/g, String(z))
54
+ .replace(/{x}/g, String(x))
55
+ .replace(/{y}/g, String(scheme === 'tms' ? Math.pow(2, z) - y - 1 : y))
56
+ .replace('{quadkey}', quadkey)
57
+ .replace('{bbox-epsg-3857}', bboxEspg3857.join(','))
58
+ }
59
+
60
+ /**
61
+ * Returns a bbox that is the smallest bounding box that contains all the input bboxes.
62
+ *
63
+ * @param {[Readonly<BBox>, ...Readonly<BBox>[]]} bboxes
64
+ * @returns {BBox} Bounding Box [w, s, e, n]
65
+ */
66
+ export function unionBBox(bboxes) {
67
+ let [w, s, e, n] = bboxes[0]
68
+ for (let i = 1; i < bboxes.length; i++) {
69
+ const [w1, s1, e1, n1] = bboxes[i]
70
+ w = Math.min(w, w1)
71
+ s = Math.min(s, s1)
72
+ e = Math.max(e, e1)
73
+ n = Math.max(n, n1)
74
+ }
75
+ return [w, s, e, n]
76
+ }
77
+
78
+ /**
79
+ * Convert a TMS Y coordinate to an XYZ Y coordinate.
80
+ *
81
+ * @param {{ y: number, z: number }} tile
82
+ * @returns {number} The XYZ Y coordinate
83
+ */
84
+ export function tmsToXyzY({ y, z }) {
85
+ return Math.pow(2, z) - y - 1
86
+ }
87
+
88
+ /** @param {{ x: number, z: number }} opts */
89
+ function tile2lon({ x, z }) {
90
+ return (x / Math.pow(2, z)) * 360 - 180
91
+ }
92
+
93
+ /** @param {{ y: number, z: number }} opts */
94
+ function tile2lat({ y, z }) {
95
+ const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z)
96
+ return r2d * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
97
+ }
@@ -0,0 +1,155 @@
1
+ 'use strict'
2
+ // from https://github.com/mapbox/mapbox-gl-js/blob/495a695/src/util/mapbox.js
3
+
4
+ export const API_URL = 'https://api.mapbox.com'
5
+ const HELP = 'See https://www.mapbox.com/api-documentation/#access-tokens'
6
+
7
+ /**
8
+ * @typedef {object} URLObject
9
+ * @property {string} protocol
10
+ * @property {string} authority
11
+ * @property {string} path
12
+ * @property {string[]} params
13
+ */
14
+
15
+ /**
16
+ * @param {URLObject} urlObject
17
+ * @param {string} [accessToken]
18
+ */
19
+ function makeAPIURL(urlObject, accessToken) {
20
+ const apiUrlObject = parseUrl(API_URL)
21
+ urlObject.protocol = apiUrlObject.protocol
22
+ urlObject.authority = apiUrlObject.authority
23
+
24
+ if (!accessToken) {
25
+ throw new Error(
26
+ `An API access token is required to use a Mapbox style. ${HELP}`,
27
+ )
28
+ }
29
+ if (accessToken[0] === 's') {
30
+ throw new Error(
31
+ `Use a public access token (pk.*) not a secret access token (sk.*). ${HELP}`,
32
+ )
33
+ }
34
+
35
+ urlObject.params.push(`access_token=${accessToken}`)
36
+ return formatUrl(urlObject)
37
+ }
38
+
39
+ /** @param {string} url */
40
+ export function isMapboxURL(url) {
41
+ return url.indexOf('mapbox:') === 0
42
+ }
43
+
44
+ /**
45
+ * @param {string} url
46
+ * @param {string} [accessToken]
47
+ */
48
+ export function normalizeStyleURL(url, accessToken) {
49
+ if (!isMapboxURL(url)) return url
50
+ if (!accessToken) throw new Error('Mapbox styles require an access token')
51
+ const urlObject = parseUrl(url)
52
+ urlObject.path = `/styles/v1${urlObject.path}`
53
+ return makeAPIURL(urlObject, accessToken)
54
+ }
55
+
56
+ /**
57
+ * @param {string} url
58
+ * @param {string} [accessToken]
59
+ */
60
+ export function normalizeGlyphsURL(url, accessToken) {
61
+ if (!isMapboxURL(url)) return url
62
+ if (!accessToken) throw new Error('Mapbox styles require an access token')
63
+ const urlObject = parseUrl(url)
64
+ urlObject.path = `/fonts/v1${urlObject.path}`
65
+ return makeAPIURL(urlObject, accessToken)
66
+ }
67
+
68
+ /**
69
+ * @param {string} url
70
+ * @param {string} [accessToken]
71
+ */
72
+ export function normalizeSourceURL(url, accessToken) {
73
+ if (!isMapboxURL(url)) return url
74
+ if (!accessToken) throw new Error('Mapbox styles require an access token')
75
+ const urlObject = parseUrl(url)
76
+ urlObject.path = `/v4/${urlObject.authority}.json`
77
+ // TileJSON requests need a secure flag appended to their URLs so
78
+ // that the server knows to send SSL-ified resource references.
79
+ urlObject.params.push('secure')
80
+ return makeAPIURL(urlObject, accessToken)
81
+ }
82
+
83
+ /**
84
+ * @param {string} url
85
+ * @param {'' | '@2x'} format
86
+ * @param {'.png' | '.json'} extension
87
+ * @param {string} [accessToken]
88
+ */
89
+ export function normalizeSpriteURL(url, format, extension, accessToken) {
90
+ const urlObject = parseUrl(url)
91
+ if (!isMapboxURL(url)) {
92
+ urlObject.path += `${format}${extension}`
93
+ return formatUrl(urlObject)
94
+ }
95
+ urlObject.path = `/styles/v1${urlObject.path}/sprite${format}${extension}`
96
+ return makeAPIURL(urlObject, accessToken)
97
+ }
98
+
99
+ const imageExtensionRe = /(\.(png|jpg)\d*)(?=$)/
100
+
101
+ /**
102
+ * @param {any} tileURL
103
+ * @param {string} sourceURL
104
+ * @param {256 | 512} [tileSize]
105
+ * @param {{ devicePixelRatio?: number; supportsWebp?: boolean; }} [opts]
106
+ */
107
+ export function normalizeTileURL(
108
+ tileURL,
109
+ sourceURL,
110
+ tileSize,
111
+ { devicePixelRatio = 1, supportsWebp = false } = {},
112
+ ) {
113
+ if (!sourceURL || !isMapboxURL(sourceURL)) return tileURL
114
+
115
+ const urlObject = parseUrl(tileURL)
116
+
117
+ // The v4 mapbox tile API supports 512x512 image tiles only when @2x
118
+ // is appended to the tile URL. If `tileSize: 512` is specified for
119
+ // a Mapbox raster source force the @2x suffix even if a non hidpi device.
120
+ const suffix = devicePixelRatio >= 2 || tileSize === 512 ? '@2x' : ''
121
+ const extension = supportsWebp ? '.webp' : '$1'
122
+ urlObject.path = urlObject.path.replace(
123
+ imageExtensionRe,
124
+ `${suffix}${extension}`,
125
+ )
126
+
127
+ return formatUrl(urlObject)
128
+ }
129
+
130
+ const urlRe = /^(\w+):\/\/([^/?]*)(\/[^?]+)?\??(.+)?/
131
+
132
+ /**
133
+ * @param {string} url
134
+ * @returns {URLObject}
135
+ */
136
+ function parseUrl(url) {
137
+ const parts = url.match(urlRe)
138
+ if (!parts) {
139
+ throw new Error('Unable to parse URL object')
140
+ }
141
+ return {
142
+ protocol: parts[1],
143
+ authority: parts[2],
144
+ path: parts[3] || '/',
145
+ params: parts[4] ? parts[4].split('&') : [],
146
+ }
147
+ }
148
+
149
+ /**
150
+ * @param {URLObject} obj
151
+ */
152
+ function formatUrl(obj) {
153
+ const params = obj.params.length ? `?${obj.params.join('&')}` : ''
154
+ return `${obj.protocol}://${obj.authority}${obj.path}${params}`
155
+ }
@@ -7,8 +7,12 @@
7
7
  * @param {T} obj
8
8
  * @returns {T}
9
9
  */
10
- declare function clone<T>(obj: T): T;
11
- declare function noop(): void;
10
+ export function clone(obj) {
11
+ return JSON.parse(JSON.stringify(obj))
12
+ }
13
+
14
+ export function noop() {}
15
+
12
16
  /**
13
17
  * Like `Object.hasOwn`, but refines the type of `key`.
14
18
  *
@@ -17,6 +21,6 @@ declare function noop(): void;
17
21
  * @param {string} key
18
22
  * @returns {key is (keyof T)}
19
23
  */
20
- declare function hasOwn<T extends Record<string, unknown>>(obj: T, key: string): key is (keyof T);
21
-
22
- export { clone, hasOwn, noop };
24
+ export function hasOwn(obj, key) {
25
+ return Object.hasOwn(obj, key)
26
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Create a ReadableStream from an async iterable. Uses the native
3
+ * `ReadableStream.from()` when available (Node 20+), otherwise falls back to a
4
+ * manual approach for Node 18 compatibility.
5
+ *
6
+ * @template T
7
+ * @param {AsyncIterable<T>} iterable
8
+ * @returns {ReadableStream<T>}
9
+ */
10
+ export function readableFromAsync(iterable) {
11
+ // @ts-ignore - ReadableStream.from() exists in Node 20+ but not in DOM types
12
+ if (typeof ReadableStream.from === 'function') {
13
+ // @ts-ignore
14
+ return ReadableStream.from(iterable)
15
+ }
16
+ const iterator = iterable[Symbol.asyncIterator]()
17
+ return new ReadableStream({
18
+ async pull(controller) {
19
+ const { value, done } = await iterator.next()
20
+ if (done) {
21
+ controller.close()
22
+ } else {
23
+ controller.enqueue(value)
24
+ }
25
+ },
26
+ async cancel(reason) {
27
+ await iterator.return?.(reason)
28
+ },
29
+ })
30
+ }
31
+
32
+ /**
33
+ * Create a writable stream from an async function. Default concurrency is 16 -
34
+ * this is the number of parallel functions that will be pending before
35
+ * backpressure is applied on the stream.
36
+ *
37
+ * @template {(...args: any[]) => Promise<void>} T
38
+ * @param {T} fn
39
+ * @returns {WritableStream}
40
+ */
41
+ export function writeStreamFromAsync(fn, { concurrency = 16 } = {}) {
42
+ const pending = new Set()
43
+ return new WritableStream(
44
+ {
45
+ write(chunk) {
46
+ const p = fn(...chunk)
47
+ pending.add(p)
48
+ p.finally(() => pending.delete(p))
49
+ if (pending.size >= concurrency) {
50
+ return Promise.race(pending)
51
+ }
52
+ },
53
+ async close() {
54
+ await Promise.all(pending)
55
+ },
56
+ },
57
+ new CountQueuingStrategy({ highWaterMark: concurrency }),
58
+ )
59
+ }
60
+
61
+ /** @typedef {(opts: { totalBytes: number, chunkBytes: number }) => void} ProgressCallback */
62
+
63
+ /**
64
+ * A web TransformStream that counts the bytes passing through it. Pass an
65
+ * optional `onprogress` callback that will be called with the accumulated
66
+ * total byte count and the chunk byte count after each chunk.
67
+ */
68
+ export class ProgressStream {
69
+ #byteLength = 0
70
+ #ts
71
+
72
+ /**
73
+ * @param {{ onprogress?: ProgressCallback }} [opts]
74
+ */
75
+ constructor({ onprogress } = {}) {
76
+ const self = this
77
+ this.#ts = new TransformStream({
78
+ transform(chunk, controller) {
79
+ self.#byteLength += chunk.byteLength
80
+ onprogress?.({
81
+ totalBytes: self.#byteLength,
82
+ chunkBytes: chunk.byteLength,
83
+ })
84
+ controller.enqueue(chunk)
85
+ },
86
+ })
87
+ }
88
+
89
+ get readable() {
90
+ return this.#ts.readable
91
+ }
92
+
93
+ get writable() {
94
+ return this.#ts.writable
95
+ }
96
+
97
+ /** Total bytes that have passed through this stream */
98
+ get byteLength() {
99
+ return this.#byteLength
100
+ }
101
+ }