@backblaze-labs/b2-sdk 0.2.0 → 0.3.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 (460) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +20 -2
  3. package/dist/_virtual/_b2-sdk-version-json.cjs +1 -1
  4. package/dist/_virtual/_b2-sdk-version-json.js +1 -1
  5. package/dist/auth/file.cjs.map +1 -1
  6. package/dist/backup/client.cjs +206 -0
  7. package/dist/backup/client.cjs.map +1 -0
  8. package/dist/backup/client.d.cts +238 -0
  9. package/dist/backup/client.d.cts.map +1 -0
  10. package/dist/backup/client.d.ts +238 -0
  11. package/dist/backup/client.d.ts.map +1 -0
  12. package/dist/backup/client.js +206 -0
  13. package/dist/backup/client.js.map +1 -0
  14. package/dist/backup/index.cjs +15 -0
  15. package/dist/backup/index.d.cts +22 -0
  16. package/dist/backup/index.d.cts.map +1 -0
  17. package/dist/backup/index.d.ts +22 -0
  18. package/dist/backup/index.d.ts.map +1 -0
  19. package/dist/backup/index.js +7 -0
  20. package/dist/backup/raw.cjs +146 -0
  21. package/dist/backup/raw.cjs.map +1 -0
  22. package/dist/backup/raw.d.cts +122 -0
  23. package/dist/backup/raw.d.cts.map +1 -0
  24. package/dist/backup/raw.d.ts +122 -0
  25. package/dist/backup/raw.d.ts.map +1 -0
  26. package/dist/backup/raw.js +146 -0
  27. package/dist/backup/raw.js.map +1 -0
  28. package/dist/bucket.cjs +2 -1
  29. package/dist/bucket.cjs.map +1 -1
  30. package/dist/bucket.js +2 -1
  31. package/dist/bucket.js.map +1 -1
  32. package/dist/copy/large.cjs +5 -3
  33. package/dist/copy/large.cjs.map +1 -1
  34. package/dist/copy/large.d.cts.map +1 -1
  35. package/dist/copy/large.d.ts.map +1 -1
  36. package/dist/copy/large.js +5 -3
  37. package/dist/copy/large.js.map +1 -1
  38. package/dist/download/checksum.cjs +6 -2
  39. package/dist/download/checksum.cjs.map +1 -1
  40. package/dist/download/checksum.js +6 -2
  41. package/dist/download/checksum.js.map +1 -1
  42. package/dist/download/parallel.cjs +52 -15
  43. package/dist/download/parallel.cjs.map +1 -1
  44. package/dist/download/parallel.d.cts +8 -1
  45. package/dist/download/parallel.d.cts.map +1 -1
  46. package/dist/download/parallel.d.ts +8 -1
  47. package/dist/download/parallel.d.ts.map +1 -1
  48. package/dist/download/parallel.js +52 -15
  49. package/dist/download/parallel.js.map +1 -1
  50. package/dist/download/single.cjs +141 -6
  51. package/dist/download/single.cjs.map +1 -1
  52. package/dist/download/single.d.cts.map +1 -1
  53. package/dist/download/single.d.ts.map +1 -1
  54. package/dist/download/single.js +141 -6
  55. package/dist/download/single.js.map +1 -1
  56. package/dist/errors/index.cjs +173 -3
  57. package/dist/errors/index.cjs.map +1 -1
  58. package/dist/errors/index.d.cts +119 -0
  59. package/dist/errors/index.d.cts.map +1 -1
  60. package/dist/errors/index.d.ts +119 -0
  61. package/dist/errors/index.d.ts.map +1 -1
  62. package/dist/errors/index.js +161 -4
  63. package/dist/errors/index.js.map +1 -1
  64. package/dist/http/retry.cjs +1 -1
  65. package/dist/http/retry.cjs.map +1 -1
  66. package/dist/http/retry.js +1 -1
  67. package/dist/http/retry.js.map +1 -1
  68. package/dist/http/transport.cjs +61 -23
  69. package/dist/http/transport.cjs.map +1 -1
  70. package/dist/http/transport.d.cts +25 -3
  71. package/dist/http/transport.d.cts.map +1 -1
  72. package/dist/http/transport.d.ts +25 -3
  73. package/dist/http/transport.d.ts.map +1 -1
  74. package/dist/http/transport.js +59 -22
  75. package/dist/http/transport.js.map +1 -1
  76. package/dist/index.cjs +25 -2
  77. package/dist/index.d.cts +6 -4
  78. package/dist/index.d.cts.map +1 -1
  79. package/dist/index.d.ts +6 -4
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +8 -6
  82. package/dist/object.cjs +1 -1
  83. package/dist/object.cjs.map +1 -1
  84. package/dist/object.d.cts +4 -2
  85. package/dist/object.d.cts.map +1 -1
  86. package/dist/object.d.ts +4 -2
  87. package/dist/object.d.ts.map +1 -1
  88. package/dist/object.js +1 -1
  89. package/dist/object.js.map +1 -1
  90. package/dist/partner/account-info.d.cts +43 -0
  91. package/dist/partner/account-info.d.cts.map +1 -0
  92. package/dist/partner/account-info.d.ts +43 -0
  93. package/dist/partner/account-info.d.ts.map +1 -0
  94. package/dist/partner/auth-clone.cjs +64 -0
  95. package/dist/partner/auth-clone.cjs.map +1 -0
  96. package/dist/partner/auth-clone.d.cts +27 -0
  97. package/dist/partner/auth-clone.d.cts.map +1 -0
  98. package/dist/partner/auth-clone.d.ts +27 -0
  99. package/dist/partner/auth-clone.d.ts.map +1 -0
  100. package/dist/partner/auth-clone.js +63 -0
  101. package/dist/partner/auth-clone.js.map +1 -0
  102. package/dist/partner/auth-core.cjs +175 -0
  103. package/dist/partner/auth-core.cjs.map +1 -0
  104. package/dist/partner/auth-core.d.cts +111 -0
  105. package/dist/partner/auth-core.d.cts.map +1 -0
  106. package/dist/partner/auth-core.d.ts +111 -0
  107. package/dist/partner/auth-core.d.ts.map +1 -0
  108. package/dist/partner/auth-core.js +175 -0
  109. package/dist/partner/auth-core.js.map +1 -0
  110. package/dist/partner/auth-shape.cjs +36 -0
  111. package/dist/partner/auth-shape.cjs.map +1 -0
  112. package/dist/partner/auth-shape.d.cts +12 -0
  113. package/dist/partner/auth-shape.d.cts.map +1 -0
  114. package/dist/partner/auth-shape.d.ts +12 -0
  115. package/dist/partner/auth-shape.d.ts.map +1 -0
  116. package/dist/partner/auth-shape.js +36 -0
  117. package/dist/partner/auth-shape.js.map +1 -0
  118. package/dist/partner/client.cjs +255 -0
  119. package/dist/partner/client.cjs.map +1 -0
  120. package/dist/partner/client.d.cts +335 -0
  121. package/dist/partner/client.d.cts.map +1 -0
  122. package/dist/partner/client.d.ts +335 -0
  123. package/dist/partner/client.d.ts.map +1 -0
  124. package/dist/partner/client.js +255 -0
  125. package/dist/partner/client.js.map +1 -0
  126. package/dist/partner/endpoint-url.cjs +108 -0
  127. package/dist/partner/endpoint-url.cjs.map +1 -0
  128. package/dist/partner/endpoint-url.d.cts +72 -0
  129. package/dist/partner/endpoint-url.d.cts.map +1 -0
  130. package/dist/partner/endpoint-url.d.ts +72 -0
  131. package/dist/partner/endpoint-url.d.ts.map +1 -0
  132. package/dist/partner/endpoint-url.js +105 -0
  133. package/dist/partner/endpoint-url.js.map +1 -0
  134. package/dist/partner/in-memory.cjs +135 -0
  135. package/dist/partner/in-memory.cjs.map +1 -0
  136. package/dist/partner/in-memory.d.cts +99 -0
  137. package/dist/partner/in-memory.d.cts.map +1 -0
  138. package/dist/partner/in-memory.d.ts +99 -0
  139. package/dist/partner/in-memory.d.ts.map +1 -0
  140. package/dist/partner/in-memory.js +135 -0
  141. package/dist/partner/in-memory.js.map +1 -0
  142. package/dist/partner/index.cjs +18 -0
  143. package/dist/partner/index.d.cts +23 -0
  144. package/dist/partner/index.d.cts.map +1 -0
  145. package/dist/partner/index.d.ts +23 -0
  146. package/dist/partner/index.d.ts.map +1 -0
  147. package/dist/partner/index.js +7 -0
  148. package/dist/partner/raw.cjs +392 -0
  149. package/dist/partner/raw.cjs.map +1 -0
  150. package/dist/partner/raw.d.cts +223 -0
  151. package/dist/partner/raw.d.cts.map +1 -0
  152. package/dist/partner/raw.d.ts +223 -0
  153. package/dist/partner/raw.d.ts.map +1 -0
  154. package/dist/partner/raw.js +390 -0
  155. package/dist/partner/raw.js.map +1 -0
  156. package/dist/partner/redaction-placeholders.cjs +10 -0
  157. package/dist/partner/redaction-placeholders.cjs.map +1 -0
  158. package/dist/partner/redaction-placeholders.d.cts +5 -0
  159. package/dist/partner/redaction-placeholders.d.cts.map +1 -0
  160. package/dist/partner/redaction-placeholders.d.ts +5 -0
  161. package/dist/partner/redaction-placeholders.d.ts.map +1 -0
  162. package/dist/partner/redaction-placeholders.js +9 -0
  163. package/dist/partner/redaction-placeholders.js.map +1 -0
  164. package/dist/partner/redaction.cjs +267 -0
  165. package/dist/partner/redaction.cjs.map +1 -0
  166. package/dist/partner/redaction.d.cts +124 -0
  167. package/dist/partner/redaction.d.cts.map +1 -0
  168. package/dist/partner/redaction.d.ts +124 -0
  169. package/dist/partner/redaction.d.ts.map +1 -0
  170. package/dist/partner/redaction.js +257 -0
  171. package/dist/partner/redaction.js.map +1 -0
  172. package/dist/raw/index.cjs +81 -19
  173. package/dist/raw/index.cjs.map +1 -1
  174. package/dist/raw/index.d.cts +6 -2
  175. package/dist/raw/index.d.cts.map +1 -1
  176. package/dist/raw/index.d.ts +6 -2
  177. package/dist/raw/index.d.ts.map +1 -1
  178. package/dist/raw/index.js +81 -19
  179. package/dist/raw/index.js.map +1 -1
  180. package/dist/raw/url.cjs +111 -0
  181. package/dist/raw/url.cjs.map +1 -0
  182. package/dist/raw/url.d.cts +41 -0
  183. package/dist/raw/url.d.cts.map +1 -0
  184. package/dist/raw/url.d.ts +41 -0
  185. package/dist/raw/url.d.ts.map +1 -0
  186. package/dist/raw/url.js +110 -0
  187. package/dist/raw/url.js.map +1 -0
  188. package/dist/s3/sigv4.cjs +15 -10
  189. package/dist/s3/sigv4.cjs.map +1 -1
  190. package/dist/s3/sigv4.js +15 -10
  191. package/dist/s3/sigv4.js.map +1 -1
  192. package/dist/simulator/index.cjs +1197 -206
  193. package/dist/simulator/index.cjs.map +1 -1
  194. package/dist/simulator/index.d.cts +109 -11
  195. package/dist/simulator/index.d.cts.map +1 -1
  196. package/dist/simulator/index.d.ts +109 -11
  197. package/dist/simulator/index.d.ts.map +1 -1
  198. package/dist/simulator/index.js +1196 -209
  199. package/dist/simulator/index.js.map +1 -1
  200. package/dist/simulator/partner.cjs +719 -0
  201. package/dist/simulator/partner.cjs.map +1 -0
  202. package/dist/simulator/partner.d.cts +183 -0
  203. package/dist/simulator/partner.d.cts.map +1 -0
  204. package/dist/simulator/partner.d.ts +183 -0
  205. package/dist/simulator/partner.d.ts.map +1 -0
  206. package/dist/simulator/partner.js +718 -0
  207. package/dist/simulator/partner.js.map +1 -0
  208. package/dist/simulator/validation.cjs +577 -0
  209. package/dist/simulator/validation.cjs.map +1 -1
  210. package/dist/simulator/validation.d.cts +117 -0
  211. package/dist/simulator/validation.d.cts.map +1 -1
  212. package/dist/simulator/validation.d.ts +117 -0
  213. package/dist/simulator/validation.d.ts.map +1 -1
  214. package/dist/simulator/validation.js +563 -1
  215. package/dist/simulator/validation.js.map +1 -1
  216. package/dist/streams/file-source.cjs +1 -1
  217. package/dist/streams/file-source.cjs.map +1 -1
  218. package/dist/streams/file-source.js +1 -1
  219. package/dist/streams/file-source.js.map +1 -1
  220. package/dist/streams/index.cjs +1 -1
  221. package/dist/streams/index.js +1 -1
  222. package/dist/streams/source.cjs +2 -1
  223. package/dist/streams/source.cjs.map +1 -1
  224. package/dist/streams/source.js +2 -1
  225. package/dist/streams/source.js.map +1 -1
  226. package/dist/sync/b2-sha1-reader.cjs +6 -2
  227. package/dist/sync/b2-sha1-reader.cjs.map +1 -1
  228. package/dist/sync/b2-sha1-reader.d.cts.map +1 -1
  229. package/dist/sync/b2-sha1-reader.d.ts.map +1 -1
  230. package/dist/sync/b2-sha1-reader.js +6 -2
  231. package/dist/sync/b2-sha1-reader.js.map +1 -1
  232. package/dist/sync/download-staging.cjs +11 -10
  233. package/dist/sync/download-staging.cjs.map +1 -1
  234. package/dist/sync/download-staging.d.cts +2 -1
  235. package/dist/sync/download-staging.d.cts.map +1 -1
  236. package/dist/sync/download-staging.d.ts +2 -1
  237. package/dist/sync/download-staging.d.ts.map +1 -1
  238. package/dist/sync/download-staging.js +11 -10
  239. package/dist/sync/download-staging.js.map +1 -1
  240. package/dist/sync/filters.cjs.map +1 -1
  241. package/dist/sync/filters.js.map +1 -1
  242. package/dist/sync/local-file-io.cjs +11 -6
  243. package/dist/sync/local-file-io.cjs.map +1 -1
  244. package/dist/sync/local-file-io.d.cts +1 -0
  245. package/dist/sync/local-file-io.d.cts.map +1 -1
  246. package/dist/sync/local-file-io.d.ts +1 -0
  247. package/dist/sync/local-file-io.d.ts.map +1 -1
  248. package/dist/sync/local-file-io.js +11 -6
  249. package/dist/sync/local-file-io.js.map +1 -1
  250. package/dist/sync/local-sha1.cjs +6 -15
  251. package/dist/sync/local-sha1.cjs.map +1 -1
  252. package/dist/sync/local-sha1.d.cts +0 -8
  253. package/dist/sync/local-sha1.d.cts.map +1 -1
  254. package/dist/sync/local-sha1.d.ts +0 -8
  255. package/dist/sync/local-sha1.d.ts.map +1 -1
  256. package/dist/sync/local-sha1.js +7 -15
  257. package/dist/sync/local-sha1.js.map +1 -1
  258. package/dist/sync/policies/compare.cjs +5 -3
  259. package/dist/sync/policies/compare.cjs.map +1 -1
  260. package/dist/sync/policies/compare.d.cts.map +1 -1
  261. package/dist/sync/policies/compare.d.ts.map +1 -1
  262. package/dist/sync/policies/compare.js +6 -4
  263. package/dist/sync/policies/compare.js.map +1 -1
  264. package/dist/sync/policies/index.cjs +4 -10
  265. package/dist/sync/policies/index.cjs.map +1 -1
  266. package/dist/sync/policies/index.js +4 -10
  267. package/dist/sync/policies/index.js.map +1 -1
  268. package/dist/sync/scanners/b2.cjs +2 -2
  269. package/dist/sync/scanners/b2.cjs.map +1 -1
  270. package/dist/sync/scanners/b2.js +2 -2
  271. package/dist/sync/scanners/b2.js.map +1 -1
  272. package/dist/sync/scanners/local.cjs.map +1 -1
  273. package/dist/sync/scanners/local.js.map +1 -1
  274. package/dist/sync/synchronizer.cjs +3 -2
  275. package/dist/sync/synchronizer.cjs.map +1 -1
  276. package/dist/sync/synchronizer.js +3 -2
  277. package/dist/sync/synchronizer.js.map +1 -1
  278. package/dist/types/auth.cjs +4 -0
  279. package/dist/types/auth.cjs.map +1 -1
  280. package/dist/types/auth.d.cts +4 -0
  281. package/dist/types/auth.d.cts.map +1 -1
  282. package/dist/types/auth.d.ts +4 -0
  283. package/dist/types/auth.d.ts.map +1 -1
  284. package/dist/types/auth.js +4 -0
  285. package/dist/types/auth.js.map +1 -1
  286. package/dist/types/backup.d.cts +80 -0
  287. package/dist/types/backup.d.cts.map +1 -0
  288. package/dist/types/backup.d.ts +80 -0
  289. package/dist/types/backup.d.ts.map +1 -0
  290. package/dist/types/bucket.cjs.map +1 -1
  291. package/dist/types/bucket.d.cts +4 -2
  292. package/dist/types/bucket.d.cts.map +1 -1
  293. package/dist/types/bucket.d.ts +4 -2
  294. package/dist/types/bucket.d.ts.map +1 -1
  295. package/dist/types/bucket.js.map +1 -1
  296. package/dist/types/download.cjs +62 -0
  297. package/dist/types/download.cjs.map +1 -0
  298. package/dist/types/download.d.cts +98 -0
  299. package/dist/types/download.d.cts.map +1 -1
  300. package/dist/types/download.d.ts +98 -0
  301. package/dist/types/download.d.ts.map +1 -1
  302. package/dist/types/download.js +61 -0
  303. package/dist/types/download.js.map +1 -0
  304. package/dist/types/encryption.cjs +58 -229
  305. package/dist/types/encryption.cjs.map +1 -1
  306. package/dist/types/encryption.d.cts +32 -0
  307. package/dist/types/encryption.d.cts.map +1 -1
  308. package/dist/types/encryption.d.ts +32 -0
  309. package/dist/types/encryption.d.ts.map +1 -1
  310. package/dist/types/encryption.js +56 -229
  311. package/dist/types/encryption.js.map +1 -1
  312. package/dist/types/errors.cjs +9 -0
  313. package/dist/types/errors.cjs.map +1 -1
  314. package/dist/types/errors.d.cts +1 -1
  315. package/dist/types/errors.d.cts.map +1 -1
  316. package/dist/types/errors.d.ts +1 -1
  317. package/dist/types/errors.d.ts.map +1 -1
  318. package/dist/types/errors.js +9 -0
  319. package/dist/types/errors.js.map +1 -1
  320. package/dist/types/file.cjs.map +1 -1
  321. package/dist/types/file.d.cts +15 -2
  322. package/dist/types/file.d.cts.map +1 -1
  323. package/dist/types/file.d.ts +15 -2
  324. package/dist/types/file.d.ts.map +1 -1
  325. package/dist/types/file.js.map +1 -1
  326. package/dist/types/ids.cjs +30 -0
  327. package/dist/types/ids.cjs.map +1 -1
  328. package/dist/types/ids.d.cts +32 -0
  329. package/dist/types/ids.d.cts.map +1 -1
  330. package/dist/types/ids.d.ts +32 -0
  331. package/dist/types/ids.d.ts.map +1 -1
  332. package/dist/types/ids.js +28 -1
  333. package/dist/types/ids.js.map +1 -1
  334. package/dist/types/index.d.cts +10 -6
  335. package/dist/types/index.d.cts.map +1 -1
  336. package/dist/types/index.d.ts +10 -6
  337. package/dist/types/index.d.ts.map +1 -1
  338. package/dist/types/lock.cjs.map +1 -1
  339. package/dist/types/lock.d.cts +14 -0
  340. package/dist/types/lock.d.cts.map +1 -1
  341. package/dist/types/lock.d.ts +14 -0
  342. package/dist/types/lock.d.ts.map +1 -1
  343. package/dist/types/lock.js.map +1 -1
  344. package/dist/types/notifications.cjs +1 -1
  345. package/dist/types/notifications.cjs.map +1 -1
  346. package/dist/types/notifications.d.cts +5 -3
  347. package/dist/types/notifications.d.cts.map +1 -1
  348. package/dist/types/notifications.d.ts +5 -3
  349. package/dist/types/notifications.d.ts.map +1 -1
  350. package/dist/types/notifications.js +1 -1
  351. package/dist/types/notifications.js.map +1 -1
  352. package/dist/types/partner.cjs +32 -0
  353. package/dist/types/partner.cjs.map +1 -0
  354. package/dist/types/partner.d.cts +394 -0
  355. package/dist/types/partner.d.cts.map +1 -0
  356. package/dist/types/partner.d.ts +394 -0
  357. package/dist/types/partner.d.ts.map +1 -0
  358. package/dist/types/partner.js +29 -0
  359. package/dist/types/partner.js.map +1 -0
  360. package/dist/types/upload.d.cts +31 -18
  361. package/dist/types/upload.d.cts.map +1 -1
  362. package/dist/types/upload.d.ts +31 -18
  363. package/dist/types/upload.d.ts.map +1 -1
  364. package/dist/upload/abort-scope.cjs +0 -45
  365. package/dist/upload/abort-scope.cjs.map +1 -1
  366. package/dist/upload/abort-scope.d.cts +0 -22
  367. package/dist/upload/abort-scope.d.cts.map +1 -1
  368. package/dist/upload/abort-scope.d.ts +0 -22
  369. package/dist/upload/abort-scope.d.ts.map +1 -1
  370. package/dist/upload/abort-scope.js +1 -44
  371. package/dist/upload/abort-scope.js.map +1 -1
  372. package/dist/upload/finish.cjs +3 -8
  373. package/dist/upload/finish.cjs.map +1 -1
  374. package/dist/upload/finish.d.cts.map +1 -1
  375. package/dist/upload/finish.d.ts.map +1 -1
  376. package/dist/upload/finish.js +1 -6
  377. package/dist/upload/finish.js.map +1 -1
  378. package/dist/upload/large.cjs +5 -1
  379. package/dist/upload/large.cjs.map +1 -1
  380. package/dist/upload/large.d.cts +16 -2
  381. package/dist/upload/large.d.cts.map +1 -1
  382. package/dist/upload/large.d.ts +16 -2
  383. package/dist/upload/large.d.ts.map +1 -1
  384. package/dist/upload/large.js +5 -1
  385. package/dist/upload/large.js.map +1 -1
  386. package/dist/upload/options.cjs.map +1 -1
  387. package/dist/upload/options.d.cts +12 -0
  388. package/dist/upload/options.d.cts.map +1 -1
  389. package/dist/upload/options.d.ts +12 -0
  390. package/dist/upload/options.d.ts.map +1 -1
  391. package/dist/upload/options.js.map +1 -1
  392. package/dist/upload/resume.cjs +2 -1
  393. package/dist/upload/resume.cjs.map +1 -1
  394. package/dist/upload/resume.d.cts +3 -1
  395. package/dist/upload/resume.d.cts.map +1 -1
  396. package/dist/upload/resume.d.ts +3 -1
  397. package/dist/upload/resume.d.ts.map +1 -1
  398. package/dist/upload/resume.js +2 -1
  399. package/dist/upload/resume.js.map +1 -1
  400. package/dist/upload/retry.cjs +8 -3
  401. package/dist/upload/retry.cjs.map +1 -1
  402. package/dist/upload/retry.js +8 -3
  403. package/dist/upload/retry.js.map +1 -1
  404. package/dist/upload/single.cjs +2 -1
  405. package/dist/upload/single.cjs.map +1 -1
  406. package/dist/upload/single.d.cts +6 -0
  407. package/dist/upload/single.d.cts.map +1 -1
  408. package/dist/upload/single.d.ts +6 -0
  409. package/dist/upload/single.d.ts.map +1 -1
  410. package/dist/upload/single.js +2 -1
  411. package/dist/upload/single.js.map +1 -1
  412. package/dist/upload/stream.cjs +6 -4
  413. package/dist/upload/stream.cjs.map +1 -1
  414. package/dist/upload/stream.d.cts +6 -0
  415. package/dist/upload/stream.d.cts.map +1 -1
  416. package/dist/upload/stream.d.ts +6 -0
  417. package/dist/upload/stream.d.ts.map +1 -1
  418. package/dist/upload/stream.js +3 -1
  419. package/dist/upload/stream.js.map +1 -1
  420. package/dist/util/abort.cjs +114 -0
  421. package/dist/util/abort.cjs.map +1 -0
  422. package/dist/util/abort.d.cts +65 -0
  423. package/dist/util/abort.d.cts.map +1 -0
  424. package/dist/util/abort.d.ts +65 -0
  425. package/dist/util/abort.d.ts.map +1 -0
  426. package/dist/util/abort.js +109 -0
  427. package/dist/util/abort.js.map +1 -0
  428. package/dist/util/defaults.cjs +3 -0
  429. package/dist/util/defaults.cjs.map +1 -1
  430. package/dist/util/defaults.d.cts +2 -0
  431. package/dist/util/defaults.d.cts.map +1 -1
  432. package/dist/util/defaults.d.ts +2 -0
  433. package/dist/util/defaults.d.ts.map +1 -1
  434. package/dist/util/defaults.js +3 -1
  435. package/dist/util/defaults.js.map +1 -1
  436. package/dist/util/md5.cjs +211 -0
  437. package/dist/util/md5.cjs.map +1 -0
  438. package/dist/util/md5.d.cts +43 -0
  439. package/dist/util/md5.d.cts.map +1 -0
  440. package/dist/util/md5.d.ts +43 -0
  441. package/dist/util/md5.d.ts.map +1 -0
  442. package/dist/util/md5.js +209 -0
  443. package/dist/util/md5.js.map +1 -0
  444. package/dist/util/normalize.cjs +5 -5
  445. package/dist/util/normalize.cjs.map +1 -1
  446. package/dist/util/normalize.d.cts +7 -7
  447. package/dist/util/normalize.d.cts.map +1 -1
  448. package/dist/util/normalize.d.ts +7 -7
  449. package/dist/util/normalize.d.ts.map +1 -1
  450. package/dist/util/normalize.js +5 -5
  451. package/dist/util/normalize.js.map +1 -1
  452. package/dist/util/paginator.cjs +7 -1
  453. package/dist/util/paginator.cjs.map +1 -1
  454. package/dist/util/paginator.d.cts +3 -1
  455. package/dist/util/paginator.d.cts.map +1 -1
  456. package/dist/util/paginator.d.ts +3 -1
  457. package/dist/util/paginator.d.ts.map +1 -1
  458. package/dist/util/paginator.js +7 -1
  459. package/dist/util/paginator.js.map +1 -1
  460. package/package.json +38 -16
@@ -1 +1 @@
1
- {"version":3,"file":"checksum.js","names":[],"sources":["../../src/download/checksum.ts"],"sourcesContent":["/**\n * Client-side checksum helpers for download streams.\n *\n * B2 sends `X-Bz-Content-Sha1` on download responses when a whole-file\n * checksum is available. These helpers verify streamed bytes against that\n * header without buffering the full response in memory.\n *\n * @packageDocumentation\n */\n\nimport { ChecksumMismatchError } from '../errors/index.ts'\nimport { IncrementalSha1 } from '../streams/hash.ts'\nimport { isVerifiableSha1 } from '../util/sha1.ts'\n\n/**\n * Builds the typed error used when downloaded bytes fail SHA-1 verification.\n *\n * @param expectedSha1 - SHA-1 digest advertised by the download response.\n * @param actualSha1 - SHA-1 digest computed from the downloaded bytes.\n *\n * @returns A typed checksum mismatch error.\n */\nfunction createDownloadChecksumMismatchError(\n expectedSha1: string,\n actualSha1: string,\n): ChecksumMismatchError {\n const expected = expectedSha1.toLowerCase()\n const actual = actualSha1.toLowerCase()\n return new ChecksumMismatchError({\n status: 400,\n code: 'bad_sha1_checksum',\n message: `Downloaded content SHA-1 mismatch: expected ${expected}, got ${actual}`,\n })\n}\n\n/**\n * Throws when a computed download SHA-1 does not match the expected value.\n *\n * @param expectedSha1 - SHA-1 digest advertised by the download response.\n * @param actualSha1 - SHA-1 digest computed from the downloaded bytes.\n *\n * @throws ChecksumMismatchError when the two digests differ.\n */\nexport function assertDownloadSha1(expectedSha1: string, actualSha1: string): void {\n if (actualSha1.toLowerCase() !== expectedSha1.toLowerCase()) {\n throw createDownloadChecksumMismatchError(expectedSha1, actualSha1)\n }\n}\n\n/**\n * Throws when two range responses disagree about the expected whole-file SHA-1.\n *\n * @param expectedSha1 - The first range's verifiable SHA-1, or null when unavailable.\n * @param actualSha1 - The current range's verifiable SHA-1, or null when unavailable.\n *\n * @throws ChecksumMismatchError when the two header states differ.\n */\nexport function assertDownloadSha1HeaderAgreement(\n expectedSha1: string | null,\n actualSha1: string | null,\n): void {\n if (expectedSha1 === actualSha1) return\n const expected = formatSha1ForMessage(expectedSha1)\n const actual = formatSha1ForMessage(actualSha1)\n throw new ChecksumMismatchError({\n status: 400,\n code: 'bad_sha1_checksum',\n message: `Downloaded content SHA-1 header mismatch: expected ${expected}, got ${actual}`,\n })\n}\n\n/**\n * Wraps a download stream with whole-body SHA-1 verification.\n *\n * If B2 did not provide a verifiable whole-file SHA-1 (for example,\n * multipart-finished files report `none`), the original stream is returned.\n *\n * @param body - Download response body.\n * @param expectedSha1 - Normalized SHA-1 header value, or null when unavailable.\n *\n * @returns A stream that emits the same bytes and errors on checksum mismatch.\n */\nexport function verifyDownloadStream(\n body: ReadableStream<Uint8Array>,\n expectedSha1: string | null,\n): ReadableStream<Uint8Array> {\n if (!isVerifiableSha1(expectedSha1)) return body\n\n const sha1 = new IncrementalSha1()\n const transform = new TransformStream<Uint8Array, Uint8Array>({\n async transform(chunk, controller) {\n await sha1.update(chunk)\n controller.enqueue(chunk)\n },\n async flush() {\n assertDownloadSha1(expectedSha1, await sha1.digest())\n },\n })\n return body.pipeThrough(transform)\n}\n\nfunction formatSha1ForMessage(sha1: string | null): string {\n return sha1 === null ? 'missing or unverifiable' : sha1.toLowerCase()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,SAAS,oCACP,cACA,YACuB;CAGvB,OAAO,IAAI,sBAAsB;EAC/B,QAAQ;EACR,MAAM;EACN,SAAS,+CALM,aAAa,YAK4B,EAAS,QAJpD,WAAW,YAIiD;CAC3E,CAAC;AACH;;;;;;;;;AAUA,SAAgB,mBAAmB,cAAsB,YAA0B;CACjF,IAAI,WAAW,YAAY,MAAM,aAAa,YAAY,GACxD,MAAM,oCAAoC,cAAc,UAAU;AAEtE;;;;;;;;;AAUA,SAAgB,kCACd,cACA,YACM;CACN,IAAI,iBAAiB,YAAY;CAGjC,MAAM,IAAI,sBAAsB;EAC9B,QAAQ;EACR,MAAM;EACN,SAAS,sDALM,qBAAqB,YAK2B,EAAS,QAJ3D,qBAAqB,UAI8C;CAClF,CAAC;AACH;;;;;;;;;;;;AAaA,SAAgB,qBACd,MACA,cAC4B;CAC5B,IAAI,CAAC,iBAAiB,YAAY,GAAG,OAAO;CAE5C,MAAM,OAAO,IAAI,gBAAgB;CACjC,MAAM,YAAY,IAAI,gBAAwC;EAC5D,MAAM,UAAU,OAAO,YAAY;GACjC,MAAM,KAAK,OAAO,KAAK;GACvB,WAAW,QAAQ,KAAK;EAC1B;EACA,MAAM,QAAQ;GACZ,mBAAmB,cAAc,MAAM,KAAK,OAAO,CAAC;EACtD;CACF,CAAC;CACD,OAAO,KAAK,YAAY,SAAS;AACnC;AAEA,SAAS,qBAAqB,MAA6B;CACzD,OAAO,SAAS,OAAO,4BAA4B,KAAK,YAAY;AACtE"}
1
+ {"version":3,"file":"checksum.js","names":[],"sources":["../../src/download/checksum.ts"],"sourcesContent":["/**\n * Client-side checksum helpers for download streams.\n *\n * B2 sends `X-Bz-Content-Sha1` on download responses when a whole-file\n * checksum is available. These helpers verify streamed bytes against that\n * header without buffering the full response in memory.\n *\n * @packageDocumentation\n */\n\nimport { ChecksumMismatchError } from '../errors/index.ts'\nimport { IncrementalSha1 } from '../streams/hash.ts'\nimport { isVerifiableSha1 } from '../util/sha1.ts'\n\n/**\n * Builds the typed error used when downloaded bytes fail SHA-1 verification.\n *\n * @param expectedSha1 - SHA-1 digest advertised by the download response.\n * @param actualSha1 - SHA-1 digest computed from the downloaded bytes.\n *\n * @returns A typed checksum mismatch error.\n */\nfunction createDownloadChecksumMismatchError(\n expectedSha1: string,\n actualSha1: string,\n): ChecksumMismatchError {\n const expected = expectedSha1.toLowerCase()\n const actual = actualSha1.toLowerCase()\n return new ChecksumMismatchError({\n status: 400,\n code: 'bad_sha1_checksum',\n message: `Downloaded content SHA-1 mismatch: expected ${expected}, got ${actual}`,\n })\n}\n\n/**\n * Throws when a computed download SHA-1 does not match the expected value.\n *\n * @param expectedSha1 - SHA-1 digest advertised by the download response.\n * @param actualSha1 - SHA-1 digest computed from the downloaded bytes.\n *\n * @throws ChecksumMismatchError when the two digests differ.\n */\nexport function assertDownloadSha1(expectedSha1: string, actualSha1: string): void {\n if (actualSha1.toLowerCase() !== expectedSha1.toLowerCase()) {\n throw createDownloadChecksumMismatchError(expectedSha1, actualSha1)\n }\n}\n\n/**\n * Throws when two range responses disagree about the expected whole-file SHA-1.\n *\n * @param expectedSha1 - The first range's verifiable SHA-1, or null when unavailable.\n * @param actualSha1 - The current range's verifiable SHA-1, or null when unavailable.\n *\n * @throws ChecksumMismatchError when the two header states differ.\n */\nexport function assertDownloadSha1HeaderAgreement(\n expectedSha1: string | null,\n actualSha1: string | null,\n): void {\n if (expectedSha1 === actualSha1) return\n const expected = formatSha1ForMessage(expectedSha1)\n const actual = formatSha1ForMessage(actualSha1)\n throw new ChecksumMismatchError({\n status: 400,\n code: 'bad_sha1_checksum',\n message: `Downloaded content SHA-1 header mismatch: expected ${expected}, got ${actual}`,\n })\n}\n\n/**\n * Wraps a download stream with whole-body SHA-1 verification.\n *\n * If B2 did not provide a verifiable whole-file SHA-1 (for example,\n * multipart-finished files report `none`), the original stream is returned.\n *\n * @param body - Download response body.\n * @param expectedSha1 - Normalized SHA-1 header value, or null when unavailable.\n *\n * @returns A stream that emits the same bytes and errors on checksum mismatch.\n */\nexport function verifyDownloadStream(\n body: ReadableStream<Uint8Array>,\n expectedSha1: string | null,\n): ReadableStream<Uint8Array> {\n if (!isVerifiableSha1(expectedSha1)) return body\n\n const sha1 = new IncrementalSha1()\n const transform = new TransformStream<Uint8Array, Uint8Array>({\n async transform(chunk, controller) {\n await sha1.update(chunk)\n controller.enqueue(chunk)\n },\n async flush() {\n assertDownloadSha1(expectedSha1, await sha1.digest())\n },\n })\n return body.pipeThrough(transform)\n}\n\nfunction formatSha1ForMessage(sha1: string | null): string {\n return sha1 === null ? 'missing or unverifiable' : sha1.toLowerCase()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,SAAS,oCACP,cACA,YACuB;CACvB,MAAM,WAAW,aAAa,YAAY;CAC1C,MAAM,SAAS,WAAW,YAAY;CACtC,OAAO,IAAI,sBAAsB;EAC/B,QAAQ;EACR,MAAM;EACN,SAAS,+CAA+C,SAAS,QAAQ;CAC3E,CAAC;AACH;;;;;;;;;AAUA,SAAgB,mBAAmB,cAAsB,YAA0B;CACjF,IAAI,WAAW,YAAY,MAAM,aAAa,YAAY,GACxD,MAAM,oCAAoC,cAAc,UAAU;AAEtE;;;;;;;;;AAUA,SAAgB,kCACd,cACA,YACM;CACN,IAAI,iBAAiB,YAAY;CACjC,MAAM,WAAW,qBAAqB,YAAY;CAClD,MAAM,SAAS,qBAAqB,UAAU;CAC9C,MAAM,IAAI,sBAAsB;EAC9B,QAAQ;EACR,MAAM;EACN,SAAS,sDAAsD,SAAS,QAAQ;CAClF,CAAC;AACH;;;;;;;;;;;;AAaA,SAAgB,qBACd,MACA,cAC4B;CAC5B,IAAI,CAAC,iBAAiB,YAAY,GAAG,OAAO;CAE5C,MAAM,OAAO,IAAI,gBAAgB;CACjC,MAAM,YAAY,IAAI,gBAAwC;EAC5D,MAAM,UAAU,OAAO,YAAY;GACjC,MAAM,KAAK,OAAO,KAAK;GACvB,WAAW,QAAQ,KAAK;EAC1B;EACA,MAAM,QAAQ;GACZ,mBAAmB,cAAc,MAAM,KAAK,OAAO,CAAC;EACtD;CACF,CAAC;CACD,OAAO,KAAK,YAAY,SAAS;AACnC;AAEA,SAAS,qBAAqB,MAA6B;CACzD,OAAO,SAAS,OAAO,4BAA4B,KAAK,YAAY;AACtE"}
@@ -1,6 +1,8 @@
1
1
  const require_errors_index = require("../errors/index.cjs");
2
+ require("../util/defaults.cjs");
2
3
  const require_plan_ranges = require("../util/plan-ranges.cjs");
3
4
  const require_text_codec = require("../util/text-codec.cjs");
5
+ const require_encryption = require("../types/encryption.cjs");
4
6
  const require_normalize = require("../util/normalize.cjs");
5
7
  const require_hash = require("../streams/hash.cjs");
6
8
  const require_sha1 = require("../util/sha1.cjs");
@@ -31,14 +33,26 @@ const require_collect = require("../streams/collect.cjs");
31
33
  * @returns A `ReadableStream` that yields file bytes in order.
32
34
  */
33
35
  function createParallelDownloadStream(raw, accountInfo, options) {
34
- const rangeSize = options.rangeSize ?? 10 * 1024 * 1024;
36
+ const rangeSize = options.rangeSize ?? 10485760;
35
37
  const concurrency = options.concurrency ?? 4;
36
38
  const totalSize = options.totalSize;
39
+ const serverSideEncryption = options.serverSideEncryption !== void 0 ? require_encryption.redactSseCKeyMaterial(options.serverSideEncryption, { label: "SseCDownloadKey" }) : void 0;
37
40
  const retryOptions = {
38
41
  ...require_retry.DEFAULT_RETRY_OPTIONS,
39
42
  maxRetries: options.maxRetries ?? 0
40
43
  };
41
- const abort = options.signal;
44
+ const internalAbort = new AbortController();
45
+ const abort = internalAbort.signal;
46
+ let removeCallerAbortListener;
47
+ if (options.signal !== void 0) {
48
+ if (options.signal.aborted) internalAbort.abort(options.signal.reason);
49
+ else {
50
+ const callerSignal = options.signal;
51
+ const abortFromCaller = () => internalAbort.abort(callerSignal.reason);
52
+ callerSignal.addEventListener("abort", abortFromCaller, { once: true });
53
+ removeCallerAbortListener = () => callerSignal.removeEventListener("abort", abortFromCaller);
54
+ }
55
+ }
42
56
  const ranges = require_plan_ranges.planRanges(totalSize, rangeSize);
43
57
  const windowSize = concurrency * 2;
44
58
  const inflight = /* @__PURE__ */ new Map();
@@ -48,6 +62,13 @@ function createParallelDownloadStream(raw, accountInfo, options) {
48
62
  let nextToEmit = 0;
49
63
  let expectedSha1;
50
64
  let firstError = null;
65
+ function abortInflight(reason) {
66
+ if (!abort.aborted) internalAbort.abort(reason);
67
+ }
68
+ function cleanupCallerAbortListener() {
69
+ removeCallerAbortListener?.();
70
+ removeCallerAbortListener = void 0;
71
+ }
51
72
  function scheduleNext() {
52
73
  while (firstError === null && abort?.aborted !== true && nextToSchedule < ranges.length && inflight.size + buffer.size < windowSize) {
53
74
  const range = ranges[nextToSchedule];
@@ -56,10 +77,23 @@ function createParallelDownloadStream(raw, accountInfo, options) {
56
77
  nextToSchedule++;
57
78
  const task = (async () => {
58
79
  try {
59
- const result = await fetchRangeWithRetry(raw, accountInfo, options.fileId, range.start, range.end, totalSize, retryOptions, abort);
80
+ const result = await fetchRangeWithRetry({
81
+ raw,
82
+ accountInfo,
83
+ fileId: options.fileId,
84
+ start: range.start,
85
+ end: range.end,
86
+ totalSize,
87
+ serverSideEncryption,
88
+ retryOptions,
89
+ signal: abort
90
+ });
60
91
  buffer.set(idx, result);
61
92
  } catch (err) {
62
- if (firstError === null) firstError = err;
93
+ if (firstError === null) {
94
+ firstError = err;
95
+ abortInflight(err);
96
+ }
63
97
  } finally {
64
98
  inflight.delete(idx);
65
99
  }
@@ -73,6 +107,7 @@ function createParallelDownloadStream(raw, accountInfo, options) {
73
107
  abort?.throwIfAborted();
74
108
  scheduleNext();
75
109
  } catch (err) {
110
+ cleanupCallerAbortListener();
76
111
  controller.error(err);
77
112
  }
78
113
  },
@@ -82,6 +117,7 @@ function createParallelDownloadStream(raw, accountInfo, options) {
82
117
  abort?.throwIfAborted();
83
118
  if (firstError !== null) throw firstError;
84
119
  if (inflight.size === 0) {
120
+ cleanupCallerAbortListener();
85
121
  controller.close();
86
122
  return;
87
123
  }
@@ -103,41 +139,42 @@ function createParallelDownloadStream(raw, accountInfo, options) {
103
139
  scheduleNext();
104
140
  if (nextToEmit >= ranges.length && buffer.size === 0 && inflight.size === 0 && firstError === null) {
105
141
  if (expectedSha1 !== void 0 && expectedSha1 !== null && assembledSha1 !== null) require_checksum.assertDownloadSha1(expectedSha1, await assembledSha1.digest());
142
+ cleanupCallerAbortListener();
106
143
  controller.close();
107
144
  }
108
145
  } catch (err) {
146
+ abortInflight(err);
147
+ cleanupCallerAbortListener();
109
148
  controller.error(err);
110
149
  }
111
150
  },
112
- cancel() {
151
+ cancel(reason) {
152
+ abortInflight(reason);
153
+ cleanupCallerAbortListener();
113
154
  buffer.clear();
114
155
  }
115
156
  });
116
157
  }
117
158
  /**
118
159
  * Fetches a single byte range with bounded retry on transient failures.
119
- * @param raw - Low-level B2 API client.
120
- * @param accountInfo - Authorized account state.
121
- * @param fileId - ID of the file being downloaded.
122
- * @param start - Inclusive byte offset where the range begins.
123
- * @param end - Inclusive byte offset where the range ends.
124
- * @param totalSize - Expected complete file size.
125
- * @param retryOptions - Retry settings controlling attempts and backoff.
126
- * @param signal - Optional abort signal that cancels the range and any pending retry.
160
+ * @param params - Range request parameters, retry settings, and cancellation.
127
161
  *
128
162
  * @returns The range's bytes, or throws after exhausting all retry attempts.
129
163
  */
130
- async function fetchRangeWithRetry(raw, accountInfo, fileId, start, end, totalSize, retryOptions, signal) {
164
+ async function fetchRangeWithRetry(params) {
165
+ const { raw, accountInfo, fileId, start, end, totalSize, serverSideEncryption, retryOptions, signal } = params;
131
166
  let lastError;
132
167
  for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {
133
168
  if (attempt > 0) {
134
169
  const retryAfter = lastError instanceof require_errors_index.B2Error && lastError.retryAfter !== void 0 ? lastError.retryAfter : void 0;
135
- await require_retry.sleep(require_retry.computeBackoff(attempt - 1, retryOptions, retryAfter), signal);
170
+ const delay = require_retry.computeBackoff(attempt - 1, retryOptions, retryAfter);
171
+ await require_retry.sleep(delay, signal);
136
172
  }
137
173
  try {
138
174
  signal?.throwIfAborted();
139
175
  const resp = await raw.downloadFileById(accountInfo.getDownloadUrl(), accountInfo.getAuthToken(), fileId, {
140
176
  range: require_plan_ranges.byteRangeHeader(start, end),
177
+ ...serverSideEncryption !== void 0 ? { serverSideEncryption } : {},
141
178
  ...signal !== void 0 ? { signal } : {}
142
179
  });
143
180
  if (resp.status < 200 || resp.status >= 300) throw await classifyDownloadResponseError(resp);
@@ -1 +1 @@
1
- {"version":3,"file":"parallel.cjs","names":[],"sources":["../../src/download/parallel.ts"],"sourcesContent":["import type { AccountInfo } from '../auth/account-info.ts'\nimport { B2Error, classifyError, NetworkError } from '../errors/index.ts'\nimport { computeBackoff, DEFAULT_RETRY_OPTIONS, type RetryOptions, sleep } from '../http/retry.ts'\nimport type { RawClient } from '../raw/index.ts'\nimport { collectStream } from '../streams/collect.ts'\nimport { IncrementalSha1 } from '../streams/hash.ts'\nimport type { B2ErrorResponse } from '../types/errors.ts'\nimport type { FileId } from '../types/ids.ts'\nimport { DEFAULT_TRANSFER_CONCURRENCY } from '../util/defaults.ts'\nimport { normalizeSha1 } from '../util/normalize.ts'\nimport { byteRangeHeader, planRanges } from '../util/plan-ranges.ts'\nimport { normalizeVerifiableSha1 } from '../util/sha1.ts'\nimport { utf8Decoder } from '../util/text-codec.ts'\nimport { assertDownloadSha1, assertDownloadSha1HeaderAgreement } from './checksum.ts'\n\n/** Options for downloading a file using concurrent byte-range requests. */\nexport interface ParallelDownloadOptions {\n /** ID of the file to download. */\n readonly fileId: FileId\n /** Total file size in bytes (must be known in advance). */\n readonly totalSize: number\n /** Size of each ranged chunk in bytes. Defaults to 10 MB. */\n readonly rangeSize?: number\n /** Maximum number of chunks fetched in parallel. Defaults to 4. */\n readonly concurrency?: number\n /**\n * Extra retry attempts per range on transient failures. Defaults to 0 because\n * `B2Client` already applies `RetryTransport`; set this only when supplying a\n * raw client that does not already retry transport failures.\n */\n readonly maxRetries?: number\n /** Signal to abort the download. */\n readonly signal?: AbortSignal\n}\n\ninterface RangeDownloadResult {\n readonly data: Uint8Array\n readonly contentSha1: string | null\n}\n\n/**\n * Creates a readable stream that downloads a file using parallel byte-range requests.\n *\n * The file is split into fixed-size ranges fetched in a **sliding\n * window** keyed off the consumer's read pace. The window size is\n * `concurrency * 2`: up to `concurrency` ranges are in flight at once,\n * plus up to `concurrency` completed-but-not-yet-emitted ranges buffered\n * ahead of the read head. New fetches kick off only when the consumer\n * reads a chunk, so a slow downstream pipe (e.g. a saturated network\n * sink or a `pipeTo` consumer that drains slowly) backpressures into\n * the SDK and bounds peak memory to `(concurrency * 2) * rangeSize`.\n *\n * The previous eager implementation scheduled every range into\n * `Promise.all` up front; a slow head-of-line range could hold the\n * entire file in memory while later ranges finished and waited.\n *\n * @param raw - Low-level B2 API client.\n * @param accountInfo - Authorized account state.\n * @param options - Parallel download parameters (file ID, size, concurrency).\n *\n * @returns A `ReadableStream` that yields file bytes in order.\n */\nexport function createParallelDownloadStream(\n raw: RawClient,\n accountInfo: AccountInfo,\n options: ParallelDownloadOptions,\n): ReadableStream<Uint8Array> {\n const rangeSize = options.rangeSize ?? 10 * 1024 * 1024\n const concurrency = options.concurrency ?? DEFAULT_TRANSFER_CONCURRENCY\n const totalSize = options.totalSize\n // The high-level B2Client already wraps the raw transport in RetryTransport.\n // Keep the parallel-download outer retry disabled by default so each range\n // has one retry budget unless callers explicitly opt into an extra raw-client\n // retry layer.\n const retryOptions = {\n ...DEFAULT_RETRY_OPTIONS,\n maxRetries: options.maxRetries ?? 0,\n }\n const abort = options.signal\n\n const ranges = planRanges(totalSize, rangeSize)\n\n // `inflight` holds currently-fetching range promises; `buffer` holds\n // completed ranges waiting for their slot in the emit order. Total\n // scheduled-but-not-emitted = inflight.size + buffer.size is capped\n // at `windowSize`, so peak memory is bounded regardless of any\n // single range's latency.\n const windowSize = concurrency * 2\n const inflight = new Map<number, Promise<void>>()\n const buffer = new Map<number, RangeDownloadResult>()\n let assembledSha1: IncrementalSha1 | null = null\n let nextToSchedule = 0\n let nextToEmit = 0\n let expectedSha1: string | null | undefined\n let firstError: unknown = null\n\n function scheduleNext(): void {\n while (\n firstError === null &&\n // Honour abort here so a completed range that triggers a top-up\n // doesn't queue one final fetch after the caller aborted. Without\n // this gate, one extra range request fires post-abort before the\n // `pull()` loop notices.\n abort?.aborted !== true &&\n nextToSchedule < ranges.length &&\n inflight.size + buffer.size < windowSize\n ) {\n const range = ranges[nextToSchedule]\n if (range === undefined) break\n const idx = nextToSchedule\n nextToSchedule++\n const task = (async () => {\n try {\n const result = await fetchRangeWithRetry(\n raw,\n accountInfo,\n options.fileId,\n range.start,\n range.end,\n totalSize,\n retryOptions,\n abort,\n )\n buffer.set(idx, result)\n } catch (err) {\n if (firstError === null) firstError = err\n } finally {\n inflight.delete(idx)\n }\n })()\n inflight.set(idx, task)\n }\n }\n\n return new ReadableStream<Uint8Array>({\n start(controller) {\n // Pre-warm: kick off the initial window so the first `pull` has\n // something to await. An already-aborted signal surfaces as a\n // stream error rather than a synchronous throw, matching the\n // contract callers had with the previous eager implementation.\n try {\n abort?.throwIfAborted()\n scheduleNext()\n } catch (err) {\n controller.error(err)\n }\n },\n async pull(controller) {\n try {\n // Wait until the next in-order chunk is available.\n while (!buffer.has(nextToEmit)) {\n abort?.throwIfAborted()\n if (firstError !== null) throw firstError\n // Zero-byte downloads have no planned ranges and close here. For\n // non-empty downloads, this guard prevents a future refactor from\n // stalling the stream if it leaves both maps empty mid-stream.\n if (inflight.size === 0) {\n controller.close()\n return\n }\n await Promise.race(inflight.values())\n }\n\n const result = buffer.get(nextToEmit)\n if (result !== undefined) {\n buffer.delete(nextToEmit)\n nextToEmit++\n const rangeSha1 = normalizeVerifiableSha1(result.contentSha1)\n if (expectedSha1 === undefined) {\n expectedSha1 = rangeSha1\n } else {\n // B2 range responses should agree on the same whole-file SHA-1;\n // any value or presence change makes verification untrustworthy.\n assertDownloadSha1HeaderAgreement(expectedSha1, rangeSha1)\n }\n if (expectedSha1 !== null) {\n assembledSha1 ??= new IncrementalSha1()\n await assembledSha1.update(result.data)\n }\n controller.enqueue(result.data)\n }\n\n // Top up the window now that we freed a slot by emitting.\n scheduleNext()\n\n if (\n nextToEmit >= ranges.length &&\n buffer.size === 0 &&\n inflight.size === 0 &&\n firstError === null\n ) {\n if (expectedSha1 !== undefined && expectedSha1 !== null && assembledSha1 !== null) {\n assertDownloadSha1(expectedSha1, await assembledSha1.digest())\n }\n controller.close()\n }\n } catch (err) {\n controller.error(err)\n }\n },\n cancel() {\n // Abort propagation handles in-flight requests; just drop buffered\n // data so it can be GC'd promptly.\n buffer.clear()\n },\n })\n}\n\n/**\n * Fetches a single byte range with bounded retry on transient failures.\n * @param raw - Low-level B2 API client.\n * @param accountInfo - Authorized account state.\n * @param fileId - ID of the file being downloaded.\n * @param start - Inclusive byte offset where the range begins.\n * @param end - Inclusive byte offset where the range ends.\n * @param totalSize - Expected complete file size.\n * @param retryOptions - Retry settings controlling attempts and backoff.\n * @param signal - Optional abort signal that cancels the range and any pending retry.\n *\n * @returns The range's bytes, or throws after exhausting all retry attempts.\n */\nasync function fetchRangeWithRetry(\n raw: RawClient,\n accountInfo: AccountInfo,\n fileId: FileId,\n start: number,\n end: number,\n totalSize: number,\n retryOptions: RetryOptions,\n signal: AbortSignal | undefined,\n): Promise<RangeDownloadResult> {\n let lastError: unknown\n for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {\n if (attempt > 0) {\n const retryAfter =\n lastError instanceof B2Error && lastError.retryAfter !== undefined\n ? lastError.retryAfter\n : undefined\n const delay = computeBackoff(attempt - 1, retryOptions, retryAfter)\n await sleep(delay, signal)\n }\n try {\n signal?.throwIfAborted()\n const resp = await raw.downloadFileById(\n accountInfo.getDownloadUrl(),\n accountInfo.getAuthToken(),\n fileId,\n {\n range: byteRangeHeader(start, end),\n ...(signal !== undefined ? { signal } : {}),\n },\n )\n if (resp.status < 200 || resp.status >= 300) {\n throw await classifyDownloadResponseError(resp)\n }\n if (!resp.body) throw new Error('Download chunk has no body')\n const data = await collectStream(resp.body)\n validateRangeResponse(resp, start, end, totalSize, data.byteLength)\n return {\n data,\n contentSha1: normalizeSha1(resp.headers.get('X-Bz-Content-Sha1')),\n }\n } catch (err) {\n lastError = err\n // Honour AbortSignal cancellation: don't retry past an abort.\n if (signal?.aborted) throw err\n if (err instanceof DOMException && err.name === 'AbortError') throw err\n if (!isRetryableRangeError(err) || attempt === retryOptions.maxRetries) {\n throw err\n }\n }\n }\n throw lastError instanceof Error ? lastError : new Error('Range download failed after retries')\n}\n\nclass RangeValidationError extends Error {\n readonly retryable = false\n\n constructor(message: string) {\n super(message)\n this.name = 'RangeValidationError'\n }\n}\n\nfunction validateRangeResponse(\n response: { headers: Headers; status: number },\n start: number,\n end: number,\n totalSize: number,\n byteLength: number,\n): void {\n const expectedLength = end - start + 1\n if (response.status !== 206) {\n throw new RangeValidationError(\n `Expected HTTP 206 Partial Content for range ${start}-${end}, got ${response.status}`,\n )\n }\n\n const contentRange = response.headers.get('Content-Range')\n if (contentRange === null) {\n throw new RangeValidationError(`Missing Content-Range for range ${start}-${end}`)\n }\n\n const match = contentRange.match(/^bytes (\\d+)-(\\d+)\\/(\\d+|\\*)$/)\n if (match === null) {\n throw new RangeValidationError(\n `Invalid Content-Range for range ${start}-${end}: ${contentRange}`,\n )\n }\n\n const actualStart = Number.parseInt(match[1] ?? '', 10)\n const actualEnd = Number.parseInt(match[2] ?? '', 10)\n const actualTotal = match[3] ?? ''\n if (actualStart !== start || actualEnd !== end) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match requested range ${start}-${end}`,\n )\n }\n\n if (actualTotal === '*') {\n throw new RangeValidationError(`Content-Range ${contentRange} does not include total size`)\n }\n\n const parsedTotal = Number.parseInt(actualTotal, 10)\n if (parsedTotal !== totalSize) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match expected total size ${totalSize}`,\n )\n }\n\n if (byteLength !== expectedLength) {\n throw new RangeValidationError(\n `Expected ${expectedLength} bytes for range ${start}-${end}, got ${byteLength}`,\n )\n }\n}\n\nasync function classifyDownloadResponseError(response: {\n headers: Headers\n status: number\n body: ReadableStream<Uint8Array> | null\n}): Promise<B2Error> {\n let errorBody: B2ErrorResponse = {\n status: response.status,\n code: 'internal_error',\n message: `HTTP ${response.status}`,\n }\n\n if (response.body !== null) {\n const bytes = await collectStream(response.body)\n try {\n const parsed = JSON.parse(utf8Decoder.decode(bytes)) as Partial<B2ErrorResponse>\n errorBody = {\n status: response.status,\n code: parsed.code ?? 'internal_error',\n message: parsed.message ?? `HTTP ${response.status}`,\n }\n } catch {\n // Keep the synthetic errorBody above when the response body is not JSON.\n }\n }\n\n const retryAfterHeader = response.headers.get('Retry-After')\n const retryAfter = retryAfterHeader !== null ? Number.parseInt(retryAfterHeader, 10) : undefined\n const requestId = response.headers.get('X-Bz-Request-Id') ?? undefined\n\n return classifyError(errorBody, {\n ...(retryAfter !== undefined ? { retryAfter } : {}),\n ...(requestId !== undefined ? { requestId } : {}),\n })\n}\n\nfunction isRetryableRangeError(err: unknown): boolean {\n if (err instanceof B2Error || err instanceof NetworkError) return err.retryable\n if (hasRetryableFlag(err)) return err.retryable\n // Bare FetchTransport surfaces network failures as TypeError. Validation\n // failures use RangeValidationError above, so they never land here.\n return err instanceof TypeError\n}\n\nfunction hasRetryableFlag(err: unknown): err is { retryable: boolean } {\n return (\n typeof err === 'object' &&\n err !== null &&\n 'retryable' in err &&\n typeof (err as { retryable?: unknown }).retryable === 'boolean'\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"parallel.cjs","names":[],"sources":["../../src/download/parallel.ts"],"sourcesContent":["import type { AccountInfo } from '../auth/account-info.ts'\nimport { B2Error, classifyError, NetworkError } from '../errors/index.ts'\nimport { computeBackoff, DEFAULT_RETRY_OPTIONS, type RetryOptions, sleep } from '../http/retry.ts'\nimport type { RawClient, SseCDownloadKey } from '../raw/index.ts'\nimport { collectStream } from '../streams/collect.ts'\nimport { IncrementalSha1 } from '../streams/hash.ts'\nimport { redactSseCKeyMaterial } from '../types/encryption.ts'\nimport type { B2ErrorResponse } from '../types/errors.ts'\nimport type { FileId } from '../types/ids.ts'\nimport { DEFAULT_DOWNLOAD_RANGE_SIZE, DEFAULT_TRANSFER_CONCURRENCY } from '../util/defaults.ts'\nimport { normalizeSha1 } from '../util/normalize.ts'\nimport { byteRangeHeader, planRanges } from '../util/plan-ranges.ts'\nimport { normalizeVerifiableSha1 } from '../util/sha1.ts'\nimport { utf8Decoder } from '../util/text-codec.ts'\nimport { assertDownloadSha1, assertDownloadSha1HeaderAgreement } from './checksum.ts'\n\n/** Options for downloading a file using concurrent byte-range requests. */\nexport interface ParallelDownloadOptions {\n /** ID of the file to download. */\n readonly fileId: FileId\n /** Total file size in bytes (must be known in advance). */\n readonly totalSize: number\n /** Size of each ranged chunk in bytes. Defaults to 10 MB. */\n readonly rangeSize?: number\n /** Maximum number of chunks fetched in parallel. Defaults to 4. */\n readonly concurrency?: number\n /**\n * SSE-C decryption parameters, required if the file was uploaded with SSE-C.\n * Internal diagnostics redact the copied options object, but the key is still\n * sent in plaintext SSE-C HTTP headers. Custom transports should not log\n * request headers that may contain customer-provided keys.\n */\n readonly serverSideEncryption?: SseCDownloadKey\n /**\n * Extra retry attempts per range on transient failures. Defaults to 0 because\n * `B2Client` already applies `RetryTransport`; set this only when supplying a\n * raw client that does not already retry transport failures.\n */\n readonly maxRetries?: number\n /** Signal to abort the download. */\n readonly signal?: AbortSignal\n}\n\ninterface RangeDownloadResult {\n readonly data: Uint8Array\n readonly contentSha1: string | null\n}\n\ninterface FetchRangeWithRetryParams {\n readonly raw: RawClient\n readonly accountInfo: AccountInfo\n readonly fileId: FileId\n readonly start: number\n readonly end: number\n readonly totalSize: number\n readonly serverSideEncryption: SseCDownloadKey | undefined\n readonly retryOptions: RetryOptions\n readonly signal: AbortSignal | undefined\n}\n\n/**\n * Creates a readable stream that downloads a file using parallel byte-range requests.\n *\n * The file is split into fixed-size ranges fetched in a **sliding\n * window** keyed off the consumer's read pace. The window size is\n * `concurrency * 2`: up to `concurrency` ranges are in flight at once,\n * plus up to `concurrency` completed-but-not-yet-emitted ranges buffered\n * ahead of the read head. New fetches kick off only when the consumer\n * reads a chunk, so a slow downstream pipe (e.g. a saturated network\n * sink or a `pipeTo` consumer that drains slowly) backpressures into\n * the SDK and bounds peak memory to `(concurrency * 2) * rangeSize`.\n *\n * The previous eager implementation scheduled every range into\n * `Promise.all` up front; a slow head-of-line range could hold the\n * entire file in memory while later ranges finished and waited.\n *\n * @param raw - Low-level B2 API client.\n * @param accountInfo - Authorized account state.\n * @param options - Parallel download parameters (file ID, size, concurrency).\n *\n * @returns A `ReadableStream` that yields file bytes in order.\n */\nexport function createParallelDownloadStream(\n raw: RawClient,\n accountInfo: AccountInfo,\n options: ParallelDownloadOptions,\n): ReadableStream<Uint8Array> {\n const rangeSize = options.rangeSize ?? DEFAULT_DOWNLOAD_RANGE_SIZE\n const concurrency = options.concurrency ?? DEFAULT_TRANSFER_CONCURRENCY\n const totalSize = options.totalSize\n const serverSideEncryption =\n options.serverSideEncryption !== undefined\n ? redactSseCKeyMaterial(options.serverSideEncryption, { label: 'SseCDownloadKey' })\n : undefined\n // The high-level B2Client already wraps the raw transport in RetryTransport.\n // Keep the parallel-download outer retry disabled by default so each range\n // has one retry budget unless callers explicitly opt into an extra raw-client\n // retry layer.\n const retryOptions = {\n ...DEFAULT_RETRY_OPTIONS,\n maxRetries: options.maxRetries ?? 0,\n }\n const internalAbort = new AbortController()\n const abort = internalAbort.signal\n let removeCallerAbortListener: (() => void) | undefined\n if (options.signal !== undefined) {\n if (options.signal.aborted) {\n internalAbort.abort(options.signal.reason)\n } else {\n const callerSignal = options.signal\n const abortFromCaller = () => internalAbort.abort(callerSignal.reason)\n callerSignal.addEventListener('abort', abortFromCaller, { once: true })\n removeCallerAbortListener = () => callerSignal.removeEventListener('abort', abortFromCaller)\n }\n }\n\n const ranges = planRanges(totalSize, rangeSize)\n\n // `inflight` holds currently-fetching range promises; `buffer` holds\n // completed ranges waiting for their slot in the emit order. Total\n // scheduled-but-not-emitted = inflight.size + buffer.size is capped\n // at `windowSize`, so peak memory is bounded regardless of any\n // single range's latency.\n const windowSize = concurrency * 2\n const inflight = new Map<number, Promise<void>>()\n const buffer = new Map<number, RangeDownloadResult>()\n let assembledSha1: IncrementalSha1 | null = null\n let nextToSchedule = 0\n let nextToEmit = 0\n let expectedSha1: string | null | undefined\n let firstError: unknown = null\n\n function abortInflight(reason?: unknown): void {\n if (!abort.aborted) internalAbort.abort(reason)\n }\n\n function cleanupCallerAbortListener(): void {\n removeCallerAbortListener?.()\n removeCallerAbortListener = undefined\n }\n\n function scheduleNext(): void {\n while (\n firstError === null &&\n // Honour abort here so a completed range that triggers a top-up\n // doesn't queue one final fetch after the caller aborted. Without\n // this gate, one extra range request fires post-abort before the\n // `pull()` loop notices.\n abort?.aborted !== true &&\n nextToSchedule < ranges.length &&\n inflight.size + buffer.size < windowSize\n ) {\n const range = ranges[nextToSchedule]\n if (range === undefined) break\n const idx = nextToSchedule\n nextToSchedule++\n const task = (async () => {\n try {\n const result = await fetchRangeWithRetry({\n raw,\n accountInfo,\n fileId: options.fileId,\n start: range.start,\n end: range.end,\n totalSize,\n serverSideEncryption,\n retryOptions,\n signal: abort,\n })\n buffer.set(idx, result)\n } catch (err) {\n if (firstError === null) {\n firstError = err\n abortInflight(err)\n }\n } finally {\n inflight.delete(idx)\n }\n })()\n inflight.set(idx, task)\n }\n }\n\n return new ReadableStream<Uint8Array>({\n start(controller) {\n // Pre-warm: kick off the initial window so the first `pull` has\n // something to await. An already-aborted signal surfaces as a\n // stream error rather than a synchronous throw, matching the\n // contract callers had with the previous eager implementation.\n try {\n abort?.throwIfAborted()\n scheduleNext()\n } catch (err) {\n cleanupCallerAbortListener()\n controller.error(err)\n }\n },\n async pull(controller) {\n try {\n // Wait until the next in-order chunk is available.\n while (!buffer.has(nextToEmit)) {\n abort?.throwIfAborted()\n if (firstError !== null) throw firstError\n // Zero-byte downloads have no planned ranges and close here. For\n // non-empty downloads, this guard prevents a future refactor from\n // stalling the stream if it leaves both maps empty mid-stream.\n if (inflight.size === 0) {\n cleanupCallerAbortListener()\n controller.close()\n return\n }\n await Promise.race(inflight.values())\n }\n\n const result = buffer.get(nextToEmit)\n if (result !== undefined) {\n buffer.delete(nextToEmit)\n nextToEmit++\n const rangeSha1 = normalizeVerifiableSha1(result.contentSha1)\n if (expectedSha1 === undefined) {\n expectedSha1 = rangeSha1\n } else {\n // B2 range responses should agree on the same whole-file SHA-1;\n // any value or presence change makes verification untrustworthy.\n assertDownloadSha1HeaderAgreement(expectedSha1, rangeSha1)\n }\n if (expectedSha1 !== null) {\n assembledSha1 ??= new IncrementalSha1()\n await assembledSha1.update(result.data)\n }\n controller.enqueue(result.data)\n }\n\n // Top up the window now that we freed a slot by emitting.\n scheduleNext()\n\n if (\n nextToEmit >= ranges.length &&\n buffer.size === 0 &&\n inflight.size === 0 &&\n firstError === null\n ) {\n if (expectedSha1 !== undefined && expectedSha1 !== null && assembledSha1 !== null) {\n assertDownloadSha1(expectedSha1, await assembledSha1.digest())\n }\n cleanupCallerAbortListener()\n controller.close()\n }\n } catch (err) {\n abortInflight(err)\n cleanupCallerAbortListener()\n controller.error(err)\n }\n },\n cancel(reason) {\n abortInflight(reason)\n cleanupCallerAbortListener()\n // Abort propagation handles in-flight requests; just drop buffered\n // data so it can be GC'd promptly.\n buffer.clear()\n },\n })\n}\n\n/**\n * Fetches a single byte range with bounded retry on transient failures.\n * @param params - Range request parameters, retry settings, and cancellation.\n *\n * @returns The range's bytes, or throws after exhausting all retry attempts.\n */\nasync function fetchRangeWithRetry(\n params: FetchRangeWithRetryParams,\n): Promise<RangeDownloadResult> {\n const {\n raw,\n accountInfo,\n fileId,\n start,\n end,\n totalSize,\n serverSideEncryption,\n retryOptions,\n signal,\n } = params\n let lastError: unknown\n for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {\n if (attempt > 0) {\n const retryAfter =\n lastError instanceof B2Error && lastError.retryAfter !== undefined\n ? lastError.retryAfter\n : undefined\n const delay = computeBackoff(attempt - 1, retryOptions, retryAfter)\n await sleep(delay, signal)\n }\n try {\n signal?.throwIfAborted()\n const resp = await raw.downloadFileById(\n accountInfo.getDownloadUrl(),\n accountInfo.getAuthToken(),\n fileId,\n {\n range: byteRangeHeader(start, end),\n ...(serverSideEncryption !== undefined ? { serverSideEncryption } : {}),\n ...(signal !== undefined ? { signal } : {}),\n },\n )\n if (resp.status < 200 || resp.status >= 300) {\n throw await classifyDownloadResponseError(resp)\n }\n if (!resp.body) throw new Error('Download chunk has no body')\n const data = await collectStream(resp.body)\n validateRangeResponse(resp, start, end, totalSize, data.byteLength)\n return {\n data,\n contentSha1: normalizeSha1(resp.headers.get('X-Bz-Content-Sha1')),\n }\n } catch (err) {\n lastError = err\n // Honour AbortSignal cancellation: don't retry past an abort.\n if (signal?.aborted) throw err\n if (err instanceof DOMException && err.name === 'AbortError') throw err\n if (!isRetryableRangeError(err) || attempt === retryOptions.maxRetries) {\n throw err\n }\n }\n }\n throw lastError instanceof Error ? lastError : new Error('Range download failed after retries')\n}\n\nclass RangeValidationError extends Error {\n readonly retryable = false\n\n constructor(message: string) {\n super(message)\n this.name = 'RangeValidationError'\n }\n}\n\nfunction validateRangeResponse(\n response: { headers: Headers; status: number },\n start: number,\n end: number,\n totalSize: number,\n byteLength: number,\n): void {\n const expectedLength = end - start + 1\n if (response.status !== 206) {\n throw new RangeValidationError(\n `Expected HTTP 206 Partial Content for range ${start}-${end}, got ${response.status}`,\n )\n }\n\n const contentRange = response.headers.get('Content-Range')\n if (contentRange === null) {\n throw new RangeValidationError(`Missing Content-Range for range ${start}-${end}`)\n }\n\n const match = contentRange.match(/^bytes (\\d+)-(\\d+)\\/(\\d+|\\*)$/)\n if (match === null) {\n throw new RangeValidationError(\n `Invalid Content-Range for range ${start}-${end}: ${contentRange}`,\n )\n }\n\n const actualStart = Number.parseInt(match[1] ?? '', 10)\n const actualEnd = Number.parseInt(match[2] ?? '', 10)\n const actualTotal = match[3] ?? ''\n if (actualStart !== start || actualEnd !== end) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match requested range ${start}-${end}`,\n )\n }\n\n if (actualTotal === '*') {\n throw new RangeValidationError(`Content-Range ${contentRange} does not include total size`)\n }\n\n const parsedTotal = Number.parseInt(actualTotal, 10)\n if (parsedTotal !== totalSize) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match expected total size ${totalSize}`,\n )\n }\n\n if (byteLength !== expectedLength) {\n throw new RangeValidationError(\n `Expected ${expectedLength} bytes for range ${start}-${end}, got ${byteLength}`,\n )\n }\n}\n\nasync function classifyDownloadResponseError(response: {\n headers: Headers\n status: number\n body: ReadableStream<Uint8Array> | null\n}): Promise<B2Error> {\n let errorBody: B2ErrorResponse = {\n status: response.status,\n code: 'internal_error',\n message: `HTTP ${response.status}`,\n }\n\n if (response.body !== null) {\n const bytes = await collectStream(response.body)\n try {\n const parsed = JSON.parse(utf8Decoder.decode(bytes)) as Partial<B2ErrorResponse>\n errorBody = {\n status: response.status,\n code: parsed.code ?? 'internal_error',\n message: parsed.message ?? `HTTP ${response.status}`,\n }\n } catch {\n // Keep the synthetic errorBody above when the response body is not JSON.\n }\n }\n\n const retryAfterHeader = response.headers.get('Retry-After')\n const retryAfter = retryAfterHeader !== null ? Number.parseInt(retryAfterHeader, 10) : undefined\n const requestId = response.headers.get('X-Bz-Request-Id') ?? undefined\n\n return classifyError(errorBody, {\n ...(retryAfter !== undefined ? { retryAfter } : {}),\n ...(requestId !== undefined ? { requestId } : {}),\n })\n}\n\nfunction isRetryableRangeError(err: unknown): boolean {\n if (err instanceof B2Error || err instanceof NetworkError) return err.retryable\n if (hasRetryableFlag(err)) return err.retryable\n // Bare FetchTransport surfaces network failures as TypeError. Validation\n // failures use RangeValidationError above, so they never land here.\n return err instanceof TypeError\n}\n\nfunction hasRetryableFlag(err: unknown): err is { retryable: boolean } {\n return (\n typeof err === 'object' &&\n err !== null &&\n 'retryable' in err &&\n typeof (err as { retryable?: unknown }).retryable === 'boolean'\n )\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { AccountInfo } from '../auth/account-info.js';
2
- import { RawClient } from '../raw/index.js';
2
+ import { RawClient, SseCDownloadKey } from '../raw/index.js';
3
3
  import { FileId } from '../types/ids.js';
4
4
  /** Options for downloading a file using concurrent byte-range requests. */
5
5
  export interface ParallelDownloadOptions {
@@ -11,6 +11,13 @@ export interface ParallelDownloadOptions {
11
11
  readonly rangeSize?: number;
12
12
  /** Maximum number of chunks fetched in parallel. Defaults to 4. */
13
13
  readonly concurrency?: number;
14
+ /**
15
+ * SSE-C decryption parameters, required if the file was uploaded with SSE-C.
16
+ * Internal diagnostics redact the copied options object, but the key is still
17
+ * sent in plaintext SSE-C HTTP headers. Custom transports should not log
18
+ * request headers that may contain customer-provided keys.
19
+ */
20
+ readonly serverSideEncryption?: SseCDownloadKey;
14
21
  /**
15
22
  * Extra retry attempts per range on transient failures. Defaults to 0 because
16
23
  * `B2Client` already applies `RetryTransport`; set this only when supplying a
@@ -1 +1 @@
1
- {"version":3,"file":"parallel.d.ts","sourceRoot":"","sources":["../../src/download/parallel.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAG1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAIhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAQ7C,2EAA2E;AAC3E,MAAM,WAAW,uBAAuB;IACtC,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAC9B;AAOD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,SAAS,EACd,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,uBAAuB,GAC/B,cAAc,CAAC,UAAU,CAAC,CA4I5B"}
1
+ {"version":3,"file":"parallel.d.ts","sourceRoot":"","sources":["../../src/download/parallel.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAG1D,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAKjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAQ7C,2EAA2E;AAC3E,MAAM,WAAW,uBAAuB;IACtC,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,eAAe,CAAA;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAC9B;AAmBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,SAAS,EACd,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,uBAAuB,GAC/B,cAAc,CAAC,UAAU,CAAC,CAgL5B"}
@@ -1,5 +1,5 @@
1
1
  import { AccountInfo } from '../auth/account-info.js';
2
- import { RawClient } from '../raw/index.js';
2
+ import { RawClient, SseCDownloadKey } from '../raw/index.js';
3
3
  import { FileId } from '../types/ids.js';
4
4
  /** Options for downloading a file using concurrent byte-range requests. */
5
5
  export interface ParallelDownloadOptions {
@@ -11,6 +11,13 @@ export interface ParallelDownloadOptions {
11
11
  readonly rangeSize?: number;
12
12
  /** Maximum number of chunks fetched in parallel. Defaults to 4. */
13
13
  readonly concurrency?: number;
14
+ /**
15
+ * SSE-C decryption parameters, required if the file was uploaded with SSE-C.
16
+ * Internal diagnostics redact the copied options object, but the key is still
17
+ * sent in plaintext SSE-C HTTP headers. Custom transports should not log
18
+ * request headers that may contain customer-provided keys.
19
+ */
20
+ readonly serverSideEncryption?: SseCDownloadKey;
14
21
  /**
15
22
  * Extra retry attempts per range on transient failures. Defaults to 0 because
16
23
  * `B2Client` already applies `RetryTransport`; set this only when supplying a
@@ -1 +1 @@
1
- {"version":3,"file":"parallel.d.ts","sourceRoot":"","sources":["../../src/download/parallel.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAG1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAIhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAQ7C,2EAA2E;AAC3E,MAAM,WAAW,uBAAuB;IACtC,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAC9B;AAOD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,SAAS,EACd,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,uBAAuB,GAC/B,cAAc,CAAC,UAAU,CAAC,CA4I5B"}
1
+ {"version":3,"file":"parallel.d.ts","sourceRoot":"","sources":["../../src/download/parallel.js"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAG1D,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAKjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAQ7C,2EAA2E;AAC3E,MAAM,WAAW,uBAAuB;IACtC,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,eAAe,CAAA;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAC9B;AAmBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,SAAS,EACd,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,uBAAuB,GAC/B,cAAc,CAAC,UAAU,CAAC,CAgL5B"}
@@ -1,6 +1,8 @@
1
1
  import { B2Error, NetworkError, classifyError } from "../errors/index.js";
2
+ import "../util/defaults.js";
2
3
  import { byteRangeHeader, planRanges } from "../util/plan-ranges.js";
3
4
  import { utf8Decoder } from "../util/text-codec.js";
5
+ import { redactSseCKeyMaterial } from "../types/encryption.js";
4
6
  import { normalizeSha1 } from "../util/normalize.js";
5
7
  import { IncrementalSha1 } from "../streams/hash.js";
6
8
  import { normalizeVerifiableSha1 } from "../util/sha1.js";
@@ -31,14 +33,26 @@ import { collectStream } from "../streams/collect.js";
31
33
  * @returns A `ReadableStream` that yields file bytes in order.
32
34
  */
33
35
  function createParallelDownloadStream(raw, accountInfo, options) {
34
- const rangeSize = options.rangeSize ?? 10 * 1024 * 1024;
36
+ const rangeSize = options.rangeSize ?? 10485760;
35
37
  const concurrency = options.concurrency ?? 4;
36
38
  const totalSize = options.totalSize;
39
+ const serverSideEncryption = options.serverSideEncryption !== void 0 ? redactSseCKeyMaterial(options.serverSideEncryption, { label: "SseCDownloadKey" }) : void 0;
37
40
  const retryOptions = {
38
41
  ...DEFAULT_RETRY_OPTIONS,
39
42
  maxRetries: options.maxRetries ?? 0
40
43
  };
41
- const abort = options.signal;
44
+ const internalAbort = new AbortController();
45
+ const abort = internalAbort.signal;
46
+ let removeCallerAbortListener;
47
+ if (options.signal !== void 0) {
48
+ if (options.signal.aborted) internalAbort.abort(options.signal.reason);
49
+ else {
50
+ const callerSignal = options.signal;
51
+ const abortFromCaller = () => internalAbort.abort(callerSignal.reason);
52
+ callerSignal.addEventListener("abort", abortFromCaller, { once: true });
53
+ removeCallerAbortListener = () => callerSignal.removeEventListener("abort", abortFromCaller);
54
+ }
55
+ }
42
56
  const ranges = planRanges(totalSize, rangeSize);
43
57
  const windowSize = concurrency * 2;
44
58
  const inflight = /* @__PURE__ */ new Map();
@@ -48,6 +62,13 @@ function createParallelDownloadStream(raw, accountInfo, options) {
48
62
  let nextToEmit = 0;
49
63
  let expectedSha1;
50
64
  let firstError = null;
65
+ function abortInflight(reason) {
66
+ if (!abort.aborted) internalAbort.abort(reason);
67
+ }
68
+ function cleanupCallerAbortListener() {
69
+ removeCallerAbortListener?.();
70
+ removeCallerAbortListener = void 0;
71
+ }
51
72
  function scheduleNext() {
52
73
  while (firstError === null && abort?.aborted !== true && nextToSchedule < ranges.length && inflight.size + buffer.size < windowSize) {
53
74
  const range = ranges[nextToSchedule];
@@ -56,10 +77,23 @@ function createParallelDownloadStream(raw, accountInfo, options) {
56
77
  nextToSchedule++;
57
78
  const task = (async () => {
58
79
  try {
59
- const result = await fetchRangeWithRetry(raw, accountInfo, options.fileId, range.start, range.end, totalSize, retryOptions, abort);
80
+ const result = await fetchRangeWithRetry({
81
+ raw,
82
+ accountInfo,
83
+ fileId: options.fileId,
84
+ start: range.start,
85
+ end: range.end,
86
+ totalSize,
87
+ serverSideEncryption,
88
+ retryOptions,
89
+ signal: abort
90
+ });
60
91
  buffer.set(idx, result);
61
92
  } catch (err) {
62
- if (firstError === null) firstError = err;
93
+ if (firstError === null) {
94
+ firstError = err;
95
+ abortInflight(err);
96
+ }
63
97
  } finally {
64
98
  inflight.delete(idx);
65
99
  }
@@ -73,6 +107,7 @@ function createParallelDownloadStream(raw, accountInfo, options) {
73
107
  abort?.throwIfAborted();
74
108
  scheduleNext();
75
109
  } catch (err) {
110
+ cleanupCallerAbortListener();
76
111
  controller.error(err);
77
112
  }
78
113
  },
@@ -82,6 +117,7 @@ function createParallelDownloadStream(raw, accountInfo, options) {
82
117
  abort?.throwIfAborted();
83
118
  if (firstError !== null) throw firstError;
84
119
  if (inflight.size === 0) {
120
+ cleanupCallerAbortListener();
85
121
  controller.close();
86
122
  return;
87
123
  }
@@ -103,41 +139,42 @@ function createParallelDownloadStream(raw, accountInfo, options) {
103
139
  scheduleNext();
104
140
  if (nextToEmit >= ranges.length && buffer.size === 0 && inflight.size === 0 && firstError === null) {
105
141
  if (expectedSha1 !== void 0 && expectedSha1 !== null && assembledSha1 !== null) assertDownloadSha1(expectedSha1, await assembledSha1.digest());
142
+ cleanupCallerAbortListener();
106
143
  controller.close();
107
144
  }
108
145
  } catch (err) {
146
+ abortInflight(err);
147
+ cleanupCallerAbortListener();
109
148
  controller.error(err);
110
149
  }
111
150
  },
112
- cancel() {
151
+ cancel(reason) {
152
+ abortInflight(reason);
153
+ cleanupCallerAbortListener();
113
154
  buffer.clear();
114
155
  }
115
156
  });
116
157
  }
117
158
  /**
118
159
  * Fetches a single byte range with bounded retry on transient failures.
119
- * @param raw - Low-level B2 API client.
120
- * @param accountInfo - Authorized account state.
121
- * @param fileId - ID of the file being downloaded.
122
- * @param start - Inclusive byte offset where the range begins.
123
- * @param end - Inclusive byte offset where the range ends.
124
- * @param totalSize - Expected complete file size.
125
- * @param retryOptions - Retry settings controlling attempts and backoff.
126
- * @param signal - Optional abort signal that cancels the range and any pending retry.
160
+ * @param params - Range request parameters, retry settings, and cancellation.
127
161
  *
128
162
  * @returns The range's bytes, or throws after exhausting all retry attempts.
129
163
  */
130
- async function fetchRangeWithRetry(raw, accountInfo, fileId, start, end, totalSize, retryOptions, signal) {
164
+ async function fetchRangeWithRetry(params) {
165
+ const { raw, accountInfo, fileId, start, end, totalSize, serverSideEncryption, retryOptions, signal } = params;
131
166
  let lastError;
132
167
  for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {
133
168
  if (attempt > 0) {
134
169
  const retryAfter = lastError instanceof B2Error && lastError.retryAfter !== void 0 ? lastError.retryAfter : void 0;
135
- await sleep(computeBackoff(attempt - 1, retryOptions, retryAfter), signal);
170
+ const delay = computeBackoff(attempt - 1, retryOptions, retryAfter);
171
+ await sleep(delay, signal);
136
172
  }
137
173
  try {
138
174
  signal?.throwIfAborted();
139
175
  const resp = await raw.downloadFileById(accountInfo.getDownloadUrl(), accountInfo.getAuthToken(), fileId, {
140
176
  range: byteRangeHeader(start, end),
177
+ ...serverSideEncryption !== void 0 ? { serverSideEncryption } : {},
141
178
  ...signal !== void 0 ? { signal } : {}
142
179
  });
143
180
  if (resp.status < 200 || resp.status >= 300) throw await classifyDownloadResponseError(resp);
@@ -1 +1 @@
1
- {"version":3,"file":"parallel.js","names":[],"sources":["../../src/download/parallel.ts"],"sourcesContent":["import type { AccountInfo } from '../auth/account-info.ts'\nimport { B2Error, classifyError, NetworkError } from '../errors/index.ts'\nimport { computeBackoff, DEFAULT_RETRY_OPTIONS, type RetryOptions, sleep } from '../http/retry.ts'\nimport type { RawClient } from '../raw/index.ts'\nimport { collectStream } from '../streams/collect.ts'\nimport { IncrementalSha1 } from '../streams/hash.ts'\nimport type { B2ErrorResponse } from '../types/errors.ts'\nimport type { FileId } from '../types/ids.ts'\nimport { DEFAULT_TRANSFER_CONCURRENCY } from '../util/defaults.ts'\nimport { normalizeSha1 } from '../util/normalize.ts'\nimport { byteRangeHeader, planRanges } from '../util/plan-ranges.ts'\nimport { normalizeVerifiableSha1 } from '../util/sha1.ts'\nimport { utf8Decoder } from '../util/text-codec.ts'\nimport { assertDownloadSha1, assertDownloadSha1HeaderAgreement } from './checksum.ts'\n\n/** Options for downloading a file using concurrent byte-range requests. */\nexport interface ParallelDownloadOptions {\n /** ID of the file to download. */\n readonly fileId: FileId\n /** Total file size in bytes (must be known in advance). */\n readonly totalSize: number\n /** Size of each ranged chunk in bytes. Defaults to 10 MB. */\n readonly rangeSize?: number\n /** Maximum number of chunks fetched in parallel. Defaults to 4. */\n readonly concurrency?: number\n /**\n * Extra retry attempts per range on transient failures. Defaults to 0 because\n * `B2Client` already applies `RetryTransport`; set this only when supplying a\n * raw client that does not already retry transport failures.\n */\n readonly maxRetries?: number\n /** Signal to abort the download. */\n readonly signal?: AbortSignal\n}\n\ninterface RangeDownloadResult {\n readonly data: Uint8Array\n readonly contentSha1: string | null\n}\n\n/**\n * Creates a readable stream that downloads a file using parallel byte-range requests.\n *\n * The file is split into fixed-size ranges fetched in a **sliding\n * window** keyed off the consumer's read pace. The window size is\n * `concurrency * 2`: up to `concurrency` ranges are in flight at once,\n * plus up to `concurrency` completed-but-not-yet-emitted ranges buffered\n * ahead of the read head. New fetches kick off only when the consumer\n * reads a chunk, so a slow downstream pipe (e.g. a saturated network\n * sink or a `pipeTo` consumer that drains slowly) backpressures into\n * the SDK and bounds peak memory to `(concurrency * 2) * rangeSize`.\n *\n * The previous eager implementation scheduled every range into\n * `Promise.all` up front; a slow head-of-line range could hold the\n * entire file in memory while later ranges finished and waited.\n *\n * @param raw - Low-level B2 API client.\n * @param accountInfo - Authorized account state.\n * @param options - Parallel download parameters (file ID, size, concurrency).\n *\n * @returns A `ReadableStream` that yields file bytes in order.\n */\nexport function createParallelDownloadStream(\n raw: RawClient,\n accountInfo: AccountInfo,\n options: ParallelDownloadOptions,\n): ReadableStream<Uint8Array> {\n const rangeSize = options.rangeSize ?? 10 * 1024 * 1024\n const concurrency = options.concurrency ?? DEFAULT_TRANSFER_CONCURRENCY\n const totalSize = options.totalSize\n // The high-level B2Client already wraps the raw transport in RetryTransport.\n // Keep the parallel-download outer retry disabled by default so each range\n // has one retry budget unless callers explicitly opt into an extra raw-client\n // retry layer.\n const retryOptions = {\n ...DEFAULT_RETRY_OPTIONS,\n maxRetries: options.maxRetries ?? 0,\n }\n const abort = options.signal\n\n const ranges = planRanges(totalSize, rangeSize)\n\n // `inflight` holds currently-fetching range promises; `buffer` holds\n // completed ranges waiting for their slot in the emit order. Total\n // scheduled-but-not-emitted = inflight.size + buffer.size is capped\n // at `windowSize`, so peak memory is bounded regardless of any\n // single range's latency.\n const windowSize = concurrency * 2\n const inflight = new Map<number, Promise<void>>()\n const buffer = new Map<number, RangeDownloadResult>()\n let assembledSha1: IncrementalSha1 | null = null\n let nextToSchedule = 0\n let nextToEmit = 0\n let expectedSha1: string | null | undefined\n let firstError: unknown = null\n\n function scheduleNext(): void {\n while (\n firstError === null &&\n // Honour abort here so a completed range that triggers a top-up\n // doesn't queue one final fetch after the caller aborted. Without\n // this gate, one extra range request fires post-abort before the\n // `pull()` loop notices.\n abort?.aborted !== true &&\n nextToSchedule < ranges.length &&\n inflight.size + buffer.size < windowSize\n ) {\n const range = ranges[nextToSchedule]\n if (range === undefined) break\n const idx = nextToSchedule\n nextToSchedule++\n const task = (async () => {\n try {\n const result = await fetchRangeWithRetry(\n raw,\n accountInfo,\n options.fileId,\n range.start,\n range.end,\n totalSize,\n retryOptions,\n abort,\n )\n buffer.set(idx, result)\n } catch (err) {\n if (firstError === null) firstError = err\n } finally {\n inflight.delete(idx)\n }\n })()\n inflight.set(idx, task)\n }\n }\n\n return new ReadableStream<Uint8Array>({\n start(controller) {\n // Pre-warm: kick off the initial window so the first `pull` has\n // something to await. An already-aborted signal surfaces as a\n // stream error rather than a synchronous throw, matching the\n // contract callers had with the previous eager implementation.\n try {\n abort?.throwIfAborted()\n scheduleNext()\n } catch (err) {\n controller.error(err)\n }\n },\n async pull(controller) {\n try {\n // Wait until the next in-order chunk is available.\n while (!buffer.has(nextToEmit)) {\n abort?.throwIfAborted()\n if (firstError !== null) throw firstError\n // Zero-byte downloads have no planned ranges and close here. For\n // non-empty downloads, this guard prevents a future refactor from\n // stalling the stream if it leaves both maps empty mid-stream.\n if (inflight.size === 0) {\n controller.close()\n return\n }\n await Promise.race(inflight.values())\n }\n\n const result = buffer.get(nextToEmit)\n if (result !== undefined) {\n buffer.delete(nextToEmit)\n nextToEmit++\n const rangeSha1 = normalizeVerifiableSha1(result.contentSha1)\n if (expectedSha1 === undefined) {\n expectedSha1 = rangeSha1\n } else {\n // B2 range responses should agree on the same whole-file SHA-1;\n // any value or presence change makes verification untrustworthy.\n assertDownloadSha1HeaderAgreement(expectedSha1, rangeSha1)\n }\n if (expectedSha1 !== null) {\n assembledSha1 ??= new IncrementalSha1()\n await assembledSha1.update(result.data)\n }\n controller.enqueue(result.data)\n }\n\n // Top up the window now that we freed a slot by emitting.\n scheduleNext()\n\n if (\n nextToEmit >= ranges.length &&\n buffer.size === 0 &&\n inflight.size === 0 &&\n firstError === null\n ) {\n if (expectedSha1 !== undefined && expectedSha1 !== null && assembledSha1 !== null) {\n assertDownloadSha1(expectedSha1, await assembledSha1.digest())\n }\n controller.close()\n }\n } catch (err) {\n controller.error(err)\n }\n },\n cancel() {\n // Abort propagation handles in-flight requests; just drop buffered\n // data so it can be GC'd promptly.\n buffer.clear()\n },\n })\n}\n\n/**\n * Fetches a single byte range with bounded retry on transient failures.\n * @param raw - Low-level B2 API client.\n * @param accountInfo - Authorized account state.\n * @param fileId - ID of the file being downloaded.\n * @param start - Inclusive byte offset where the range begins.\n * @param end - Inclusive byte offset where the range ends.\n * @param totalSize - Expected complete file size.\n * @param retryOptions - Retry settings controlling attempts and backoff.\n * @param signal - Optional abort signal that cancels the range and any pending retry.\n *\n * @returns The range's bytes, or throws after exhausting all retry attempts.\n */\nasync function fetchRangeWithRetry(\n raw: RawClient,\n accountInfo: AccountInfo,\n fileId: FileId,\n start: number,\n end: number,\n totalSize: number,\n retryOptions: RetryOptions,\n signal: AbortSignal | undefined,\n): Promise<RangeDownloadResult> {\n let lastError: unknown\n for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {\n if (attempt > 0) {\n const retryAfter =\n lastError instanceof B2Error && lastError.retryAfter !== undefined\n ? lastError.retryAfter\n : undefined\n const delay = computeBackoff(attempt - 1, retryOptions, retryAfter)\n await sleep(delay, signal)\n }\n try {\n signal?.throwIfAborted()\n const resp = await raw.downloadFileById(\n accountInfo.getDownloadUrl(),\n accountInfo.getAuthToken(),\n fileId,\n {\n range: byteRangeHeader(start, end),\n ...(signal !== undefined ? { signal } : {}),\n },\n )\n if (resp.status < 200 || resp.status >= 300) {\n throw await classifyDownloadResponseError(resp)\n }\n if (!resp.body) throw new Error('Download chunk has no body')\n const data = await collectStream(resp.body)\n validateRangeResponse(resp, start, end, totalSize, data.byteLength)\n return {\n data,\n contentSha1: normalizeSha1(resp.headers.get('X-Bz-Content-Sha1')),\n }\n } catch (err) {\n lastError = err\n // Honour AbortSignal cancellation: don't retry past an abort.\n if (signal?.aborted) throw err\n if (err instanceof DOMException && err.name === 'AbortError') throw err\n if (!isRetryableRangeError(err) || attempt === retryOptions.maxRetries) {\n throw err\n }\n }\n }\n throw lastError instanceof Error ? lastError : new Error('Range download failed after retries')\n}\n\nclass RangeValidationError extends Error {\n readonly retryable = false\n\n constructor(message: string) {\n super(message)\n this.name = 'RangeValidationError'\n }\n}\n\nfunction validateRangeResponse(\n response: { headers: Headers; status: number },\n start: number,\n end: number,\n totalSize: number,\n byteLength: number,\n): void {\n const expectedLength = end - start + 1\n if (response.status !== 206) {\n throw new RangeValidationError(\n `Expected HTTP 206 Partial Content for range ${start}-${end}, got ${response.status}`,\n )\n }\n\n const contentRange = response.headers.get('Content-Range')\n if (contentRange === null) {\n throw new RangeValidationError(`Missing Content-Range for range ${start}-${end}`)\n }\n\n const match = contentRange.match(/^bytes (\\d+)-(\\d+)\\/(\\d+|\\*)$/)\n if (match === null) {\n throw new RangeValidationError(\n `Invalid Content-Range for range ${start}-${end}: ${contentRange}`,\n )\n }\n\n const actualStart = Number.parseInt(match[1] ?? '', 10)\n const actualEnd = Number.parseInt(match[2] ?? '', 10)\n const actualTotal = match[3] ?? ''\n if (actualStart !== start || actualEnd !== end) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match requested range ${start}-${end}`,\n )\n }\n\n if (actualTotal === '*') {\n throw new RangeValidationError(`Content-Range ${contentRange} does not include total size`)\n }\n\n const parsedTotal = Number.parseInt(actualTotal, 10)\n if (parsedTotal !== totalSize) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match expected total size ${totalSize}`,\n )\n }\n\n if (byteLength !== expectedLength) {\n throw new RangeValidationError(\n `Expected ${expectedLength} bytes for range ${start}-${end}, got ${byteLength}`,\n )\n }\n}\n\nasync function classifyDownloadResponseError(response: {\n headers: Headers\n status: number\n body: ReadableStream<Uint8Array> | null\n}): Promise<B2Error> {\n let errorBody: B2ErrorResponse = {\n status: response.status,\n code: 'internal_error',\n message: `HTTP ${response.status}`,\n }\n\n if (response.body !== null) {\n const bytes = await collectStream(response.body)\n try {\n const parsed = JSON.parse(utf8Decoder.decode(bytes)) as Partial<B2ErrorResponse>\n errorBody = {\n status: response.status,\n code: parsed.code ?? 'internal_error',\n message: parsed.message ?? `HTTP ${response.status}`,\n }\n } catch {\n // Keep the synthetic errorBody above when the response body is not JSON.\n }\n }\n\n const retryAfterHeader = response.headers.get('Retry-After')\n const retryAfter = retryAfterHeader !== null ? Number.parseInt(retryAfterHeader, 10) : undefined\n const requestId = response.headers.get('X-Bz-Request-Id') ?? undefined\n\n return classifyError(errorBody, {\n ...(retryAfter !== undefined ? { retryAfter } : {}),\n ...(requestId !== undefined ? { requestId } : {}),\n })\n}\n\nfunction isRetryableRangeError(err: unknown): boolean {\n if (err instanceof B2Error || err instanceof NetworkError) return err.retryable\n if (hasRetryableFlag(err)) return err.retryable\n // Bare FetchTransport surfaces network failures as TypeError. Validation\n // failures use RangeValidationError above, so they never land here.\n return err instanceof TypeError\n}\n\nfunction hasRetryableFlag(err: unknown): err is { retryable: boolean } {\n return (\n typeof err === 'object' &&\n err !== null &&\n 'retryable' in err &&\n typeof (err as { retryable?: unknown }).retryable === 'boolean'\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"parallel.js","names":[],"sources":["../../src/download/parallel.ts"],"sourcesContent":["import type { AccountInfo } from '../auth/account-info.ts'\nimport { B2Error, classifyError, NetworkError } from '../errors/index.ts'\nimport { computeBackoff, DEFAULT_RETRY_OPTIONS, type RetryOptions, sleep } from '../http/retry.ts'\nimport type { RawClient, SseCDownloadKey } from '../raw/index.ts'\nimport { collectStream } from '../streams/collect.ts'\nimport { IncrementalSha1 } from '../streams/hash.ts'\nimport { redactSseCKeyMaterial } from '../types/encryption.ts'\nimport type { B2ErrorResponse } from '../types/errors.ts'\nimport type { FileId } from '../types/ids.ts'\nimport { DEFAULT_DOWNLOAD_RANGE_SIZE, DEFAULT_TRANSFER_CONCURRENCY } from '../util/defaults.ts'\nimport { normalizeSha1 } from '../util/normalize.ts'\nimport { byteRangeHeader, planRanges } from '../util/plan-ranges.ts'\nimport { normalizeVerifiableSha1 } from '../util/sha1.ts'\nimport { utf8Decoder } from '../util/text-codec.ts'\nimport { assertDownloadSha1, assertDownloadSha1HeaderAgreement } from './checksum.ts'\n\n/** Options for downloading a file using concurrent byte-range requests. */\nexport interface ParallelDownloadOptions {\n /** ID of the file to download. */\n readonly fileId: FileId\n /** Total file size in bytes (must be known in advance). */\n readonly totalSize: number\n /** Size of each ranged chunk in bytes. Defaults to 10 MB. */\n readonly rangeSize?: number\n /** Maximum number of chunks fetched in parallel. Defaults to 4. */\n readonly concurrency?: number\n /**\n * SSE-C decryption parameters, required if the file was uploaded with SSE-C.\n * Internal diagnostics redact the copied options object, but the key is still\n * sent in plaintext SSE-C HTTP headers. Custom transports should not log\n * request headers that may contain customer-provided keys.\n */\n readonly serverSideEncryption?: SseCDownloadKey\n /**\n * Extra retry attempts per range on transient failures. Defaults to 0 because\n * `B2Client` already applies `RetryTransport`; set this only when supplying a\n * raw client that does not already retry transport failures.\n */\n readonly maxRetries?: number\n /** Signal to abort the download. */\n readonly signal?: AbortSignal\n}\n\ninterface RangeDownloadResult {\n readonly data: Uint8Array\n readonly contentSha1: string | null\n}\n\ninterface FetchRangeWithRetryParams {\n readonly raw: RawClient\n readonly accountInfo: AccountInfo\n readonly fileId: FileId\n readonly start: number\n readonly end: number\n readonly totalSize: number\n readonly serverSideEncryption: SseCDownloadKey | undefined\n readonly retryOptions: RetryOptions\n readonly signal: AbortSignal | undefined\n}\n\n/**\n * Creates a readable stream that downloads a file using parallel byte-range requests.\n *\n * The file is split into fixed-size ranges fetched in a **sliding\n * window** keyed off the consumer's read pace. The window size is\n * `concurrency * 2`: up to `concurrency` ranges are in flight at once,\n * plus up to `concurrency` completed-but-not-yet-emitted ranges buffered\n * ahead of the read head. New fetches kick off only when the consumer\n * reads a chunk, so a slow downstream pipe (e.g. a saturated network\n * sink or a `pipeTo` consumer that drains slowly) backpressures into\n * the SDK and bounds peak memory to `(concurrency * 2) * rangeSize`.\n *\n * The previous eager implementation scheduled every range into\n * `Promise.all` up front; a slow head-of-line range could hold the\n * entire file in memory while later ranges finished and waited.\n *\n * @param raw - Low-level B2 API client.\n * @param accountInfo - Authorized account state.\n * @param options - Parallel download parameters (file ID, size, concurrency).\n *\n * @returns A `ReadableStream` that yields file bytes in order.\n */\nexport function createParallelDownloadStream(\n raw: RawClient,\n accountInfo: AccountInfo,\n options: ParallelDownloadOptions,\n): ReadableStream<Uint8Array> {\n const rangeSize = options.rangeSize ?? DEFAULT_DOWNLOAD_RANGE_SIZE\n const concurrency = options.concurrency ?? DEFAULT_TRANSFER_CONCURRENCY\n const totalSize = options.totalSize\n const serverSideEncryption =\n options.serverSideEncryption !== undefined\n ? redactSseCKeyMaterial(options.serverSideEncryption, { label: 'SseCDownloadKey' })\n : undefined\n // The high-level B2Client already wraps the raw transport in RetryTransport.\n // Keep the parallel-download outer retry disabled by default so each range\n // has one retry budget unless callers explicitly opt into an extra raw-client\n // retry layer.\n const retryOptions = {\n ...DEFAULT_RETRY_OPTIONS,\n maxRetries: options.maxRetries ?? 0,\n }\n const internalAbort = new AbortController()\n const abort = internalAbort.signal\n let removeCallerAbortListener: (() => void) | undefined\n if (options.signal !== undefined) {\n if (options.signal.aborted) {\n internalAbort.abort(options.signal.reason)\n } else {\n const callerSignal = options.signal\n const abortFromCaller = () => internalAbort.abort(callerSignal.reason)\n callerSignal.addEventListener('abort', abortFromCaller, { once: true })\n removeCallerAbortListener = () => callerSignal.removeEventListener('abort', abortFromCaller)\n }\n }\n\n const ranges = planRanges(totalSize, rangeSize)\n\n // `inflight` holds currently-fetching range promises; `buffer` holds\n // completed ranges waiting for their slot in the emit order. Total\n // scheduled-but-not-emitted = inflight.size + buffer.size is capped\n // at `windowSize`, so peak memory is bounded regardless of any\n // single range's latency.\n const windowSize = concurrency * 2\n const inflight = new Map<number, Promise<void>>()\n const buffer = new Map<number, RangeDownloadResult>()\n let assembledSha1: IncrementalSha1 | null = null\n let nextToSchedule = 0\n let nextToEmit = 0\n let expectedSha1: string | null | undefined\n let firstError: unknown = null\n\n function abortInflight(reason?: unknown): void {\n if (!abort.aborted) internalAbort.abort(reason)\n }\n\n function cleanupCallerAbortListener(): void {\n removeCallerAbortListener?.()\n removeCallerAbortListener = undefined\n }\n\n function scheduleNext(): void {\n while (\n firstError === null &&\n // Honour abort here so a completed range that triggers a top-up\n // doesn't queue one final fetch after the caller aborted. Without\n // this gate, one extra range request fires post-abort before the\n // `pull()` loop notices.\n abort?.aborted !== true &&\n nextToSchedule < ranges.length &&\n inflight.size + buffer.size < windowSize\n ) {\n const range = ranges[nextToSchedule]\n if (range === undefined) break\n const idx = nextToSchedule\n nextToSchedule++\n const task = (async () => {\n try {\n const result = await fetchRangeWithRetry({\n raw,\n accountInfo,\n fileId: options.fileId,\n start: range.start,\n end: range.end,\n totalSize,\n serverSideEncryption,\n retryOptions,\n signal: abort,\n })\n buffer.set(idx, result)\n } catch (err) {\n if (firstError === null) {\n firstError = err\n abortInflight(err)\n }\n } finally {\n inflight.delete(idx)\n }\n })()\n inflight.set(idx, task)\n }\n }\n\n return new ReadableStream<Uint8Array>({\n start(controller) {\n // Pre-warm: kick off the initial window so the first `pull` has\n // something to await. An already-aborted signal surfaces as a\n // stream error rather than a synchronous throw, matching the\n // contract callers had with the previous eager implementation.\n try {\n abort?.throwIfAborted()\n scheduleNext()\n } catch (err) {\n cleanupCallerAbortListener()\n controller.error(err)\n }\n },\n async pull(controller) {\n try {\n // Wait until the next in-order chunk is available.\n while (!buffer.has(nextToEmit)) {\n abort?.throwIfAborted()\n if (firstError !== null) throw firstError\n // Zero-byte downloads have no planned ranges and close here. For\n // non-empty downloads, this guard prevents a future refactor from\n // stalling the stream if it leaves both maps empty mid-stream.\n if (inflight.size === 0) {\n cleanupCallerAbortListener()\n controller.close()\n return\n }\n await Promise.race(inflight.values())\n }\n\n const result = buffer.get(nextToEmit)\n if (result !== undefined) {\n buffer.delete(nextToEmit)\n nextToEmit++\n const rangeSha1 = normalizeVerifiableSha1(result.contentSha1)\n if (expectedSha1 === undefined) {\n expectedSha1 = rangeSha1\n } else {\n // B2 range responses should agree on the same whole-file SHA-1;\n // any value or presence change makes verification untrustworthy.\n assertDownloadSha1HeaderAgreement(expectedSha1, rangeSha1)\n }\n if (expectedSha1 !== null) {\n assembledSha1 ??= new IncrementalSha1()\n await assembledSha1.update(result.data)\n }\n controller.enqueue(result.data)\n }\n\n // Top up the window now that we freed a slot by emitting.\n scheduleNext()\n\n if (\n nextToEmit >= ranges.length &&\n buffer.size === 0 &&\n inflight.size === 0 &&\n firstError === null\n ) {\n if (expectedSha1 !== undefined && expectedSha1 !== null && assembledSha1 !== null) {\n assertDownloadSha1(expectedSha1, await assembledSha1.digest())\n }\n cleanupCallerAbortListener()\n controller.close()\n }\n } catch (err) {\n abortInflight(err)\n cleanupCallerAbortListener()\n controller.error(err)\n }\n },\n cancel(reason) {\n abortInflight(reason)\n cleanupCallerAbortListener()\n // Abort propagation handles in-flight requests; just drop buffered\n // data so it can be GC'd promptly.\n buffer.clear()\n },\n })\n}\n\n/**\n * Fetches a single byte range with bounded retry on transient failures.\n * @param params - Range request parameters, retry settings, and cancellation.\n *\n * @returns The range's bytes, or throws after exhausting all retry attempts.\n */\nasync function fetchRangeWithRetry(\n params: FetchRangeWithRetryParams,\n): Promise<RangeDownloadResult> {\n const {\n raw,\n accountInfo,\n fileId,\n start,\n end,\n totalSize,\n serverSideEncryption,\n retryOptions,\n signal,\n } = params\n let lastError: unknown\n for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {\n if (attempt > 0) {\n const retryAfter =\n lastError instanceof B2Error && lastError.retryAfter !== undefined\n ? lastError.retryAfter\n : undefined\n const delay = computeBackoff(attempt - 1, retryOptions, retryAfter)\n await sleep(delay, signal)\n }\n try {\n signal?.throwIfAborted()\n const resp = await raw.downloadFileById(\n accountInfo.getDownloadUrl(),\n accountInfo.getAuthToken(),\n fileId,\n {\n range: byteRangeHeader(start, end),\n ...(serverSideEncryption !== undefined ? { serverSideEncryption } : {}),\n ...(signal !== undefined ? { signal } : {}),\n },\n )\n if (resp.status < 200 || resp.status >= 300) {\n throw await classifyDownloadResponseError(resp)\n }\n if (!resp.body) throw new Error('Download chunk has no body')\n const data = await collectStream(resp.body)\n validateRangeResponse(resp, start, end, totalSize, data.byteLength)\n return {\n data,\n contentSha1: normalizeSha1(resp.headers.get('X-Bz-Content-Sha1')),\n }\n } catch (err) {\n lastError = err\n // Honour AbortSignal cancellation: don't retry past an abort.\n if (signal?.aborted) throw err\n if (err instanceof DOMException && err.name === 'AbortError') throw err\n if (!isRetryableRangeError(err) || attempt === retryOptions.maxRetries) {\n throw err\n }\n }\n }\n throw lastError instanceof Error ? lastError : new Error('Range download failed after retries')\n}\n\nclass RangeValidationError extends Error {\n readonly retryable = false\n\n constructor(message: string) {\n super(message)\n this.name = 'RangeValidationError'\n }\n}\n\nfunction validateRangeResponse(\n response: { headers: Headers; status: number },\n start: number,\n end: number,\n totalSize: number,\n byteLength: number,\n): void {\n const expectedLength = end - start + 1\n if (response.status !== 206) {\n throw new RangeValidationError(\n `Expected HTTP 206 Partial Content for range ${start}-${end}, got ${response.status}`,\n )\n }\n\n const contentRange = response.headers.get('Content-Range')\n if (contentRange === null) {\n throw new RangeValidationError(`Missing Content-Range for range ${start}-${end}`)\n }\n\n const match = contentRange.match(/^bytes (\\d+)-(\\d+)\\/(\\d+|\\*)$/)\n if (match === null) {\n throw new RangeValidationError(\n `Invalid Content-Range for range ${start}-${end}: ${contentRange}`,\n )\n }\n\n const actualStart = Number.parseInt(match[1] ?? '', 10)\n const actualEnd = Number.parseInt(match[2] ?? '', 10)\n const actualTotal = match[3] ?? ''\n if (actualStart !== start || actualEnd !== end) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match requested range ${start}-${end}`,\n )\n }\n\n if (actualTotal === '*') {\n throw new RangeValidationError(`Content-Range ${contentRange} does not include total size`)\n }\n\n const parsedTotal = Number.parseInt(actualTotal, 10)\n if (parsedTotal !== totalSize) {\n throw new RangeValidationError(\n `Content-Range ${contentRange} does not match expected total size ${totalSize}`,\n )\n }\n\n if (byteLength !== expectedLength) {\n throw new RangeValidationError(\n `Expected ${expectedLength} bytes for range ${start}-${end}, got ${byteLength}`,\n )\n }\n}\n\nasync function classifyDownloadResponseError(response: {\n headers: Headers\n status: number\n body: ReadableStream<Uint8Array> | null\n}): Promise<B2Error> {\n let errorBody: B2ErrorResponse = {\n status: response.status,\n code: 'internal_error',\n message: `HTTP ${response.status}`,\n }\n\n if (response.body !== null) {\n const bytes = await collectStream(response.body)\n try {\n const parsed = JSON.parse(utf8Decoder.decode(bytes)) as Partial<B2ErrorResponse>\n errorBody = {\n status: response.status,\n code: parsed.code ?? 'internal_error',\n message: parsed.message ?? `HTTP ${response.status}`,\n }\n } catch {\n // Keep the synthetic errorBody above when the response body is not JSON.\n }\n }\n\n const retryAfterHeader = response.headers.get('Retry-After')\n const retryAfter = retryAfterHeader !== null ? Number.parseInt(retryAfterHeader, 10) : undefined\n const requestId = response.headers.get('X-Bz-Request-Id') ?? undefined\n\n return classifyError(errorBody, {\n ...(retryAfter !== undefined ? { retryAfter } : {}),\n ...(requestId !== undefined ? { requestId } : {}),\n })\n}\n\nfunction isRetryableRangeError(err: unknown): boolean {\n if (err instanceof B2Error || err instanceof NetworkError) return err.retryable\n if (hasRetryableFlag(err)) return err.retryable\n // Bare FetchTransport surfaces network failures as TypeError. Validation\n // failures use RangeValidationError above, so they never land here.\n return err instanceof TypeError\n}\n\nfunction hasRetryableFlag(err: unknown): err is { retryable: boolean } {\n return (\n typeof err === 'object' &&\n err !== null &&\n 'retryable' in err &&\n typeof (err as { retryable?: unknown }).retryable === 'boolean'\n )\n}\n"],"mappings":"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"}