abysshub 0.1.1-dev.4 → 0.1.1-dev.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,7 +103,7 @@ const paths = await run.save("output"); // ["output/charts/a.png", "output/outpu
103
103
  - **A string passes through** as an `https` URL or an upload id. A multi-file Field takes a list, which may mix all three kinds.
104
104
  - **Before the press,** each file is uploaded with `POST /v1/widgets/{widget}/uploads`, then sent to storage, in parallel, and its id goes in the input. An upload refusal (`held_input_*`) raises before any press. A retried press reuses the ids; it never uploads again.
105
105
  - **`await run.save(dir)`** writes every output file under `dir` at its `path`, making folders, and answers the paths it wrote.
106
- - **Each of `run.output_files`** has `await f.save(path)` and `await f.read()`, which answers a `Uint8Array`. An expired URL is refreshed once, by reading the run again with `GET /v1/runs/{id}`.
106
+ - **Each of `run.output_files`** has `await f.save(path)` and `await f.read()`, which answers a `Uint8Array`. An expired URL is refreshed once, by reading the run again with `GET /v1/runs/{id}`. A file of 64 MiB or more downloads as 16 parallel byte ranges of its one URL.
107
107
 
108
108
  ## Errors
109
109
 
package/dist/run.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { RunData, RunError, RunFileData, RunStatus } from "./types.js";
2
2
  /** Reads the run again with `GET /v1/runs/{id}`. */
3
3
  export type Reread = () => Promise<RunData>;
4
+ /** A file this many bytes or larger downloads as parallel byte ranges: 64 MiB. */
5
+ export declare const RANGED_FROM: number;
6
+ /** How many parallel byte ranges a large file downloads as. */
7
+ export declare const RANGES = 16;
4
8
  /** A run, with `/v1`'s names. */
5
9
  export declare class Run {
6
10
  #private;
@@ -26,7 +30,12 @@ export declare class RunFile {
26
30
  content_type: string;
27
31
  url: string;
28
32
  constructor(data: RunFileData, refresh: () => Promise<void>);
29
- /** Downloads the file. An expired `url` is refreshed once, by reading the run again. */
33
+ /**
34
+ * Downloads the file. An expired `url` is refreshed once, by reading the run again. A
35
+ * file of `RANGED_FROM` bytes or more downloads as `RANGES` parallel byte ranges of its
36
+ * one `url`: the first goes alone, and once it answers 206 the others go together. A
37
+ * storage that ignores `Range` answers the whole file to the first, which is kept.
38
+ */
30
39
  read(): Promise<Uint8Array>;
31
40
  /** Downloads the file to `path`, making its folders, and answers `path`. */
32
41
  save(path: string): Promise<string>;
package/dist/run.js CHANGED
@@ -1,5 +1,19 @@
1
1
  import { AbyssError } from "./error.js";
2
2
  import { nodeFS, nodePath } from "./node.js";
3
+ /** A file this many bytes or larger downloads as parallel byte ranges: 64 MiB. */
4
+ export const RANGED_FROM = 64 * 1024 * 1024;
5
+ /** How many parallel byte ranges a large file downloads as. */
6
+ export const RANGES = 16;
7
+ /** The byte ranges a file of `size` bytes downloads as, or null when it downloads whole. */
8
+ function ranges(size) {
9
+ if (size < RANGED_FROM)
10
+ return null;
11
+ const step = Math.ceil(size / RANGES);
12
+ const parts = [];
13
+ for (let first = 0; first < size; first += step)
14
+ parts.push([first, Math.min(first + step, size) - 1]);
15
+ return parts;
16
+ }
3
17
  /** A run, with `/v1`'s names. */
4
18
  export class Run {
5
19
  id;
@@ -67,23 +81,78 @@ export class RunFile {
67
81
  this.url = data.url;
68
82
  this.#refresh = refresh;
69
83
  }
70
- /** Downloads the file. An expired `url` is refreshed once, by reading the run again. */
84
+ /**
85
+ * Downloads the file. An expired `url` is refreshed once, by reading the run again. A
86
+ * file of `RANGED_FROM` bytes or more downloads as `RANGES` parallel byte ranges of its
87
+ * one `url`: the first goes alone, and once it answers 206 the others go together. A
88
+ * storage that ignores `Range` answers the whole file to the first, which is kept.
89
+ */
71
90
  async read() {
72
- let response = await fetch(this.url);
73
- if (response.status === 403) {
74
- await response.body?.cancel();
75
- await this.#refresh();
76
- response = await fetch(this.url);
91
+ const parts = ranges(this.size);
92
+ const stop = new AbortController();
93
+ try {
94
+ let response = await this.#get(parts?.[0], stop.signal);
95
+ if (response.status === 403) {
96
+ await response.body?.cancel();
97
+ await this.#refresh();
98
+ response = await this.#get(parts?.[0], stop.signal);
99
+ }
100
+ if (!response.ok) {
101
+ await response.body?.cancel();
102
+ throw this.#answered(response.status);
103
+ }
104
+ if (!parts || response.status !== 206)
105
+ return new Uint8Array(await response.arrayBuffer());
106
+ const first = response;
107
+ const reads = parts.map(async (part, index) => this.#part(index === 0 ? first : await this.#get(part, stop.signal), part));
108
+ try {
109
+ const bytes = new Uint8Array(this.size);
110
+ for (const [index, part] of (await Promise.all(reads)).entries())
111
+ bytes.set(part, parts[index][0]);
112
+ return bytes;
113
+ }
114
+ catch (error) {
115
+ stop.abort();
116
+ await Promise.allSettled(reads);
117
+ throw error;
118
+ }
77
119
  }
78
- if (!response.ok) {
120
+ catch (error) {
121
+ if (error instanceof AbyssError)
122
+ throw error;
123
+ throw new AbyssError({
124
+ code: "connection_error",
125
+ message: "The connection to storage dropped during a download.",
126
+ cause: error,
127
+ });
128
+ }
129
+ }
130
+ /** Fetches the file from storage, without the API key: whole, or the byte range `part`. */
131
+ #get(part, signal) {
132
+ return fetch(this.url, { signal, ...(part ? { headers: { Range: `bytes=${part[0]}-${part[1]}` } } : {}) });
133
+ }
134
+ /** The bytes of one range, which must answer 206 with exactly the range's length. */
135
+ async #part(response, [first, last]) {
136
+ if (response.status !== 206) {
79
137
  await response.body?.cancel();
138
+ throw this.#answered(response.status);
139
+ }
140
+ const bytes = new Uint8Array(await response.arrayBuffer());
141
+ if (bytes.length !== last - first + 1) {
80
142
  throw new AbyssError({
81
143
  code: "unexpected_response",
82
- message: `The download of ${this.path} answered ${response.status}.`,
144
+ message: `The download of ${this.path} answered ${bytes.length} bytes for the range ${first}-${last}.`,
83
145
  status: response.status,
84
146
  });
85
147
  }
86
- return new Uint8Array(await response.arrayBuffer());
148
+ return bytes;
149
+ }
150
+ #answered(status) {
151
+ return new AbyssError({
152
+ code: "unexpected_response",
153
+ message: `The download of ${this.path} answered ${status}.`,
154
+ status,
155
+ });
87
156
  }
88
157
  /** Downloads the file to `path`, making its folders, and answers `path`. */
89
158
  async save(path) {
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's version, written from package.json by scripts/version.js at build. */
2
- export declare const VERSION = "0.1.1-dev.4";
2
+ export declare const VERSION = "0.1.1-dev.6";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's version, written from package.json by scripts/version.js at build. */
2
- export const VERSION = "0.1.1-dev.4";
2
+ export const VERSION = "0.1.1-dev.6";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abysshub",
3
- "version": "0.1.1-dev.4",
3
+ "version": "0.1.1-dev.6",
4
4
  "description": "The official JavaScript library for the Abyss API.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/Abysshub/abysshub-sdk/tree/main/js#readme",