@build-qube/papyra 0.0.3

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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Size-bounded LRU for rendered pages.
3
+ *
4
+ * Worth having because a render is never cheap: on a large CAD drawing every page costs
5
+ * >=93ms no matter how small the output, since the cost is per-draw-call rather than
6
+ * per-pixel (see docs/spike-results.md). A viewer re-renders the same page constantly —
7
+ * scrolling back, returning from a zoom, promoting a thumbnail — and each of those is a
8
+ * fresh 93ms without a cache.
9
+ *
10
+ * Bounded by **bytes**, not entries, because page bitmaps span three orders of
11
+ * magnitude: a 160px thumbnail of an ARCH-E sheet is 0.07 MB and the same page at
12
+ * 2000px is 11.4 MB. An entry-count limit would either hold almost nothing or hundreds
13
+ * of megabytes depending on the document.
14
+ */
15
+ export interface CacheStats {
16
+ /** Bytes currently held. */
17
+ bytes: number;
18
+ entries: number;
19
+ hits: number;
20
+ misses: number;
21
+ /** Entries dropped to stay under budget. */
22
+ evictions: number;
23
+ }
24
+ export declare class RenderCache<T> {
25
+ #private;
26
+ constructor(budgetBytes: number, sizeOf: (value: T) => number);
27
+ get enabled(): boolean;
28
+ get(key: string): T | undefined;
29
+ set(key: string, value: T): void;
30
+ clear(): void;
31
+ get stats(): CacheStats;
32
+ }
33
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,WAAW,CAAC,CAAC;;IAUxB,YAAY,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,EAG5D;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAW9B;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAc/B;IAcD,KAAK,IAAI,IAAI,CAGZ;IAED,IAAI,KAAK,IAAI,UAAU,CAQtB;CACF"}
package/dist/cache.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Size-bounded LRU for rendered pages.
3
+ *
4
+ * Worth having because a render is never cheap: on a large CAD drawing every page costs
5
+ * >=93ms no matter how small the output, since the cost is per-draw-call rather than
6
+ * per-pixel (see docs/spike-results.md). A viewer re-renders the same page constantly —
7
+ * scrolling back, returning from a zoom, promoting a thumbnail — and each of those is a
8
+ * fresh 93ms without a cache.
9
+ *
10
+ * Bounded by **bytes**, not entries, because page bitmaps span three orders of
11
+ * magnitude: a 160px thumbnail of an ARCH-E sheet is 0.07 MB and the same page at
12
+ * 2000px is 11.4 MB. An entry-count limit would either hold almost nothing or hundreds
13
+ * of megabytes depending on the document.
14
+ */
15
+ export class RenderCache {
16
+ #budget;
17
+ #sizeOf;
18
+ /** Insertion order is the LRU order: re-inserting on read moves an entry to the end. */
19
+ #entries = new Map();
20
+ #bytes = 0;
21
+ #hits = 0;
22
+ #misses = 0;
23
+ #evictions = 0;
24
+ constructor(budgetBytes, sizeOf) {
25
+ this.#budget = Math.max(0, budgetBytes);
26
+ this.#sizeOf = sizeOf;
27
+ }
28
+ get enabled() {
29
+ return this.#budget > 0;
30
+ }
31
+ get(key) {
32
+ const value = this.#entries.get(key);
33
+ if (value === undefined) {
34
+ this.#misses++;
35
+ return undefined;
36
+ }
37
+ // Move to the most-recently-used end.
38
+ this.#entries.delete(key);
39
+ this.#entries.set(key, value);
40
+ this.#hits++;
41
+ return value;
42
+ }
43
+ set(key, value) {
44
+ if (!this.enabled)
45
+ return;
46
+ const size = this.#sizeOf(value);
47
+ // An item larger than the whole budget would evict everything and still not fit.
48
+ if (size > this.#budget)
49
+ return;
50
+ const existing = this.#entries.get(key);
51
+ if (existing !== undefined) {
52
+ this.#bytes -= this.#sizeOf(existing);
53
+ this.#entries.delete(key);
54
+ }
55
+ this.#entries.set(key, value);
56
+ this.#bytes += size;
57
+ this.#evict();
58
+ }
59
+ #evict() {
60
+ while (this.#bytes > this.#budget) {
61
+ // Map iteration starts at the least-recently-used entry.
62
+ const oldest = this.#entries.keys().next();
63
+ if (oldest.done)
64
+ return;
65
+ const value = this.#entries.get(oldest.value);
66
+ if (value !== undefined)
67
+ this.#bytes -= this.#sizeOf(value);
68
+ this.#entries.delete(oldest.value);
69
+ this.#evictions++;
70
+ }
71
+ }
72
+ clear() {
73
+ this.#entries.clear();
74
+ this.#bytes = 0;
75
+ }
76
+ get stats() {
77
+ return {
78
+ bytes: this.#bytes,
79
+ entries: this.#entries.size,
80
+ hits: this.#hits,
81
+ misses: this.#misses,
82
+ evictions: this.#evictions,
83
+ };
84
+ }
85
+ }
86
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { RenderedPage } from './types.js';
2
+ /**
3
+ * Wrap a rendered page as `ImageData`, ready for `putImageData`.
4
+ *
5
+ * This copies. It has to: on the wasm build the pixels live in the module's shared
6
+ * linear memory, and `ImageData` refuses a `SharedArrayBuffer`-backed view. Copying
7
+ * unconditionally keeps both runtimes on the same path.
8
+ */
9
+ export declare function toImageData(page: RenderedPage): ImageData;
10
+ /** Paint a rendered page onto a canvas, resizing the canvas to match. */
11
+ export declare function paintToCanvas(page: RenderedPage, canvas: HTMLCanvasElement | OffscreenCanvas): void;
12
+ //# sourceMappingURL=canvas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,CAMzD;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,iBAAiB,GAAG,eAAe,GAC1C,IAAI,CASN"}
package/dist/canvas.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Wrap a rendered page as `ImageData`, ready for `putImageData`.
3
+ *
4
+ * This copies. It has to: on the wasm build the pixels live in the module's shared
5
+ * linear memory, and `ImageData` refuses a `SharedArrayBuffer`-backed view. Copying
6
+ * unconditionally keeps both runtimes on the same path.
7
+ */
8
+ export function toImageData(page) {
9
+ return new ImageData(Uint8ClampedArray.from(page.data), page.width, page.height);
10
+ }
11
+ /** Paint a rendered page onto a canvas, resizing the canvas to match. */
12
+ export function paintToCanvas(page, canvas) {
13
+ canvas.width = page.width;
14
+ canvas.height = page.height;
15
+ const ctx = canvas.getContext('2d');
16
+ if (!ctx)
17
+ throw new Error('papyra: could not get a 2d canvas context');
18
+ ctx.putImageData(toImageData(page), 0, 0);
19
+ }
20
+ //# sourceMappingURL=canvas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,IAAkB;IAC5C,OAAO,IAAI,SAAS,CAClB,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EACjC,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,MAAM,CACZ,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAC3B,IAAkB,EAClB,MAA2C;IAE3C,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IAC1B,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAG1B,CAAC;IACT,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IACvE,GAAG,CAAC,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { PdfDocument as NativeDocument } from '@build-qube/papyra-native';
2
+ import { type CacheStats } from './cache.js';
3
+ import { type JobHandle } from './scheduler.js';
4
+ import type { OpenOptions, PageSize, PdfSource, RenderedPage, RenderOptions, StreamedPage, StreamOptions } from './types.js';
5
+ /** A queued render, plus whether it was served from cache without rendering at all. */
6
+ export interface RenderHandle extends JobHandle<RenderedPage> {
7
+ readonly cached: boolean;
8
+ }
9
+ /** Open a PDF from bytes, an `ArrayBuffer`, a `Blob`, or a `File`. */
10
+ export declare function open(source: PdfSource, options?: OpenOptions): Promise<Document>;
11
+ export declare class Document {
12
+ #private;
13
+ /** @internal — construct via {@link open}. */
14
+ constructor(inner: NativeDocument, options?: OpenOptions);
15
+ /** Rendered pages held for reuse, and how often that has paid off. */
16
+ get cache(): CacheStats;
17
+ /** Drop every cached page. */
18
+ clearCache(): void;
19
+ /** Pages queued but not yet started, pages rendering, and the longest wait so far. */
20
+ get queued(): {
21
+ pending: number;
22
+ running: number;
23
+ oldestWaitMs: number;
24
+ };
25
+ get pageCount(): number;
26
+ /** Page dimensions in PDF points (1/72 inch). */
27
+ pageSize(index: number): PageSize;
28
+ /** Render a single page. Never blocks the event loop. */
29
+ renderPage(index: number, options?: RenderOptions): Promise<RenderedPage>;
30
+ /**
31
+ * Render a page, returning a handle you can reprioritise or drop.
32
+ *
33
+ * This is the viewer path: as the user scrolls, promote what came into view and
34
+ * demote what left, without re-submitting or duplicating work. Requests for the same
35
+ * page at the same size coalesce into one render, taking the most urgent priority
36
+ * asked for.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const jobs = visible.map((p) => doc.render(p, { fitWidth: 1600, priority: 0 }));
41
+ * onScroll(() => {
42
+ * for (const [i, job] of jobs.entries()) job.setPriority(distanceFromViewport(i));
43
+ * });
44
+ * ```
45
+ */
46
+ render(index: number, options?: RenderOptions): RenderHandle;
47
+ /**
48
+ * Render `[start, end)`.
49
+ *
50
+ * The concurrency strategy differs by runtime, for reasons measured in
51
+ * `docs/spike-results.md`:
52
+ *
53
+ * - **native** — one async task with rayon inside. Fastest, and independent of
54
+ * `UV_THREADPOOL_SIZE`, which we cannot set from inside an addon.
55
+ * - **wasm** — per-page async tasks. rayon's workers there are Web Workers that the
56
+ * JS event loop has to create, so a rayon batch cannot be driven from a blocked
57
+ * thread; per-page tasks map onto the runtime's own worker pool instead.
58
+ */
59
+ renderPages(start: number, end: number, options?: RenderOptions): Promise<RenderedPage[]>;
60
+ /**
61
+ * Render pages as an async iterable, yielding each as soon as it finishes.
62
+ *
63
+ * At most `concurrency` renders are in flight, so memory stays bounded — 23 letter
64
+ * pages at 150 DPI is ~193 MB of RGBA if you hold them all. Breaking out of the loop
65
+ * stops scheduling further work, which makes "thumbnails for the visible rows only"
66
+ * cheap.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * for await (const { page, bitmap } of doc.stream({ dpi: 48, order: visible })) {
71
+ * paint(page, bitmap);
72
+ * }
73
+ * ```
74
+ */
75
+ stream(options?: StreamOptions): AsyncGenerator<StreamedPage>;
76
+ /** How many pages this document will render at once. */
77
+ get concurrency(): number;
78
+ }
79
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,IAAI,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAAE,KAAK,UAAU,EAAe,MAAM,YAAY,CAAC;AAO1D,OAAO,EAAoB,KAAK,SAAS,EAAa,MAAM,gBAAgB,CAAC;AAE7E,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AAcpB,uFAAuF;AACvF,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,YAAY,CAAC;IAC3D,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,sEAAsE;AACtE,wBAAsB,IAAI,CACxB,MAAM,EAAE,SAAS,EACjB,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAQnB;AAED,qBAAa,QAAQ;;IAMnB,8CAA8C;IAC9C,YAAY,KAAK,EAAE,cAAc,EAAE,OAAO,GAAE,WAAgB,EAU3D;IAED,sEAAsE;IACtE,IAAI,KAAK,IAAI,UAAU,CAEtB;IAED,8BAA8B;IAC9B,UAAU,IAAI,IAAI,CAEjB;IAED,sFAAsF;IACtF,IAAI,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAMvE;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAEhC;IAED,yDAAyD;IACnD,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CAEvB;IAED;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,YAAY,CAgC/D;IA4BD;;;;;;;;;;;OAWG;IACG,WAAW,CACf,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,EAAE,CAAC,CAoBzB;IAED;;;;;;;;;;;;;;OAcG;IACI,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,cAAc,CAAC,YAAY,CAAC,CA2CvE;IAED,wDAAwD;IACxD,IAAI,WAAW,IAAI,MAAM,CAExB;CACF"}
@@ -0,0 +1,255 @@
1
+ import { PdfDocument as NativeDocument } from '@build-qube/papyra-native';
2
+ import { RenderCache } from './cache.js';
3
+ import { currentRuntime, hardwareConcurrency, init, MAX_WASM_CONCURRENCY, } from './runtime.js';
4
+ import { DEFAULT_PRIORITY, Scheduler } from './scheduler.js';
5
+ import { toBytes } from './source.js';
6
+ const DEFAULT_DPI = 72;
7
+ /**
8
+ * Refuse renders large enough to take down the tab.
9
+ *
10
+ * 100 MP is ~400 MB of RGBA — beyond this you almost certainly meant `fitWidth`.
11
+ */
12
+ const MAX_PIXELS = 100_000_000;
13
+ /** Default bytes of rendered pages to keep. Roughly eleven 2000px ARCH-E sheets. */
14
+ const DEFAULT_CACHE_BYTES = 128 * 1024 * 1024;
15
+ /** Open a PDF from bytes, an `ArrayBuffer`, a `Blob`, or a `File`. */
16
+ export async function open(source, options = {}) {
17
+ init();
18
+ const bytes = await toBytes(source);
19
+ const inner = options.password === undefined
20
+ ? NativeDocument.load(bytes)
21
+ : NativeDocument.loadWithPassword(bytes, options.password);
22
+ return new Document(inner, options);
23
+ }
24
+ export class Document {
25
+ #inner;
26
+ #scheduler;
27
+ #limit;
28
+ #cache;
29
+ /** @internal — construct via {@link open}. */
30
+ constructor(inner, options = {}) {
31
+ this.#inner = inner;
32
+ this.#limit = Math.max(1, options.concurrency ?? defaultConcurrency());
33
+ this.#scheduler = new Scheduler(this.#limit, {
34
+ yieldToUrgent: options.yieldToUrgent,
35
+ });
36
+ this.#cache = new RenderCache(options.cacheBytes ?? DEFAULT_CACHE_BYTES, (page) => page.data.byteLength);
37
+ }
38
+ /** Rendered pages held for reuse, and how often that has paid off. */
39
+ get cache() {
40
+ return this.#cache.stats;
41
+ }
42
+ /** Drop every cached page. */
43
+ clearCache() {
44
+ this.#cache.clear();
45
+ }
46
+ /** Pages queued but not yet started, pages rendering, and the longest wait so far. */
47
+ get queued() {
48
+ return {
49
+ pending: this.#scheduler.pendingCount,
50
+ running: this.#scheduler.runningCount,
51
+ oldestWaitMs: this.#scheduler.oldestWaitMs,
52
+ };
53
+ }
54
+ get pageCount() {
55
+ return this.#inner.pageCount;
56
+ }
57
+ /** Page dimensions in PDF points (1/72 inch). */
58
+ pageSize(index) {
59
+ return this.#inner.pageSize(index);
60
+ }
61
+ /** Render a single page. Never blocks the event loop. */
62
+ async renderPage(index, options = {}) {
63
+ return this.render(index, options).promise;
64
+ }
65
+ /**
66
+ * Render a page, returning a handle you can reprioritise or drop.
67
+ *
68
+ * This is the viewer path: as the user scrolls, promote what came into view and
69
+ * demote what left, without re-submitting or duplicating work. Requests for the same
70
+ * page at the same size coalesce into one render, taking the most urgent priority
71
+ * asked for.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const jobs = visible.map((p) => doc.render(p, { fitWidth: 1600, priority: 0 }));
76
+ * onScroll(() => {
77
+ * for (const [i, job] of jobs.entries()) job.setPriority(distanceFromViewport(i));
78
+ * });
79
+ * ```
80
+ */
81
+ render(index, options = {}) {
82
+ const dpi = this.#resolveDpi(index, options);
83
+ const key = `${index}@${dpi.toFixed(4)}`;
84
+ const hit = this.#cache.get(key);
85
+ if (hit)
86
+ return cachedHandle(key, hit);
87
+ const handle = this.#scheduler.submit({
88
+ key,
89
+ priority: options.priority ?? DEFAULT_PRIORITY,
90
+ run: () => this.#inner.renderPageAsync(index, dpi).then((page) => {
91
+ this.#cache.set(key, page);
92
+ return page;
93
+ }),
94
+ });
95
+ const { signal } = options;
96
+ if (signal) {
97
+ if (signal.aborted)
98
+ handle.cancel('signal aborted');
99
+ else
100
+ signal.addEventListener('abort', () => handle.cancel('signal aborted'), {
101
+ once: true,
102
+ });
103
+ }
104
+ return liveHandle(handle);
105
+ }
106
+ /**
107
+ * Turn `fitWidth`/`dpi` into a concrete DPI, and refuse absurd outputs.
108
+ *
109
+ * `fitWidth` exists because page sizes vary by two orders of magnitude in area: a
110
+ * DPI that is sensible for US Letter produces a 113 MB bitmap for an ARCH-E drawing.
111
+ */
112
+ #resolveDpi(index, options) {
113
+ const { width, height } = this.pageSize(index);
114
+ const dpi = options.fitWidth !== undefined && width > 0
115
+ ? (options.fitWidth / width) * 72
116
+ : (options.dpi ?? DEFAULT_DPI);
117
+ const pixels = ((width * dpi) / 72) * ((height * dpi) / 72);
118
+ if (pixels > MAX_PIXELS) {
119
+ const mb = Math.round((pixels * 4) / 1e6);
120
+ throw new RangeError(`papyra: page ${index} at ${dpi.toFixed(1)} DPI would be ` +
121
+ `${Math.round((width * dpi) / 72)}x${Math.round((height * dpi) / 72)} ` +
122
+ `(${mb} MB). The page is ${(width / 72).toFixed(1)}x${(height / 72).toFixed(1)}in — ` +
123
+ 'use { fitWidth } to size by output pixels instead of DPI.');
124
+ }
125
+ return dpi;
126
+ }
127
+ /**
128
+ * Render `[start, end)`.
129
+ *
130
+ * The concurrency strategy differs by runtime, for reasons measured in
131
+ * `docs/spike-results.md`:
132
+ *
133
+ * - **native** — one async task with rayon inside. Fastest, and independent of
134
+ * `UV_THREADPOOL_SIZE`, which we cannot set from inside an addon.
135
+ * - **wasm** — per-page async tasks. rayon's workers there are Web Workers that the
136
+ * JS event loop has to create, so a rayon batch cannot be driven from a blocked
137
+ * thread; per-page tasks map onto the runtime's own worker pool instead.
138
+ */
139
+ async renderPages(start, end, options = {}) {
140
+ if (currentRuntime() === 'native' && options.fitWidth === undefined) {
141
+ // One DPI for the whole batch, so every page in range has to pass the guard —
142
+ // a document can mix a letter-size cover sheet with ARCH-E drawings.
143
+ let dpi = DEFAULT_DPI;
144
+ for (let i = start; i < end; i++)
145
+ dpi = this.#resolveDpi(i, options);
146
+ return (await this.#inner.renderPagesAsync(start, end, dpi));
147
+ }
148
+ const out = [];
149
+ for await (const { page, bitmap } of this.stream({
150
+ ...options,
151
+ order: range(start, end),
152
+ })) {
153
+ out[page - start] = bitmap;
154
+ }
155
+ return out;
156
+ }
157
+ /**
158
+ * Render pages as an async iterable, yielding each as soon as it finishes.
159
+ *
160
+ * At most `concurrency` renders are in flight, so memory stays bounded — 23 letter
161
+ * pages at 150 DPI is ~193 MB of RGBA if you hold them all. Breaking out of the loop
162
+ * stops scheduling further work, which makes "thumbnails for the visible rows only"
163
+ * cheap.
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * for await (const { page, bitmap } of doc.stream({ dpi: 48, order: visible })) {
168
+ * paint(page, bitmap);
169
+ * }
170
+ * ```
171
+ */
172
+ async *stream(options = {}) {
173
+ const pages = options.order ?? range(0, this.pageCount);
174
+ const window = Math.max(1, options.concurrency ?? Math.min(this.#limit, pages.length || 1));
175
+ const inflight = new Map();
176
+ const handles = new Map();
177
+ let next = 0;
178
+ const schedule = (slot) => {
179
+ const page = pages[slot];
180
+ if (page === undefined)
181
+ return;
182
+ const handle = this.render(page, options);
183
+ handles.set(slot, handle);
184
+ inflight.set(slot, handle.promise.then((bitmap) => ({ slot, page, bitmap })));
185
+ };
186
+ try {
187
+ while (next < pages.length && inflight.size < window)
188
+ schedule(next++);
189
+ while (inflight.size > 0) {
190
+ const { slot, page, bitmap } = await Promise.race(inflight.values());
191
+ inflight.delete(slot);
192
+ handles.delete(slot);
193
+ if (next < pages.length)
194
+ schedule(next++);
195
+ yield { page, bitmap };
196
+ }
197
+ }
198
+ finally {
199
+ // Breaking out of the loop drops everything still queued, so "thumbnails for
200
+ // the rows I can see" does not keep rendering the rows I cannot.
201
+ for (const [slot, handle] of handles) {
202
+ handle.cancel('stream ended');
203
+ // The derived promise rejects once cancelled; nobody is listening any more.
204
+ inflight.get(slot)?.catch(() => { });
205
+ }
206
+ inflight.clear();
207
+ handles.clear();
208
+ }
209
+ }
210
+ /** How many pages this document will render at once. */
211
+ get concurrency() {
212
+ return this.#limit;
213
+ }
214
+ }
215
+ function defaultConcurrency() {
216
+ return currentRuntime() === 'wasm'
217
+ ? MAX_WASM_CONCURRENCY
218
+ : hardwareConcurrency();
219
+ }
220
+ /**
221
+ * Wrap a scheduler handle as a {@link RenderHandle}.
222
+ *
223
+ * Delegates rather than spreading: `timing` is a getter that is null until the job
224
+ * settles, and a spread would freeze it at null forever.
225
+ */
226
+ function liveHandle(handle) {
227
+ return {
228
+ key: handle.key,
229
+ cached: false,
230
+ promise: handle.promise,
231
+ get timing() {
232
+ return handle.timing;
233
+ },
234
+ setPriority: (priority) => handle.setPriority(priority),
235
+ cancel: (reason) => handle.cancel(reason),
236
+ };
237
+ }
238
+ /** A handle for work that never had to happen. */
239
+ function cachedHandle(key, page) {
240
+ return {
241
+ key,
242
+ cached: true,
243
+ promise: Promise.resolve(page),
244
+ timing: { waitMs: 0, runMs: 0 },
245
+ setPriority: () => { },
246
+ cancel: () => { },
247
+ };
248
+ }
249
+ function range(start, end) {
250
+ const out = [];
251
+ for (let i = start; i < end; i++)
252
+ out.push(i);
253
+ return out;
254
+ }
255
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * papyra — fast PDF rendering for Node and the browser.
3
+ *
4
+ * ```ts
5
+ * import { open, paintToCanvas } from '@build-qube/papyra';
6
+ *
7
+ * const doc = await open(file); // Uint8Array | Blob | File
8
+ * const page = await doc.renderPage(0, { fitWidth: 1600 });
9
+ * paintToCanvas(page, canvas);
10
+ *
11
+ * // Viewers: attach your own priorities. Lower runs first; the default is urgent.
12
+ * const job = doc.render(3, { fitWidth: 1600, priority: 2 });
13
+ * onScroll(() => job.setPriority(0));
14
+ * ```
15
+ */
16
+ export type { CacheStats } from './cache.js';
17
+ export { paintToCanvas, toImageData } from './canvas.js';
18
+ export type { RenderHandle } from './document.js';
19
+ export { Document, open } from './document.js';
20
+ export type { Runtime } from './runtime.js';
21
+ export { backend, currentRuntime, hardwareConcurrency, init, } from './runtime.js';
22
+ export type { JobHandle, JobTiming } from './scheduler.js';
23
+ export { AbortError, DEFAULT_PRIORITY } from './scheduler.js';
24
+ export type { OpenOptions, PageSize, PdfSource, RenderedPage, RenderOptions, StreamedPage, StreamOptions, } from './types.js';
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACL,OAAO,EACP,cAAc,EACd,mBAAmB,EACnB,IAAI,GACL,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EACV,WAAW,EACX,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * papyra — fast PDF rendering for Node and the browser.
3
+ *
4
+ * ```ts
5
+ * import { open, paintToCanvas } from '@build-qube/papyra';
6
+ *
7
+ * const doc = await open(file); // Uint8Array | Blob | File
8
+ * const page = await doc.renderPage(0, { fitWidth: 1600 });
9
+ * paintToCanvas(page, canvas);
10
+ *
11
+ * // Viewers: attach your own priorities. Lower runs first; the default is urgent.
12
+ * const job = doc.render(3, { fitWidth: 1600, priority: 2 });
13
+ * onScroll(() => job.setPriority(0));
14
+ * ```
15
+ */
16
+ export { paintToCanvas, toImageData } from './canvas.js';
17
+ export { Document, open } from './document.js';
18
+ export { backend, currentRuntime, hardwareConcurrency, init, } from './runtime.js';
19
+ export { AbortError, DEFAULT_PRIORITY } from './scheduler.js';
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EACL,OAAO,EACP,cAAc,EACd,mBAAmB,EACnB,IAAI,GACL,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,25 @@
1
+ export type Runtime = 'native' | 'wasm';
2
+ /**
3
+ * Useful concurrency ceiling on wasm.
4
+ *
5
+ * napi-rs hardcodes `asyncWorkPoolSize = 4` in its generated browser glue, and
6
+ * `AsyncTask` work is dispatched through that pool. Measured throughput is flat from
7
+ * 4 through 16 in-flight renders, so anything above this only inflates peak memory.
8
+ */
9
+ export declare const MAX_WASM_CONCURRENCY = 4;
10
+ /** Best guess at how many renders this host can genuinely run at once. */
11
+ export declare function hardwareConcurrency(): number;
12
+ export declare function currentRuntime(): Runtime;
13
+ export declare function backend(): string;
14
+ /**
15
+ * Prepare the rendering thread pool. Idempotent, and called automatically by
16
+ * {@link open}.
17
+ *
18
+ * Only meaningful on wasm, where `available_parallelism()` is unsupported and rayon
19
+ * would otherwise pin itself to a single thread. Must run before any other rayon use,
20
+ * so we do it before the first document is loaded.
21
+ */
22
+ export declare function init(options?: {
23
+ threads?: number;
24
+ }): void;
25
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAItC,0EAA0E;AAC1E,wBAAgB,mBAAmB,IAAI,MAAM,CAY5C;AAED,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CAS7D"}
@@ -0,0 +1,52 @@
1
+ import { backendName, configureThreadPool, runtime, } from '@build-qube/papyra-native';
2
+ /**
3
+ * Useful concurrency ceiling on wasm.
4
+ *
5
+ * napi-rs hardcodes `asyncWorkPoolSize = 4` in its generated browser glue, and
6
+ * `AsyncTask` work is dispatched through that pool. Measured throughput is flat from
7
+ * 4 through 16 in-flight renders, so anything above this only inflates peak memory.
8
+ */
9
+ export const MAX_WASM_CONCURRENCY = 4;
10
+ let initialised = false;
11
+ /** Best guess at how many renders this host can genuinely run at once. */
12
+ export function hardwareConcurrency() {
13
+ const nav = globalThis
14
+ .navigator;
15
+ if (typeof nav?.hardwareConcurrency === 'number') {
16
+ return Math.max(1, nav.hardwareConcurrency);
17
+ }
18
+ // Node/Bun: avoid a static `node:os` import so bundlers do not pull it into
19
+ // browser builds.
20
+ const proc = globalThis
21
+ .process;
22
+ const fromEnv = Number(proc?.env?.PAPYRA_THREADS);
23
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 4;
24
+ }
25
+ export function currentRuntime() {
26
+ return runtime() === 'wasm' ? 'wasm' : 'native';
27
+ }
28
+ export function backend() {
29
+ return backendName();
30
+ }
31
+ /**
32
+ * Prepare the rendering thread pool. Idempotent, and called automatically by
33
+ * {@link open}.
34
+ *
35
+ * Only meaningful on wasm, where `available_parallelism()` is unsupported and rayon
36
+ * would otherwise pin itself to a single thread. Must run before any other rayon use,
37
+ * so we do it before the first document is loaded.
38
+ */
39
+ export function init(options = {}) {
40
+ if (initialised)
41
+ return;
42
+ initialised = true;
43
+ if (currentRuntime() !== 'wasm')
44
+ return;
45
+ try {
46
+ configureThreadPool(options.threads ?? hardwareConcurrency(), undefined);
47
+ }
48
+ catch {
49
+ // Already built (another copy of the library got there first). Harmless.
50
+ }
51
+ }
52
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,OAAO,GACR,MAAM,2BAA2B,CAAC;AAInC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAEtC,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB;IACjC,MAAM,GAAG,GAAI,UAA+D;SACzE,SAAS,CAAC;IACb,IAAI,OAAO,GAAG,EAAE,mBAAmB,KAAK,QAAQ,EAAE,CAAC;QACjD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC9C,CAAC;IACD,4EAA4E;IAC5E,kBAAkB;IAClB,MAAM,IAAI,GAAI,UAA6D;SACxE,OAAO,CAAC;IACX,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,cAAc,CAAC,CAAC;IAClD,OAAO,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,IAAI,CAAC,OAAO,GAAyB,EAAE;IACrD,IAAI,WAAW;QAAE,OAAO;IACxB,WAAW,GAAG,IAAI,CAAC;IACnB,IAAI,cAAc,EAAE,KAAK,MAAM;QAAE,OAAO;IACxC,IAAI,CAAC;QACH,mBAAmB,CAAC,OAAO,CAAC,OAAO,IAAI,mBAAmB,EAAE,EAAE,SAAS,CAAC,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;IAC3E,CAAC;AACH,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Priority-ordered render scheduler.
3
+ *
4
+ * papyra does not decide what is urgent — the caller does. A viewer knows which pages
5
+ * are on screen, which are one scroll away, and which are thumbnails nobody is looking
6
+ * at; the library does not. Callers attach a priority and this queue honours it.
7
+ *
8
+ * Pending work is reordered freely. Work already running is never interrupted: a page
9
+ * render is dominated by a fixed, non-preemptible content-stream interpretation cost
10
+ * (~95ms of a ~150ms render on a large drawing, independent of output size — see
11
+ * docs/spike-results.md), so cancelling mid-render would reclaim only the rasterisation
12
+ * tail. Reordering what has not started yet captures nearly all of the benefit.
13
+ */
14
+ /** Runs first. Lower is more urgent; the default is the most urgent tier. */
15
+ export declare const DEFAULT_PRIORITY = 0;
16
+ export interface SchedulerJob<T> {
17
+ /** Identity for coalescing. Two live requests with the same key share one render. */
18
+ key: string;
19
+ priority: number;
20
+ run: () => Promise<T>;
21
+ }
22
+ /** Where a job's wall-clock time actually went. */
23
+ export interface JobTiming {
24
+ /** Submitted until a slot freed and it started. */
25
+ waitMs: number;
26
+ /** Started until it finished. */
27
+ runMs: number;
28
+ }
29
+ export interface JobHandle<T> {
30
+ readonly key: string;
31
+ /**
32
+ * Populated once the job settles, `null` before that.
33
+ *
34
+ * The point is to tell a slow render from a long queue: if `waitMs` dominates the
35
+ * pool is too busy or the priority is wrong, and if `runMs` dominates the render
36
+ * itself is the cost. Guessing between those two is how you optimise the wrong half.
37
+ */
38
+ readonly timing: JobTiming | null;
39
+ /** Reassign urgency while the job is still pending. A no-op once it is running. */
40
+ setPriority(priority: number): void;
41
+ readonly promise: Promise<T>;
42
+ /**
43
+ * Detach from this job. If nothing else is waiting on it and it has not started,
44
+ * it is dropped from the queue; if it is already running it is left to finish.
45
+ */
46
+ cancel(reason?: string): void;
47
+ }
48
+ export declare class AbortError extends Error {
49
+ readonly name = "AbortError";
50
+ constructor(reason?: string);
51
+ }
52
+ export interface SchedulerOptions {
53
+ /**
54
+ * Hold back lower-priority work while something more urgent is still running.
55
+ *
56
+ * Ordering the queue is not enough on its own: once a job starts it competes for CPU
57
+ * for its whole duration, so a page rendered alongside three thumbnails takes ~4x as
58
+ * long as one rendered alone. With this on, a job only starts if nothing strictly
59
+ * more urgent is running — already-running work is left to finish, never preempted.
60
+ *
61
+ * It is a no-op when every job shares a priority, so batch throughput is unaffected
62
+ * and it only engages once a caller has expressed intent. On by default.
63
+ */
64
+ yieldToUrgent?: boolean;
65
+ }
66
+ export declare class Scheduler<T> {
67
+ #private;
68
+ constructor(limit: number, options?: SchedulerOptions);
69
+ get pendingCount(): number;
70
+ get runningCount(): number;
71
+ /** How long the longest-waiting pending job has been queued. */
72
+ get oldestWaitMs(): number;
73
+ submit(job: SchedulerJob<T>): JobHandle<T>;
74
+ }
75
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;CACvB;AAED,mDAAmD;AACnD,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IAClC,mFAAmF;IACnF,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7B;;;OAGG;IACH,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAkBD,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,gBAAgB;IACtC,YAAY,MAAM,SAAc,EAE/B;CACF;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,qBAAa,SAAS,CAAC,CAAC;;IAOtB,YAAY,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,EAGxD;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,IAAI,YAAY,IAAI,MAAM,CAEzB;IASD,gEAAgE;IAChE,IAAI,YAAY,IAAI,MAAM,CAOzB;IAED,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAkCzC;CAgFF"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Priority-ordered render scheduler.
3
+ *
4
+ * papyra does not decide what is urgent — the caller does. A viewer knows which pages
5
+ * are on screen, which are one scroll away, and which are thumbnails nobody is looking
6
+ * at; the library does not. Callers attach a priority and this queue honours it.
7
+ *
8
+ * Pending work is reordered freely. Work already running is never interrupted: a page
9
+ * render is dominated by a fixed, non-preemptible content-stream interpretation cost
10
+ * (~95ms of a ~150ms render on a large drawing, independent of output size — see
11
+ * docs/spike-results.md), so cancelling mid-render would reclaim only the rasterisation
12
+ * tail. Reordering what has not started yet captures nearly all of the benefit.
13
+ */
14
+ /** Runs first. Lower is more urgent; the default is the most urgent tier. */
15
+ export const DEFAULT_PRIORITY = 0;
16
+ export class AbortError extends Error {
17
+ name = 'AbortError';
18
+ constructor(reason = 'cancelled') {
19
+ super(reason);
20
+ }
21
+ }
22
+ export class Scheduler {
23
+ #limit;
24
+ #yieldToUrgent;
25
+ #pending = new Map();
26
+ #running = new Map();
27
+ #seq = 0;
28
+ constructor(limit, options = {}) {
29
+ this.#limit = Math.max(1, limit);
30
+ this.#yieldToUrgent = options.yieldToUrgent ?? true;
31
+ }
32
+ get pendingCount() {
33
+ return this.#pending.size;
34
+ }
35
+ get runningCount() {
36
+ return this.#running.size;
37
+ }
38
+ #hasMoreUrgentRunning(priority) {
39
+ for (const entry of this.#running.values()) {
40
+ if (entry.priority < priority)
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ /** How long the longest-waiting pending job has been queued. */
46
+ get oldestWaitMs() {
47
+ let oldest = 0;
48
+ const now = performance.now();
49
+ for (const entry of this.#pending.values()) {
50
+ oldest = Math.max(oldest, now - entry.queuedAt);
51
+ }
52
+ return oldest;
53
+ }
54
+ submit(job) {
55
+ const existing = this.#pending.get(job.key) ?? this.#running.get(job.key);
56
+ if (existing) {
57
+ existing.waiters++;
58
+ // A more urgent request for work already queued promotes it rather than
59
+ // duplicating it — the common case when a thumbnail scrolls into view.
60
+ if (job.priority < existing.priority)
61
+ existing.priority = job.priority;
62
+ return this.#handle(existing);
63
+ }
64
+ let resolve;
65
+ let reject;
66
+ const promise = new Promise((res, rej) => {
67
+ resolve = res;
68
+ reject = rej;
69
+ });
70
+ const entry = {
71
+ key: job.key,
72
+ priority: job.priority,
73
+ queuedAt: performance.now(),
74
+ startedAt: 0,
75
+ finishedAt: 0,
76
+ seq: this.#seq++,
77
+ run: job.run,
78
+ waiters: 1,
79
+ started: false,
80
+ resolve,
81
+ reject,
82
+ promise,
83
+ };
84
+ this.#pending.set(entry.key, entry);
85
+ this.#drain();
86
+ return this.#handle(entry);
87
+ }
88
+ #handle(entry) {
89
+ let detached = false;
90
+ return {
91
+ key: entry.key,
92
+ promise: entry.promise,
93
+ get timing() {
94
+ return entry.finishedAt
95
+ ? {
96
+ waitMs: entry.startedAt - entry.queuedAt,
97
+ runMs: entry.finishedAt - entry.startedAt,
98
+ }
99
+ : null;
100
+ },
101
+ setPriority: (priority) => {
102
+ if (!entry.started)
103
+ entry.priority = priority;
104
+ },
105
+ cancel: (reason) => {
106
+ if (detached)
107
+ return;
108
+ detached = true;
109
+ entry.waiters--;
110
+ if (entry.waiters > 0 || entry.started)
111
+ return;
112
+ this.#pending.delete(entry.key);
113
+ entry.reject(new AbortError(reason));
114
+ },
115
+ };
116
+ }
117
+ /**
118
+ * Start whatever fits, most urgent first.
119
+ *
120
+ * A linear scan rather than a heap: queues are page-sized (hundreds at most) and
121
+ * each job costs milliseconds, so the scan is free — and mutable priorities in a
122
+ * binary heap need re-heapification on every change, which is a rich source of bugs.
123
+ */
124
+ #drain() {
125
+ while (this.#running.size < this.#limit && this.#pending.size > 0) {
126
+ let next;
127
+ for (const entry of this.#pending.values()) {
128
+ if (!next ||
129
+ entry.priority < next.priority ||
130
+ (entry.priority === next.priority && entry.seq < next.seq)) {
131
+ next = entry;
132
+ }
133
+ }
134
+ if (!next)
135
+ return;
136
+ // Do not pile lower-priority work on top of something more urgent that is
137
+ // already running; it would only steal CPU from it.
138
+ if (this.#yieldToUrgent && this.#hasMoreUrgentRunning(next.priority))
139
+ return;
140
+ this.#pending.delete(next.key);
141
+ this.#running.set(next.key, next);
142
+ next.started = true;
143
+ next.startedAt = performance.now();
144
+ // Stamp the finish time before settling, so `timing` is already populated by
145
+ // the time an awaiting caller resumes — a `.finally()` would run after them.
146
+ next
147
+ .run()
148
+ .then((value) => {
149
+ next.finishedAt = performance.now();
150
+ next.resolve(value);
151
+ }, (error) => {
152
+ next.finishedAt = performance.now();
153
+ next.reject(error);
154
+ })
155
+ .finally(() => {
156
+ this.#running.delete(next.key);
157
+ this.#drain();
158
+ });
159
+ }
160
+ }
161
+ }
162
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAqDlC,MAAM,OAAO,UAAW,SAAQ,KAAK;IACjB,IAAI,GAAG,YAAY,CAAC;IACtC,YAAY,MAAM,GAAG,WAAW;QAC9B,KAAK,CAAC,MAAM,CAAC,CAAC;IAChB,CAAC;CACF;AAiBD,MAAM,OAAO,SAAS;IACX,MAAM,CAAS;IACf,cAAc,CAAU;IACxB,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IACvC,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAChD,IAAI,GAAG,CAAC,CAAC;IAET,YAAY,KAAa,EAAE,OAAO,GAAqB,EAAE;QACvD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QACjC,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC;IACtD,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,qBAAqB,CAAC,QAAgB;QACpC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,IAAI,KAAK,CAAC,QAAQ,GAAG,QAAQ;gBAAE,OAAO,IAAI,CAAC;QAC7C,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gEAAgE;IAChE,IAAI,YAAY;QACd,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAC9B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC3C,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,CAAC,GAAoB;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1E,IAAI,QAAQ,EAAE,CAAC;YACb,QAAQ,CAAC,OAAO,EAAE,CAAC;YACnB,wEAAwE;YACxE,uEAAuE;YACvE,IAAI,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAC,QAAQ;gBAAE,QAAQ,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;YACvE,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,OAA4B,CAAC;QACjC,IAAI,MAAkC,CAAC;QACvC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC1C,OAAO,GAAG,GAAG,CAAC;YACd,MAAM,GAAG,GAAG,CAAC;QACf,CAAC,CAAC,CAAC;QAEH,MAAM,KAAK,GAAa;YACtB,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,QAAQ,EAAE,WAAW,CAAC,GAAG,EAAE;YAC3B,SAAS,EAAE,CAAC;YACZ,UAAU,EAAE,CAAC;YACb,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE;YAChB,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,KAAK;YACd,OAAO;YACP,MAAM;YACN,OAAO;SACR,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,CAAC,KAAe;QACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,OAAO;YACL,GAAG,EAAE,KAAK,CAAC,GAAG;YACd,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,IAAI,MAAM;gBACR,OAAO,KAAK,CAAC,UAAU;oBACrB,CAAC,CAAC;wBACE,MAAM,EAAE,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,QAAQ;wBACxC,KAAK,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,SAAS;qBAC1C;oBACH,CAAC,CAAC,IAAI,CAAC;YACX,CAAC;YACD,WAAW,EAAE,CAAC,QAAgB,EAAE,EAAE;gBAChC,IAAI,CAAC,KAAK,CAAC,OAAO;oBAAE,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAChD,CAAC;YACD,MAAM,EAAE,CAAC,MAAe,EAAE,EAAE;gBAC1B,IAAI,QAAQ;oBAAE,OAAO;gBACrB,QAAQ,GAAG,IAAI,CAAC;gBAChB,KAAK,CAAC,OAAO,EAAE,CAAC;gBAChB,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO;oBAAE,OAAO;gBAC/C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAChC,KAAK,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;YACvC,CAAC;SACF,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,MAAM;QACJ,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAClE,IAAI,IAA0B,CAAC;YAC/B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC3C,IACE,CAAC,IAAI;oBACL,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ;oBAC9B,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,EAC1D,CAAC;oBACD,IAAI,GAAG,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;YACD,IAAI,CAAC,IAAI;gBAAE,OAAO;YAElB,0EAA0E;YAC1E,oDAAoD;YACpD,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAClE,OAAO;YAET,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAClC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;YAEnC,6EAA6E;YAC7E,6EAA6E;YAC7E,IAAI;iBACD,GAAG,EAAE;iBACL,IAAI,CACH,CAAC,KAAK,EAAE,EAAE;gBACR,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;gBACpC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC,EACD,CAAC,KAAK,EAAE,EAAE;gBACR,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;gBACpC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC,CACF;iBACA,OAAO,CAAC,GAAG,EAAE;gBACZ,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,CAAC,CAAC,CAAC;QACP,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ import type { PdfSource } from './types.js';
2
+ /**
3
+ * Normalise any accepted input to bytes.
4
+ *
5
+ * Accepts `File` and `Blob` so an `<input type="file">` value can be passed straight
6
+ * through, which is the common case in a viewer.
7
+ */
8
+ export declare function toBytes(source: PdfSource): Promise<Uint8Array>;
9
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM5C;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAUpE"}
package/dist/source.js ADDED
@@ -0,0 +1,22 @@
1
+ function isBlob(value) {
2
+ return typeof Blob !== 'undefined' && value instanceof Blob;
3
+ }
4
+ /**
5
+ * Normalise any accepted input to bytes.
6
+ *
7
+ * Accepts `File` and `Blob` so an `<input type="file">` value can be passed straight
8
+ * through, which is the common case in a viewer.
9
+ */
10
+ export async function toBytes(source) {
11
+ if (source instanceof Uint8Array)
12
+ return source;
13
+ if (isBlob(source))
14
+ return new Uint8Array(await source.arrayBuffer());
15
+ if (source instanceof ArrayBuffer)
16
+ return new Uint8Array(source);
17
+ if (ArrayBuffer.isView(source)) {
18
+ return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
19
+ }
20
+ throw new TypeError('papyra: expected a Uint8Array, ArrayBuffer, TypedArray, Blob, or File');
21
+ }
22
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AAEA,SAAS,MAAM,CAAC,KAAc;IAC5B,OAAO,OAAO,IAAI,KAAK,WAAW,IAAI,KAAK,YAAY,IAAI,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,MAAiB;IAC7C,IAAI,MAAM,YAAY,UAAU;QAAE,OAAO,MAAM,CAAC;IAChD,IAAI,MAAM,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACtE,IAAI,MAAM,YAAY,WAAW;QAAE,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IACjE,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,IAAI,SAAS,CACjB,uEAAuE,CACxE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,91 @@
1
+ /** Anything we can turn into PDF bytes. `File` is a `Blob`, so it is covered. */
2
+ export type PdfSource = Uint8Array | ArrayBuffer | ArrayBufferView | Blob;
3
+ export interface RenderOptions {
4
+ /**
5
+ * Scheduling order. **Lower runs first**; the default, 0, is the most urgent tier.
6
+ *
7
+ * papyra does not infer urgency — a viewer knows what is on screen and the library
8
+ * does not. Attach whatever scale suits you, e.g. 0 for the visible page, 1 for its
9
+ * neighbours, 2 for thumbnails in view, 3 for prefetch.
10
+ *
11
+ * Pending work reorders freely; work already running is never interrupted.
12
+ */
13
+ priority?: number;
14
+ /** Abort a request that has not started yet. */
15
+ signal?: AbortSignal;
16
+ /** Dots per inch. 72 is the PDF's natural size. Defaults to 72. */
17
+ dpi?: number;
18
+ /**
19
+ * Target output width in pixels. Overrides `dpi`.
20
+ *
21
+ * Prefer this for thumbnails and for fitting a viewport. A fixed DPI silently
22
+ * explodes on large-format pages: 36 DPI is a 216x279 thumbnail for US Letter but
23
+ * a 1512x1080, 6.5 MB image for a 42x30in drawing. Sizing by output width keeps
24
+ * cost proportional to what is actually displayed.
25
+ */
26
+ fitWidth?: number;
27
+ }
28
+ export interface StreamOptions extends RenderOptions {
29
+ /**
30
+ * How far ahead of consumption to queue. Defaults to the runtime's concurrency.
31
+ *
32
+ * This is backpressure, not a concurrency limit — the document-wide scheduler owns
33
+ * that, so a high-priority page still preempts a stream that is mid-flight.
34
+ */
35
+ concurrency?: number;
36
+ /**
37
+ * Page indices to render, in priority order. Defaults to every page in order.
38
+ * Results are yielded as they complete, not in this order.
39
+ */
40
+ order?: readonly number[];
41
+ }
42
+ /** A rendered page. `data` is tightly packed RGBA8, `height * stride` bytes. */
43
+ export interface RenderedPage {
44
+ readonly width: number;
45
+ readonly height: number;
46
+ readonly stride: number;
47
+ readonly format: 'rgba8';
48
+ readonly data: Uint8Array;
49
+ }
50
+ export interface StreamedPage {
51
+ /** Index of the page in the document. */
52
+ readonly page: number;
53
+ readonly bitmap: RenderedPage;
54
+ }
55
+ /** Page dimensions in PDF points (1/72 inch). */
56
+ export interface PageSize {
57
+ readonly width: number;
58
+ readonly height: number;
59
+ }
60
+ export interface OpenOptions {
61
+ /** Password for an encrypted document. */
62
+ password?: string;
63
+ /**
64
+ * How many pages may render at once. Defaults to the runtime's concurrency.
65
+ *
66
+ * This is a responsiveness/throughput dial. Priority can only reorder work that has
67
+ * not started, so a wide pool renders a batch faster but makes an urgent page wait
68
+ * behind more in-flight work. Viewers usually want it narrow (2-4); batch jobs want
69
+ * it wide.
70
+ */
71
+ concurrency?: number;
72
+ /**
73
+ * Hold back lower-priority renders while something more urgent is still running.
74
+ *
75
+ * Defaults to on, and is a no-op when every request shares a priority. Turn it off
76
+ * to maximise throughput at the cost of making urgent work wait.
77
+ */
78
+ yieldToUrgent?: boolean;
79
+ /**
80
+ * Bytes of rendered pages to keep for reuse. Defaults to 128 MB; `0` disables it.
81
+ *
82
+ * A render is never cheap — on a large CAD drawing every page costs >=93ms however
83
+ * small the output — and viewers re-render the same page constantly. Bounded by bytes
84
+ * rather than entries because page bitmaps span three orders of magnitude.
85
+ *
86
+ * Only the single-page path is cached (`render`, `renderPage`, `stream`).
87
+ * `renderPages` is a throughput API and would evict everything useful in one call.
88
+ */
89
+ cacheBytes?: number;
90
+ }
91
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,aAAa;IAClD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B;AAED,iDAAiD;AACjD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@build-qube/papyra",
3
+ "version": "0.0.3",
4
+ "description": "Fast PDF rendering for Node and the browser, powered by Rust",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/BuildQube/papyra.git",
9
+ "directory": "packages/papyra"
10
+ },
11
+ "keywords": [
12
+ "pdf",
13
+ "render",
14
+ "thumbnail",
15
+ "rust",
16
+ "wasm",
17
+ "pdfjs"
18
+ ],
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "publishConfig": {
32
+ "registry": "https://registry.npmjs.org/",
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "typecheck": "tsc -p tsconfig.json --noEmit",
38
+ "clean": "rm -rf dist *.tsbuildinfo",
39
+ "test": "bun test"
40
+ },
41
+ "dependencies": {
42
+ "@build-qube/papyra-native": "workspace:*"
43
+ },
44
+ "devDependencies": {
45
+ "typescript": "^7.0.2"
46
+ }
47
+ }