@2018kguo/recto 0.1.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 (44) hide show
  1. package/dist/assets.d.ts +11 -6
  2. package/dist/assets.js +123 -42
  3. package/dist/document.d.ts +48 -2
  4. package/dist/document.js +109 -4
  5. package/dist/engine.d.ts +17 -9
  6. package/dist/engine.js +223 -45
  7. package/dist/errors.d.ts +4 -2
  8. package/dist/errors.js +4 -1
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.js +13 -0
  11. package/dist/pool-worker.d.ts +14 -0
  12. package/dist/pool-worker.js +65 -0
  13. package/dist/pool.d.ts +36 -0
  14. package/dist/pool.js +287 -0
  15. package/dist/ranged.d.ts +10 -0
  16. package/dist/ranged.js +38 -11
  17. package/package.json +2 -1
  18. package/wasm/FoxitDingbats.pfb +0 -0
  19. package/wasm/FoxitFixed.pfb +0 -0
  20. package/wasm/FoxitFixedBold.pfb +0 -0
  21. package/wasm/FoxitFixedBoldItalic.pfb +0 -0
  22. package/wasm/FoxitFixedItalic.pfb +0 -0
  23. package/wasm/FoxitSans.pfb +0 -0
  24. package/wasm/FoxitSansBold.pfb +0 -0
  25. package/wasm/FoxitSansBoldItalic.pfb +0 -0
  26. package/wasm/FoxitSansItalic.pfb +0 -0
  27. package/wasm/FoxitSerif.pfb +0 -0
  28. package/wasm/FoxitSerifBold.pfb +0 -0
  29. package/wasm/FoxitSerifBoldItalic.pfb +0 -0
  30. package/wasm/FoxitSerifItalic.pfb +0 -0
  31. package/wasm/FoxitSymbol.pfb +0 -0
  32. package/wasm/THIRD_PARTY_NOTICES +2 -158
  33. package/wasm/cmaps.deflate +0 -0
  34. package/wasm/pins.d.mts +1 -1
  35. package/wasm/pins.mjs +19 -3
  36. package/wasm/recto-more.wasm +0 -0
  37. package/wasm/recto-rare.wasm +0 -0
  38. package/wasm/recto-render.wasm +0 -0
  39. package/wasm/recto.gen.d.mts +2 -1
  40. package/wasm/recto.gen.mjs +118 -12
  41. package/wasm/recto.types.d.mts +46 -1
  42. package/wasm/recto.wasm +0 -0
  43. package/wasm/cmaps.brotli +0 -0
  44. package/wasm/fonts.bin +0 -0
package/dist/pool.js ADDED
@@ -0,0 +1,287 @@
1
+ // Pages read and drawn across worker threads, from one call per page. Each thread has its own
2
+ // engine and, the first time it's given a page of a document, its own copy of the file; a thread
3
+ // starts only when there's work and none is free, so calls made one at a time stay on one thread.
4
+ // No special headers: SharedArrayBuffer only saves a copy, where it's allowed.
5
+ //
6
+ // No call waits for ever on a thread that's gone: a thread reports a failed start, its death fails its
7
+ // call, and one that has said nothing by START_TIMEOUT_MS counts as dead. A call that's running can't be
8
+ // stopped, only the page budgets end it, or `close()`.
9
+ import { PdfError } from "./errors.js";
10
+ /**
11
+ * The pool's copy of a file. Shared memory where it's allowed (Node, or a page with COOP and COEP), so that
12
+ * telling each thread about the file doesn't copy it again before the thread copies it into its wasm.
13
+ */
14
+ function privateCopy(view) {
15
+ if (typeof SharedArrayBuffer !== "function") {
16
+ return view.slice();
17
+ }
18
+ const copy = new Uint8Array(new SharedArrayBuffer(view.byteLength));
19
+ copy.set(view);
20
+ return copy;
21
+ }
22
+ /** Longer than a thread's slowest fetch of its wasm (ASSET_TIMEOUT_MS), and its compile. */
23
+ const START_TIMEOUT_MS = 90_000;
24
+ const MAX_THREADS = 64;
25
+ function defaultThreads() {
26
+ const cores = globalThis.process?.getBuiltinModule?.("node:os")?.availableParallelism() ?? navigator.hardwareConcurrency ?? 2;
27
+ return Math.max(1, Math.min(8, cores - 1));
28
+ }
29
+ function failureOf(failure) {
30
+ if (failure.code) {
31
+ return new PdfError(failure.code, failure.message);
32
+ }
33
+ // A bad argument stays what it is on a single thread.
34
+ const type = failure.name === "RangeError" ? RangeError : failure.name === "TypeError" ? TypeError : Error;
35
+ return new type(failure.message);
36
+ }
37
+ const closedError = () => new PdfError("closed", "this document or pool is closed");
38
+ class Pool {
39
+ #limit;
40
+ #ceiling;
41
+ #assetBase;
42
+ #threads = [];
43
+ /** Started and not yet ready. */
44
+ #startingThreads = new Set();
45
+ #queue = [];
46
+ #nextFile = 0;
47
+ #nextId = 0;
48
+ #closed = false;
49
+ constructor(limit, assetBase) {
50
+ this.#limit = limit;
51
+ this.#ceiling = limit;
52
+ this.#assetBase = assetBase;
53
+ }
54
+ #spawn() {
55
+ const thread = { worker: null, running: null };
56
+ this.#startingThreads.add(thread);
57
+ let dead = false;
58
+ let waiting;
59
+ const onMessage = (message) => {
60
+ if (this.#closed) {
61
+ void thread.worker.terminate();
62
+ return;
63
+ }
64
+ if (message.startFailed) {
65
+ onDeath(message.failure.message, failureOf(message.failure));
66
+ return;
67
+ }
68
+ if (message.ready) {
69
+ clearTimeout(waiting);
70
+ this.#startingThreads.delete(thread);
71
+ this.#threads.push(thread);
72
+ // Idle, a thread doesn't keep a Node process alive; a call under way does.
73
+ thread.worker.unref?.();
74
+ }
75
+ else {
76
+ const task = thread.running;
77
+ thread.running = null;
78
+ thread.worker.unref?.();
79
+ // It'll need the file again: this was the call that was to open it.
80
+ if (!message.opened) {
81
+ task.file.on.delete(thread);
82
+ }
83
+ if (message.failure) {
84
+ task.reject(failureOf(message.failure));
85
+ }
86
+ else {
87
+ task.resolve(message.result);
88
+ }
89
+ }
90
+ this.#pump();
91
+ };
92
+ // In Node a failure is an `error` and then an `exit`.
93
+ const onDeath = (reason, typed) => {
94
+ if (dead || this.#closed) {
95
+ return;
96
+ }
97
+ dead = true;
98
+ clearTimeout(waiting);
99
+ void thread.worker?.terminate();
100
+ const failure = typed ?? new PdfError("crash", `a pool thread stopped: ${String(reason)}`, reason);
101
+ if (this.#startingThreads.delete(thread)) {
102
+ // It couldn't start, so another wouldn't either: run on what's left, or fail what's waiting.
103
+ this.#limit = this.#threads.length + this.#startingThreads.size;
104
+ if (this.#limit === 0) {
105
+ // The next call tries again: what stopped it, a missing file say, may be back by then.
106
+ this.#limit = this.#ceiling;
107
+ for (const task of this.#queue.splice(0)) {
108
+ task.reject(failure);
109
+ }
110
+ }
111
+ }
112
+ else {
113
+ this.#threads.splice(this.#threads.indexOf(thread), 1);
114
+ }
115
+ thread.running?.file.on.delete(thread);
116
+ thread.running?.reject(failure);
117
+ thread.running = null;
118
+ this.#pump();
119
+ };
120
+ waiting = setTimeout(() => onDeath(`it hadn't started after ${START_TIMEOUT_MS} ms`), START_TIMEOUT_MS);
121
+ waiting.unref?.();
122
+ try {
123
+ const threads = globalThis.process?.getBuiltinModule?.("node:worker_threads");
124
+ if (threads) {
125
+ const worker = new threads.Worker(new URL("./pool-worker.js", import.meta.url), { execArgv: [] });
126
+ worker.on("message", onMessage);
127
+ worker.on("messageerror", onDeath);
128
+ worker.on("error", onDeath);
129
+ worker.on("exit", onDeath);
130
+ thread.worker = worker;
131
+ }
132
+ else {
133
+ // Not `new URL("…", import.meta.url)`: a bundler would emit the file as it stands for that.
134
+ const here = import.meta.url;
135
+ const worker = new URL(here).origin === location.origin
136
+ ? // Written out in full: this is the form bundlers recognise, and they emit the worker for it.
137
+ new Worker(new URL("./pool-worker.js", import.meta.url), { type: "module" })
138
+ : // A worker's script has to be of this page's origin, so from a CDN it's one line that imports the
139
+ // real one, which the CDN has to allow with CORS, as it does the wasm.
140
+ // ponytail: the blob's URL lives as long as the page; one per thread started.
141
+ new Worker(URL.createObjectURL(new Blob([`import ${JSON.stringify(new URL("./pool-worker.js", here).href)};`], { type: "text/javascript" })), { type: "module" });
142
+ worker.onmessage = ({ data }) => onMessage(data);
143
+ worker.onmessageerror = () => onDeath("a message from it could not be read");
144
+ worker.onerror = (event) => {
145
+ event.preventDefault();
146
+ onDeath(event.message || "its script did not load");
147
+ };
148
+ thread.worker = worker;
149
+ }
150
+ const start = { assetBase: this.#assetBase };
151
+ thread.worker.postMessage(start);
152
+ }
153
+ catch (error) {
154
+ onDeath(error);
155
+ }
156
+ }
157
+ /** Gives each free thread work, the oldest call on a document it has if there is one. */
158
+ #pump() {
159
+ for (const thread of this.#threads) {
160
+ if (thread.running || this.#queue.length === 0) {
161
+ continue;
162
+ }
163
+ let at = this.#queue.findIndex((task) => task.file.on.has(thread));
164
+ if (at < 0) {
165
+ // Not a copy of the file for a call that a thread already holding it will take next.
166
+ const free = this.#threads.filter((other) => !other.running);
167
+ at = this.#queue.findIndex((task) => !free.some((other) => task.file.on.has(other)));
168
+ }
169
+ if (at < 0) {
170
+ continue;
171
+ }
172
+ const [task] = this.#queue.splice(at, 1);
173
+ thread.running = task;
174
+ thread.worker.ref?.();
175
+ const { file } = task;
176
+ const had = file.on.has(thread);
177
+ const open = had ? {} : { open: { bytes: file.bytes, password: file.password } };
178
+ file.on.add(thread);
179
+ try {
180
+ thread.worker.postMessage({ ...task.request, id: this.#nextId++, ...open });
181
+ }
182
+ catch (error) {
183
+ // An argument that can't cross. Nothing reached the thread.
184
+ thread.running = null;
185
+ thread.worker.unref?.();
186
+ if (!had) {
187
+ file.on.delete(thread);
188
+ }
189
+ task.reject(error);
190
+ // Nothing will come back from the thread to take the next call.
191
+ this.#pump();
192
+ return;
193
+ }
194
+ }
195
+ const wanted = Math.min(this.#limit - this.#threads.length, this.#queue.length) - this.#startingThreads.size;
196
+ for (let i = 0; i < wanted; i++) {
197
+ this.#spawn();
198
+ }
199
+ }
200
+ #call(file, method, args) {
201
+ if (file.closed || this.#closed) {
202
+ return Promise.reject(closedError());
203
+ }
204
+ return new Promise((resolve, reject) => {
205
+ this.#queue.push({ request: { doc: file.id, method, args }, file, resolve, reject });
206
+ this.#pump();
207
+ });
208
+ }
209
+ async open(input, options = {}) {
210
+ const view = ArrayBuffer.isView(input) ? input : new Uint8Array(input);
211
+ const file = {
212
+ id: this.#nextFile++,
213
+ bytes: privateCopy(view),
214
+ password: options.password,
215
+ closed: false,
216
+ on: new Set(),
217
+ };
218
+ const call = (method, ...args) => this.#call(file, method, args);
219
+ const pageCount = (await call("pageCount"));
220
+ const close = async () => {
221
+ if (file.closed) {
222
+ return;
223
+ }
224
+ file.closed = true;
225
+ this.#queue = this.#queue.filter((task) => {
226
+ if (task.file === file) {
227
+ task.reject(closedError());
228
+ }
229
+ return task.file !== file;
230
+ });
231
+ // Each thread frees it when it gets there, past any call of it under way, which still answers.
232
+ for (const thread of file.on) {
233
+ thread.worker.postMessage({ id: this.#nextId++, doc: file.id, method: "close", args: [] });
234
+ }
235
+ file.on.clear();
236
+ };
237
+ return {
238
+ pageCount,
239
+ pageInfo: (index) => call("pageInfo", index),
240
+ text: (index) => call("text", index),
241
+ objects: (index) => call("objects", index),
242
+ annotations: (index) => call("annotations", index),
243
+ outline: () => call("outline"),
244
+ render: (index, renderOptions) => {
245
+ if (renderOptions?.signal) {
246
+ return Promise.reject(new TypeError("a pool's render takes no signal: a call that's running can't be stopped. pool.close() ends all of them."));
247
+ }
248
+ return call("render", index, { width: renderOptions?.width, maxHeight: renderOptions?.maxHeight });
249
+ },
250
+ close,
251
+ [Symbol.asyncDispose]: close,
252
+ };
253
+ }
254
+ stats() {
255
+ return { threads: this.#threads.length, queued: this.#queue.length };
256
+ }
257
+ close() {
258
+ if (this.#closed) {
259
+ return;
260
+ }
261
+ this.#closed = true;
262
+ for (const thread of [...this.#threads, ...this.#startingThreads]) {
263
+ thread.running?.reject(closedError());
264
+ void thread.worker?.terminate();
265
+ }
266
+ for (const task of this.#queue.splice(0)) {
267
+ task.reject(closedError());
268
+ }
269
+ }
270
+ [Symbol.dispose]() {
271
+ this.close();
272
+ }
273
+ }
274
+ export function startPool(options) {
275
+ const threads = options.threads ?? defaultThreads();
276
+ if (!(Number.isInteger(threads) && threads >= 1 && threads <= MAX_THREADS)) {
277
+ throw new RangeError(`threads must be a whole number from 1 to ${MAX_THREADS}, not ${threads}`);
278
+ }
279
+ if (!globalThis.process?.getBuiltinModule?.("node:worker_threads") && typeof Worker === "undefined") {
280
+ throw new PdfError("wrong-thread", "a pool needs worker threads, which this thread can't start");
281
+ }
282
+ // A thread's location isn't this one's, so a relative URL has to be resolved here.
283
+ const assetBase = options.assetBase !== undefined && typeof location !== "undefined"
284
+ ? new URL(options.assetBase, location.href).href
285
+ : options.assetBase;
286
+ return new Pool(threads, assetBase);
287
+ }
package/dist/ranged.d.ts CHANGED
@@ -18,6 +18,14 @@ export type PdfRangedOpenOptions = {
18
18
  signal?: AbortSignal;
19
19
  /** How long one fetch round may block the thread (code "timeout"). Default 30 s. */
20
20
  timeoutMs?: number;
21
+ /**
22
+ * Wall time for the whole read, from the first fetch on, in place of counting rounds (code "timeout").
23
+ * A fetch in flight is cut when it runs out, and none starts after; that bounds the network waits, not
24
+ * the parsing between them. What's already fetched reads until a read fails, after which every call
25
+ * on the document throws that failure: reopen to go on. In WebKit a response that keeps trickling is
26
+ * not cut. Default: no limit.
27
+ */
28
+ totalTimeoutMs?: number;
21
29
  };
22
30
  export type PdfRangedStats = {
23
31
  /** Bytes received from the source, across every request. */
@@ -44,6 +52,7 @@ type Answer = {
44
52
  bytes: Uint8Array;
45
53
  length: number;
46
54
  etag: string | null;
55
+ status: number;
47
56
  };
48
57
  /** Fetches `ranges`; `limit` is how many bytes may still arrive. */
49
58
  type Fetch = (ranges: Range[], timeoutMs: number, limit: number) => Answer[];
@@ -58,6 +67,7 @@ declare class RangeLoader {
58
67
  chunkBytes: number;
59
68
  maxBytes: number;
60
69
  timeoutMs: number;
70
+ totalTimeoutMs: number;
61
71
  signal: AbortSignal | undefined;
62
72
  });
63
73
  stats(): PdfRangedStats;
package/dist/ranged.js CHANGED
@@ -50,6 +50,8 @@ class RangeLoader {
50
50
  #fetch;
51
51
  #maxBytes;
52
52
  #timeoutMs;
53
+ #totalTimeoutMs;
54
+ #deadline;
53
55
  #signal;
54
56
  constructor(fetch, options) {
55
57
  this.#fetch = fetch;
@@ -57,6 +59,8 @@ class RangeLoader {
57
59
  this.#readAhead = options.chunkBytes;
58
60
  this.#maxBytes = options.maxBytes;
59
61
  this.#timeoutMs = options.timeoutMs;
62
+ this.#totalTimeoutMs = options.totalTimeoutMs;
63
+ this.#deadline = performance.now() + options.totalTimeoutMs;
60
64
  this.#signal = options.signal;
61
65
  }
62
66
  stats() {
@@ -101,20 +105,35 @@ class RangeLoader {
101
105
  if (this.length > 0 && this.#bytesFetched + wanted > this.#maxBytes) {
102
106
  throw tooLarge(this.#maxBytes);
103
107
  }
108
+ const passedTotal = `the ranged read passed its ${this.#totalTimeoutMs} ms (totalTimeoutMs)`;
109
+ const left = this.#deadline - performance.now();
110
+ if (left <= 0) {
111
+ throw new PdfError("timeout", passedTotal);
112
+ }
104
113
  this.#rounds += 1;
105
- const answers = this.#fetch(runs, this.#timeoutMs, this.#maxBytes - this.#bytesFetched);
114
+ const passesTotal = left < this.#timeoutMs;
115
+ let answers;
116
+ try {
117
+ answers = this.#fetch(runs, passesTotal ? Math.ceil(left) : this.#timeoutMs, this.#maxBytes - this.#bytesFetched);
118
+ }
119
+ catch (error) {
120
+ if (passesTotal && error.code === "timeout") {
121
+ throw new PdfError("timeout", passedTotal, error);
122
+ }
123
+ throw error;
124
+ }
106
125
  for (const answer of answers) {
107
126
  this.#requests += 1;
108
127
  this.#bytesFetched += answer.bytes.byteLength;
109
128
  if (this.length > 0 && answer.length !== this.length) {
110
- throw new PdfError("fetch-failed", `the file changed size during the ranged read (${this.length} bytes, now ${answer.length})`);
129
+ throw new PdfError("fetch-failed", `the file changed size during the ranged read (${this.length} bytes, now ${answer.length})`, undefined, answer.status);
111
130
  }
112
131
  // Before anything is sized from it: the total is whatever the server said.
113
132
  if (answer.length > MAX_FILE_BYTES) {
114
133
  throw new PdfError("too-large", `a ranged read holds files up to ${MAX_FILE_BYTES} bytes (this one is ${answer.length}); use open() or a smaller file`);
115
134
  }
116
135
  if (this.#etag && answer.etag && answer.etag !== this.#etag) {
117
- throw new PdfError("fetch-failed", "the file was replaced during the ranged read (its ETag changed)");
136
+ throw new PdfError("fetch-failed", "the file was replaced during the ranged read (its ETag changed)", undefined, answer.status);
118
137
  }
119
138
  this.#etag ??= answer.etag;
120
139
  this.length = answer.length;
@@ -217,13 +236,14 @@ class RangeLoader {
217
236
  */
218
237
  export function startRangeLoader(source, options = {}) {
219
238
  const positive = (name, value) => {
220
- if (value !== undefined && !(value > 0)) {
239
+ if (value !== undefined && !(typeof value === "number" && value > 0)) {
221
240
  throw new RangeError(`${name} must be above 0 (got ${value})`);
222
241
  }
223
242
  };
224
243
  positive("chunkBytes", options.chunkBytes);
225
244
  positive("maxBytes", options.maxBytes);
226
245
  positive("timeoutMs", options.timeoutMs);
246
+ positive("totalTimeoutMs", options.totalTimeoutMs);
227
247
  if (options.chunkBytes !== undefined && !Number.isSafeInteger(options.chunkBytes)) {
228
248
  throw new RangeError(`chunkBytes must be a whole number (got ${options.chunkBytes})`);
229
249
  }
@@ -235,6 +255,7 @@ export function startRangeLoader(source, options = {}) {
235
255
  chunkBytes,
236
256
  maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,
237
257
  timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
258
+ totalTimeoutMs: options.totalTimeoutMs ?? Number.POSITIVE_INFINITY,
238
259
  signal: options.signal,
239
260
  });
240
261
  loader.fetchChunks(0, HEAD_CHUNKS);
@@ -281,6 +302,7 @@ function fetcherFor(source) {
281
302
  bytes: bytes.subarray(start, Math.min(end, bytes.byteLength)),
282
303
  length: bytes.byteLength,
283
304
  etag: null,
305
+ status: 206,
284
306
  }));
285
307
  }
286
308
  const url = source;
@@ -303,24 +325,25 @@ function answerOf(range, status, contentRange, etag, bytes, limit) {
303
325
  if (bytes.byteLength > limit) {
304
326
  throw new PdfError("too-large", `the server does not answer Range requests, and the whole file is past the ${limit} bytes left of maxBytes`);
305
327
  }
306
- return { start: 0, bytes, length: bytes.byteLength, etag };
328
+ return { start: 0, bytes, length: bytes.byteLength, etag, status };
307
329
  }
308
330
  if (status !== 206) {
309
- throw new PdfError("fetch-failed", `the ranged read failed (HTTP ${status})`);
331
+ throw new PdfError("fetch-failed", `the ranged read failed (HTTP ${status})`, undefined, status);
310
332
  }
311
333
  const match = /^bytes (\d+)-\d+\/(\d+)$/u.exec(contentRange?.trim() ?? "");
312
334
  if (!match) {
313
335
  throw new PdfError("fetch-failed", "the server's 206 answer has no usable Content-Range (with a total length). " +
314
- "A cross-origin server must also send Access-Control-Expose-Headers: Content-Range.");
336
+ "A cross-origin server must also send Access-Control-Expose-Headers: Content-Range.", undefined, status);
315
337
  }
316
338
  const start = Number(match[1]);
317
339
  const length = Number(match[2]);
318
340
  // A short or shifted answer (a proxy capping ranges) would store a partial chunk.
319
341
  if (start !== range.start || bytes.byteLength !== Math.min(range.end, length) - range.start) {
320
- throw new PdfError("fetch-failed", `the server answered a different range than asked (asked ${range.start}-${range.end - 1}, got ${bytes.byteLength} bytes from ${start})`);
342
+ throw new PdfError("fetch-failed", `the server answered a different range than asked (asked ${range.start}-${range.end - 1}, got ${bytes.byteLength} bytes from ${start})`, undefined, status);
321
343
  }
322
- return { start, bytes, length, etag };
344
+ return { start, bytes, length, etag, status };
323
345
  }
346
+ const NO_ANSWER = "the ranged read failed: the network, or a cross-origin server that does not allow it";
324
347
  function fetchOverXhr(url, ranges, timeoutMs, limit) {
325
348
  const deadline = performance.now() + timeoutMs;
326
349
  return ranges.map((range) => {
@@ -336,7 +359,11 @@ function fetchOverXhr(url, ranges, timeoutMs, limit) {
336
359
  const timedOut = error.name === "TimeoutError";
337
360
  throw new PdfError(timedOut ? "timeout" : "fetch-failed", timedOut
338
361
  ? `the ranged read timed out after ${timeoutMs} ms`
339
- : "the ranged read failed: the network, or a cross-origin server that does not allow it", error);
362
+ : NO_ANSWER, error);
363
+ }
364
+ // Firefox reports a refused cross-origin read as status 0, not a throw.
365
+ if (xhr.status === 0) {
366
+ throw new PdfError("fetch-failed", NO_ANSWER);
340
367
  }
341
368
  return answerOf(range, xhr.status, xhr.getResponseHeader("Content-Range"), xhr.getResponseHeader("ETag"), new Uint8Array(xhr.response), limit);
342
369
  });
@@ -364,7 +391,7 @@ async function get(m, signal) {
364
391
  }
365
392
  const parts = [];
366
393
  let size = 0;
367
- for await (const part of response.body) {
394
+ for await (const part of response.body ?? []) {
368
395
  size += part.byteLength;
369
396
  if (size > m.limit) return { ...reply, overLimit: true, bytes: new ArrayBuffer(0) };
370
397
  parts.push(part);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2018kguo/recto",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "A PDF engine for JavaScript: text with boxes, page objects, annotations and rendering, in Rust compiled to WebAssembly.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "types": "./dist/index.d.ts",
27
27
  "default": "./dist/index.js"
28
28
  },
29
+ "./wasm/*": "./wasm/*",
29
30
  "./package.json": "./package.json"
30
31
  },
31
32
  "files": [
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file