@reportwright/engine 0.0.0-stage → 0.12.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.
- package/CHANGELOG.md +233 -0
- package/LICENSE +23 -0
- package/README.md +105 -2
- package/dist/index.js +22548 -0
- package/dist/pdfstreamworker.js +38 -0
- package/dist/types/packages/engine/entry.d.ts +125 -0
- package/dist/types/src/designer/tableGen.d.ts +119 -0
- package/dist/types/src/engine/data/guard.d.ts +50 -0
- package/dist/types/src/engine/data/index.d.ts +120 -0
- package/dist/types/src/engine/data/odata.d.ts +35 -0
- package/dist/types/src/engine/data/stream.d.ts +48 -0
- package/dist/types/src/engine/data/xml.d.ts +108 -0
- package/dist/types/src/engine/expr/evaluate.d.ts +183 -0
- package/dist/types/src/engine/expr/format.d.ts +122 -0
- package/dist/types/src/engine/expr/javafmt.d.ts +26 -0
- package/dist/types/src/engine/expr/parser.d.ts +29 -0
- package/dist/types/src/engine/image.d.ts +107 -0
- package/dist/types/src/engine/index.d.ts +239 -0
- package/dist/types/src/engine/items/barcode.d.ts +121 -0
- package/dist/types/src/engine/items/cells.d.ts +11 -0
- package/dist/types/src/engine/items/chart-kit.d.ts +75 -0
- package/dist/types/src/engine/items/chart-more.d.ts +52 -0
- package/dist/types/src/engine/items/chart-round.d.ts +28 -0
- package/dist/types/src/engine/items/chart.d.ts +42 -0
- package/dist/types/src/engine/items/hpage.d.ts +33 -0
- package/dist/types/src/engine/items/index.d.ts +117 -0
- package/dist/types/src/engine/items/map.d.ts +11 -0
- package/dist/types/src/engine/items/paint.d.ts +138 -0
- package/dist/types/src/engine/items/pivot.d.ts +45 -0
- package/dist/types/src/engine/items/rich.d.ts +105 -0
- package/dist/types/src/engine/items/toc.d.ts +10 -0
- package/dist/types/src/engine/items/visuals.d.ts +5 -0
- package/dist/types/src/engine/layout.d.ts +24 -0
- package/dist/types/src/engine/paged.d.ts +19 -0
- package/dist/types/src/engine/paginate/index.d.ts +17 -0
- package/dist/types/src/engine/reuse.d.ts +120 -0
- package/dist/types/src/engine/schema/report.schema.d.ts +2421 -0
- package/dist/types/src/engine/schema/template.d.ts +212 -0
- package/dist/types/src/engine/schema/validate.d.ts +21 -0
- package/dist/types/src/engine/stream.d.ts +54 -0
- package/dist/types/src/engine/style.d.ts +70 -0
- package/dist/types/src/engine/text/fonts.d.ts +96 -0
- package/dist/types/src/engine/text/measure.d.ts +145 -0
- package/dist/types/src/engine/text/rich.d.ts +48 -0
- package/dist/types/src/engine/text/shaper.d.ts +20 -0
- package/dist/types/src/engine/units.d.ts +53 -0
- package/dist/types/src/engine/url.d.ts +23 -0
- package/dist/types/src/exporters/color.d.ts +9 -0
- package/dist/types/src/exporters/csv.d.ts +44 -0
- package/dist/types/src/exporters/deadline.d.ts +6 -0
- package/dist/types/src/exporters/docx.d.ts +19 -0
- package/dist/types/src/exporters/encrypt.d.ts +39 -0
- package/dist/types/src/exporters/figures.d.ts +112 -0
- package/dist/types/src/exporters/html.d.ts +24 -0
- package/dist/types/src/exporters/htmldata.d.ts +32 -0
- package/dist/types/src/exporters/pdf.d.ts +45 -0
- package/dist/types/src/exporters/pdfa.d.ts +42 -0
- package/dist/types/src/exporters/pdfpaint.d.ts +107 -0
- package/dist/types/src/exporters/pdfstream.d.ts +198 -0
- package/dist/types/src/exporters/pdfstreamtags.d.ts +38 -0
- package/dist/types/src/exporters/pdfua.d.ts +17 -0
- package/dist/types/src/exporters/png.d.ts +10 -0
- package/dist/types/src/exporters/pptx.d.ts +14 -0
- package/dist/types/src/exporters/regions.d.ts +30 -0
- package/dist/types/src/exporters/subset.d.ts +5 -0
- package/dist/types/src/exporters/svg.d.ts +26 -0
- package/dist/types/src/exporters/svgimage.d.ts +33 -0
- package/dist/types/src/exporters/xlsx.d.ts +61 -0
- package/dist/types/src/exporters/xlsxchart.d.ts +26 -0
- package/dist/types/src/exporters/xlsxwriter.d.ts +83 -0
- package/dist/types/src/importers/xml.d.ts +34 -0
- package/examples/stream-1m.mjs +87 -0
- package/index.d.ts +3 -0
- package/package.json +79 -4
- package/pool/index.d.ts +23 -0
- package/pool/index.js +85 -0
- package/pool/worker.js +27 -0
- package/report.d.ts +638 -0
- package/schema.json +2517 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A million-row ledger streamed to a PDF file, with the process's peak memory: the proof that a large report's PDF
|
|
3
|
+
// is written with flat memory. The rows are made as they are read (an async generator standing in for a database
|
|
4
|
+
// cursor), so nothing holds them all.
|
|
5
|
+
//
|
|
6
|
+
// npm i @reportwright/engine @reportwright/fonts
|
|
7
|
+
// node node_modules/@reportwright/engine/examples/stream-1m.mjs [rows=1000000] [out=ledger.pdf] [--plain] [--grouped] [--tagged] [--parallel=4]
|
|
8
|
+
//
|
|
9
|
+
// Prints one JSON line: { rows, pages, streamed, peakRssMB, ms, bytes, ... }. "Page N of M" is in the page footer
|
|
10
|
+
// (the rows are read twice: once to count the pages); --plain leaves M out (one pass). --grouped adds two group levels
|
|
11
|
+
// with subtotals; --tagged makes an accessible PDF/A (PDF/UA-1 + PDF/A-2a); --parallel=N lays out and paints with N
|
|
12
|
+
// worker threads. RW_ENGINE: import the engine from another path (a checkout: packages/engine/entry.js); RW_PUBLIC: a
|
|
13
|
+
// folder with fonts/ and harfbuzz-subset.wasm instead of @reportwright/fonts.
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const flag = (f) => args.includes(`--${f}`);
|
|
20
|
+
const opt = (f) => args.find(a => a.startsWith(`--${f}=`))?.split('=')[1];
|
|
21
|
+
const pos = args.filter(a => !a.startsWith('--'));
|
|
22
|
+
const N = Number(pos[0]) || 1_000_000;
|
|
23
|
+
const file = pos[1] || 'ledger.pdf';
|
|
24
|
+
const engine = await import(process.env.RW_ENGINE ? pathToFileURL(path.resolve(process.env.RW_ENGINE)).href : '@reportwright/engine');
|
|
25
|
+
const { exportPdfStream, FontStore, fontFile, createSubsetter } = engine;
|
|
26
|
+
|
|
27
|
+
// fonts: @reportwright/fonts (the engine finds it), or a folder of them
|
|
28
|
+
let fonts, subset, icc;
|
|
29
|
+
if (process.env.RW_PUBLIC) {
|
|
30
|
+
const dir = path.resolve(process.env.RW_PUBLIC);
|
|
31
|
+
fonts = new FontStore(async (k) => fs.promises.readFile(path.join(dir, 'fonts', fontFile(k))));
|
|
32
|
+
subset = createSubsetter(() => fs.promises.readFile(path.join(dir, 'harfbuzz-subset.wasm')));
|
|
33
|
+
icc = fs.readFileSync(path.join(dir, 'icc', 'sRGB-v2-magic.icc'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const REGIONS = ['North', 'South', 'East', 'West'], TYPES = ['Deposit', 'Withdrawal', 'Transfer', 'Fee', 'Interest'];
|
|
37
|
+
/** the rows, a thousand at a time, made as they are asked for */
|
|
38
|
+
async function* rows() {
|
|
39
|
+
for (let i = 0; i < N; i += 1000) {
|
|
40
|
+
const batch = [];
|
|
41
|
+
for (let j = i; j < Math.min(N, i + 1000); j++) {
|
|
42
|
+
batch.push({ id: j + 1, account: `AC${String(10000 + (j * 7) % 9000)}`, region: REGIONS[(j >> 9) % 4], type: TYPES[(j >> 6) % 5],
|
|
43
|
+
date: new Date(Date.UTC(2025, j % 12, 1 + (j % 28))), amount: Math.round(((j * 7919) % 200000 - 100000) * 1.37) / 100,
|
|
44
|
+
reference: j % 37 === 0 ? `Reversal of an earlier transfer between linked accounts, batch ${j}` : `REF-${j * 31}` });
|
|
45
|
+
}
|
|
46
|
+
yield batch;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const grouped = flag('grouped');
|
|
50
|
+
const total = (label) => [{ height: 15, style: { fontWeight: 'bold' }, cells: [{ value: label }, { value: '=CountRows()' }, null, null, null, { value: '=Sum(Fields.amount)', style: { format: 'N2', textAlign: 'right' } }, null] }];
|
|
51
|
+
const def = {
|
|
52
|
+
$schema: 'pagewright/report@1', name: 'Ledger', locale: 'en-IN', page: { size: 'A4', margins: [36, 36, 36, 36] },
|
|
53
|
+
// the rows come from opts.sqlRows below (a cursor); the query text is not run
|
|
54
|
+
dataSources: [{ name: 'db', type: 'sql', connection: 'ledger', query: 'select * from transactions order by id' }],
|
|
55
|
+
dataSets: [{ name: 'Tx', source: 'db', fields: [{ name: 'id', type: 'number' }, { name: 'account', type: 'string' }, { name: 'region', type: 'string' }, { name: 'type', type: 'string' },
|
|
56
|
+
{ name: 'date', type: 'date' }, { name: 'amount', type: 'number' }, { name: 'reference', type: 'string' }] }],
|
|
57
|
+
sections: {
|
|
58
|
+
pageHeader: { height: 22, items: [{ type: 'textbox', name: 'Title', x: 0, y: 0, w: 300, h: 18, value: 'Transactions ledger', style: { fontWeight: 'bold', fontSize: 12 } }] },
|
|
59
|
+
body: { items: [{ type: 'table', name: 'Tx', dataSet: 'Tx', x: 0, y: 0, w: 523, h: 40,
|
|
60
|
+
columns: [{ width: 50 }, { width: 62 }, { width: 50 }, { width: 62 }, { width: 62 }, { width: 70 }, { width: 167 }],
|
|
61
|
+
header: [{ height: 16, style: { fontWeight: 'bold', backgroundColor: '#eef2f7' }, cells: ['Id', 'Account', 'Region', 'Type', 'Date', 'Amount', 'Reference'].map(value => ({ value })) }],
|
|
62
|
+
detail: [{ height: 14, style: { borderBottom: '0.5 solid #e5e7eb' }, cells: [{ value: '=Fields.id' }, { value: '=Fields.account' }, { value: '=Fields.region' }, { value: '=Fields.type' },
|
|
63
|
+
{ value: '=Fields.date', style: { format: 'd' } }, { value: '=Fields.amount', style: { format: 'N2', textAlign: 'right' } }, { value: '=Fields.reference', style: { fontSize: 8 } }] }],
|
|
64
|
+
footer: total('Total'),
|
|
65
|
+
...(grouped ? { groups: [
|
|
66
|
+
{ name: 'Region', expr: '=Fields.region', repeatHeader: true, header: [{ height: 18, style: { fontWeight: 'bold', backgroundColor: '#dbeafe' }, cells: [{ value: '="Region " & Fields.region', colSpan: 3 }, null, null] }], footer: total('Region total') },
|
|
67
|
+
{ name: 'Type', expr: '=Fields.type', header: [{ height: 15, cells: [null, { value: '=Fields.type' }] }], footer: total('Type total') },
|
|
68
|
+
] } : {}) }] },
|
|
69
|
+
pageFooter: { height: 18, items: [{ type: 'textbox', name: 'Page', x: 0, y: 2, w: 523, h: 14, style: { textAlign: 'right' },
|
|
70
|
+
value: flag('plain') ? 'Page {Globals.PageNumber}' : 'Page {Globals.PageNumber} of {Globals.TotalPages}' }] },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let peak = 0;
|
|
75
|
+
const rss = () => { const m = process.memoryUsage.rss() / 1048576; if (m > peak) peak = m; };
|
|
76
|
+
rss();
|
|
77
|
+
const base = peak;
|
|
78
|
+
const timer = setInterval(rss, 20);
|
|
79
|
+
const t0 = Date.now();
|
|
80
|
+
const out = fs.createWriteStream(file);
|
|
81
|
+
const r = await exportPdfStream(def, { fonts, subset, title: 'Ledger', sqlRows: () => rows(), maxRows: Math.max(N, 5_000_000), timeoutMs: 24 * 3600e3,
|
|
82
|
+
...(flag('tagged') ? { tagged: { lang: 'en-IN' }, pdfa: icc ? { icc } : true } : {}), ...(opt('parallel') ? { parallel: Number(opt('parallel')) } : {}) }, out);
|
|
83
|
+
await new Promise((ok, fail) => out.end((e) => (e ? fail(e) : ok())));
|
|
84
|
+
clearInterval(timer);
|
|
85
|
+
rss();
|
|
86
|
+
console.log(JSON.stringify({ rows: N, pages: r.pages, streamed: r.streamed, ...(r.why ? { why: r.why } : {}), peakRssMB: Math.round(peak), startRssMB: Math.round(base), ms: Date.now() - t0,
|
|
87
|
+
bytes: fs.statSync(file).size, file, node: process.version, grouped, tagged: flag('tagged'), parallel: Number(opt('parallel')) || 1 }));
|
package/index.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportwright/engine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Arun Mallikarjun",
|
|
10
|
+
"homepage": "https://github.com/MrArun005/reportwright/tree/main/packages/engine#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/MrArun005/reportwright/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/MrArun005/reportwright.git",
|
|
17
|
+
"directory": "packages/engine"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"reportwright",
|
|
21
|
+
"report",
|
|
22
|
+
"reporting",
|
|
23
|
+
"pdf",
|
|
24
|
+
"pdf-a",
|
|
25
|
+
"excel",
|
|
26
|
+
"xlsx",
|
|
27
|
+
"docx",
|
|
28
|
+
"pptx",
|
|
29
|
+
"report-engine",
|
|
30
|
+
"jasperreports",
|
|
31
|
+
"ssrs",
|
|
32
|
+
"rdl"
|
|
33
|
+
],
|
|
34
|
+
"description": "ReportWright report engine: JSON report definitions to pages, PDF (incl. PDF/A), Word, Excel, PowerPoint, HTML and CSV. Node and browsers. Fonts: @reportwright/fonts; CLI: @reportwright/cli.",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./index.d.ts",
|
|
38
|
+
"default": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./report": {
|
|
41
|
+
"types": "./report.d.ts"
|
|
42
|
+
},
|
|
43
|
+
"./schema.json": "./schema.json",
|
|
44
|
+
"./pool": {
|
|
45
|
+
"types": "./pool/index.d.ts",
|
|
46
|
+
"default": "./pool/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./package.json": "./package.json"
|
|
49
|
+
},
|
|
50
|
+
"types": "./index.d.ts",
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"pool",
|
|
54
|
+
"examples",
|
|
55
|
+
"index.d.ts",
|
|
56
|
+
"report.d.ts",
|
|
57
|
+
"schema.json",
|
|
58
|
+
"README.md",
|
|
59
|
+
"CHANGELOG.md",
|
|
60
|
+
"LICENSE"
|
|
61
|
+
],
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20.9"
|
|
64
|
+
},
|
|
65
|
+
"sideEffects": false,
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"pdf-lib": "1.17.1",
|
|
68
|
+
"@pdf-lib/fontkit": "1.1.1",
|
|
69
|
+
"bwip-js": "4.11.4",
|
|
70
|
+
"fflate": "0.8.3",
|
|
71
|
+
"bidi-js": "1.0.3"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@reportwright/fonts": "0.12.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"@reportwright/fonts": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/pool/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @reportwright/engine/pool (Node only): render many reports in parallel on worker threads.
|
|
2
|
+
import type { Report } from '../report.js';
|
|
3
|
+
export interface PoolRenderOptions {
|
|
4
|
+
/** default pdf */
|
|
5
|
+
format?: 'pdf' | 'xlsx' | 'docx' | 'pptx' | 'csv' | 'html';
|
|
6
|
+
parameters?: Record<string, unknown>;
|
|
7
|
+
timeZone?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
/** passed to the exporter: exportPdf's title, pdfa, … */
|
|
10
|
+
options?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface Pool {
|
|
13
|
+
/** the file's bytes */
|
|
14
|
+
render(definition: Report | object, opts?: PoolRenderOptions): Promise<Uint8Array>;
|
|
15
|
+
/** stops the workers; renders still waiting are rejected */
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
readonly size: number;
|
|
18
|
+
/** renders waiting or running */
|
|
19
|
+
readonly pending: number;
|
|
20
|
+
}
|
|
21
|
+
/** workers: default the CPU count less one. fontsDir: a folder of the fonts (default @reportwright/fonts).
|
|
22
|
+
* heapMB: each worker's heap limit (default 1024). timeoutMs: stop a render past it with status 504 (default 0: no limit). */
|
|
23
|
+
export function createPool(opt?: { workers?: number; fontsDir?: string; maxQueue?: number; heapMB?: number; timeoutMs?: number }): Pool;
|
package/pool/index.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @reportwright/engine/pool (Node only): render many reports in parallel on worker threads.
|
|
2
|
+
// const pool = createPool({ workers: 4 });
|
|
3
|
+
// const pdf = await pool.render(definition, { format: 'pdf', parameters: { id: 42 } });
|
|
4
|
+
// await pool.close();
|
|
5
|
+
import { Worker } from 'node:worker_threads';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ format?: 'pdf' | 'xlsx' | 'docx' | 'pptx' | 'csv' | 'html', parameters?: Record<string, unknown>,
|
|
10
|
+
* timeZone?: string, timeoutMs?: number, options?: Record<string, unknown> }} PoolRenderOptions
|
|
11
|
+
* options: passed to the exporter (exportPdf's title, pdfa, …)
|
|
12
|
+
* @typedef {{ render: (definition: object, opts?: PoolRenderOptions) => Promise<Uint8Array>, close: () => Promise<void>, readonly size: number, readonly pending: number }} Pool
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A pool of worker threads, each with the engine and its fonts loaded once.
|
|
17
|
+
* @param {{ workers?: number, fontsDir?: string, maxQueue?: number, heapMB?: number, timeoutMs?: number }} [opt]
|
|
18
|
+
* workers: default the CPU count less one. fontsDir: a folder of the fonts (default @reportwright/fonts).
|
|
19
|
+
* maxQueue: renders waiting beyond it are refused (default 10,000).
|
|
20
|
+
* heapMB: each worker's heap limit (default 1024): a render past it fails alone (its worker is replaced), instead of
|
|
21
|
+
* taking the whole process down. timeoutMs: a render running longer is stopped (status 504) and its worker replaced
|
|
22
|
+
* (default: no limit; the engine's own opts.timeoutMs per render is checked between rows, this one always holds).
|
|
23
|
+
* @returns {Pool}
|
|
24
|
+
*/
|
|
25
|
+
export function createPool({ workers = Math.max(1, (os.availableParallelism?.() ?? os.cpus().length) - 1), fontsDir, maxQueue = 10000, heapMB = 1024, timeoutMs = 0 } = {}) {
|
|
26
|
+
if (!Number.isInteger(workers) || workers < 1) throw new TypeError('createPool({ workers }): workers must be a whole number of at least 1');
|
|
27
|
+
if (!Number.isFinite(heapMB) || heapMB < 32) throw new TypeError('createPool({ heapMB }): heapMB must be a number of at least 32');
|
|
28
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new TypeError('createPool({ timeoutMs }): timeoutMs must be 0 (no limit) or more');
|
|
29
|
+
/** @type {{ def: object, opts: PoolRenderOptions, resolve: (b: Uint8Array) => void, reject: (e: Error) => void }[]} */
|
|
30
|
+
const queue = [];
|
|
31
|
+
/** @type {Set<Worker>} */
|
|
32
|
+
const idle = new Set();
|
|
33
|
+
/** @type {Map<Worker, (typeof queue)[number]>} */
|
|
34
|
+
const busy = new Map();
|
|
35
|
+
/** @type {Map<Worker, any>} */
|
|
36
|
+
const timers = new Map();
|
|
37
|
+
let closed = false;
|
|
38
|
+
const done = (w) => { clearTimeout(timers.get(w)); timers.delete(w); const job = busy.get(w); busy.delete(w); return job; };
|
|
39
|
+
const spawn = () => {
|
|
40
|
+
const w = new Worker(new URL('./worker.js', import.meta.url), { workerData: { fontsDir }, resourceLimits: { maxOldGenerationSizeMb: heapMB } });
|
|
41
|
+
w.on('message', (msg) => {
|
|
42
|
+
const job = done(w);
|
|
43
|
+
if (job) msg.error ? job.reject(Object.assign(new Error(msg.error.message), msg.error)) : job.resolve(new Uint8Array(msg.bytes));
|
|
44
|
+
idle.add(w);
|
|
45
|
+
next();
|
|
46
|
+
});
|
|
47
|
+
w.on('error', (e) => {
|
|
48
|
+
const oom = /** @type {any} */ (e)?.code === 'ERR_WORKER_OUT_OF_MEMORY';
|
|
49
|
+
done(w)?.reject(oom ? Object.assign(new Error(`the render ran out of memory (heapMB: ${heapMB}) and was stopped`), { status: 500, code: e.code }) : e);
|
|
50
|
+
});
|
|
51
|
+
w.on('exit', () => {
|
|
52
|
+
idle.delete(w);
|
|
53
|
+
done(w)?.reject(new Error('the render worker stopped'));
|
|
54
|
+
if (!closed) { idle.add(spawn()); next(); } // a crashed worker is replaced
|
|
55
|
+
});
|
|
56
|
+
return w;
|
|
57
|
+
};
|
|
58
|
+
const next = () => {
|
|
59
|
+
while (queue.length && idle.size) {
|
|
60
|
+
const w = /** @type {Worker} */ (idle.values().next().value);
|
|
61
|
+
idle.delete(w);
|
|
62
|
+
const job = /** @type {(typeof queue)[number]} */ (queue.shift());
|
|
63
|
+
busy.set(w, job);
|
|
64
|
+
// past the time limit the worker is stopped (a render in a long synchronous step cannot be interrupted otherwise)
|
|
65
|
+
if (timeoutMs > 0) timers.set(w, setTimeout(() => { done(w)?.reject(Object.assign(new Error(`the render ran longer than ${timeoutMs} ms and was stopped`), { status: 504 })); w.terminate(); }, timeoutMs));
|
|
66
|
+
w.postMessage({ def: job.def, opts: job.opts });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
for (let i = 0; i < workers; i++) idle.add(spawn());
|
|
70
|
+
return {
|
|
71
|
+
get size() { return workers; },
|
|
72
|
+
get pending() { return queue.length + busy.size; },
|
|
73
|
+
render(def, opts = {}) {
|
|
74
|
+
if (closed) return Promise.reject(new Error('the pool is closed'));
|
|
75
|
+
if (!def || typeof def !== 'object') return Promise.reject(new TypeError('pool.render(definition, opts): definition must be a report object'));
|
|
76
|
+
if (queue.length >= maxQueue) return Promise.reject(new Error(`the pool queue is full (${maxQueue} renders waiting)`));
|
|
77
|
+
return new Promise((resolve, reject) => { queue.push({ def, opts, resolve, reject }); next(); });
|
|
78
|
+
},
|
|
79
|
+
async close() {
|
|
80
|
+
closed = true;
|
|
81
|
+
for (const job of queue.splice(0)) job.reject(new Error('the pool is closed'));
|
|
82
|
+
await Promise.all([...idle, ...busy.keys()].map(w => w.terminate()));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
package/pool/worker.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// One render thread of @reportwright/engine/pool: the engine and the fonts load once, then one job at a time.
|
|
2
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { render, FontStore, fontFile, defaultFontStore, exportPdf, exportXlsx, exportDocx, exportPptx, exportHtml, exportCsv, needsExportData } from '@reportwright/engine';
|
|
6
|
+
|
|
7
|
+
const fonts = workerData?.fontsDir ? new FontStore((key) => fs.readFile(path.join(workerData.fontsDir, fontFile(key)))) : defaultFontStore();
|
|
8
|
+
const EXPORT = {
|
|
9
|
+
pdf: (m, o) => exportPdf(m, { fonts, ...o }),
|
|
10
|
+
xlsx: (m, o) => exportXlsx(m, { fonts, ...o }),
|
|
11
|
+
docx: (m, o) => exportDocx(m, { fonts, ...o }),
|
|
12
|
+
pptx: (m, o) => exportPptx(m, { fonts, ...o }),
|
|
13
|
+
html: async (m, o) => new TextEncoder().encode(await exportHtml(m, fonts, o)),
|
|
14
|
+
csv: async (m, o) => new TextEncoder().encode(exportCsv(m, o)),
|
|
15
|
+
};
|
|
16
|
+
/** @type {import('node:worker_threads').MessagePort} */ (parentPort).on('message', async ({ def, opts }) => {
|
|
17
|
+
try {
|
|
18
|
+
const format = opts.format || 'pdf';
|
|
19
|
+
if (!EXPORT[format]) throw new Error(`unknown format "${format}" (pdf, xlsx, docx, pptx, html, csv)`);
|
|
20
|
+
const model = await render(def, { fontStore: fonts, parameters: opts.parameters, timeZone: opts.timeZone, timeoutMs: opts.timeoutMs, exportData: needsExportData(format, opts.options) });
|
|
21
|
+
const bytes = await EXPORT[format](model, opts.options || {});
|
|
22
|
+
const buf = bytes.buffer.byteLength === bytes.byteLength ? bytes.buffer : bytes.slice().buffer;
|
|
23
|
+
parentPort?.postMessage({ bytes: buf }, [buf]);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
parentPort?.postMessage({ error: { message: String(e?.message || e), status: e?.status } });
|
|
26
|
+
}
|
|
27
|
+
});
|