@reportwright/engine 0.11.0 → 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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportwright/engine",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -51,6 +51,7 @@
51
51
  "files": [
52
52
  "dist",
53
53
  "pool",
54
+ "examples",
54
55
  "index.d.ts",
55
56
  "report.d.ts",
56
57
  "schema.json",
@@ -70,7 +71,7 @@
70
71
  "bidi-js": "1.0.3"
71
72
  },
72
73
  "peerDependencies": {
73
- "@reportwright/fonts": "0.11.0"
74
+ "@reportwright/fonts": "0.12.0"
74
75
  },
75
76
  "peerDependenciesMeta": {
76
77
  "@reportwright/fonts": {