Server Side - SmithSoft-Studios/JAZMIN GitHub Wiki
On a server, JAZMIN files are read per request, served as downloads, turned into PDFs, and written from database cursors. This page covers:
- Serving embedded files (Node and ASP.NET Core)
- Rendering documents to PDF
- Async reading and writing
- Servers with many requests
serveFiles(reader, { prefix }) is a Node request handler (for http.createServer or Express) that serves a file's
embedded files: the invoices, photos or videos stored with the records.
import http from 'node:http';
import { open, serveFiles } from '@smithsoft-studios/jazmin';
// not run: starts a server
const reader = open('claims.jzm', { key: accessKey }); // what this key can see is what is served
const files = serveFiles(reader, { prefix: '/files/' });
http.createServer((req, res) => files(req, res, () => { res.writeHead(404); res.end(); })).listen(8080);
// GET /files/claims/CL-0015/photo-1.png Express: app.use(files)createFileHandler(reader) gives the same answers without a server, for your own framework, request interception or
tests:
import { createFileHandler, write, open } from '@smithsoft-studios/jazmin';
const video = Buffer.alloc(1_000_000, 7); // a stand-in for a 1 MB video
const reader = open(write(null, [{ id: 1 }], {
files: [
{ path: 'media/intro.mp4', content: video },
{ path: 'index.html', content: '<h1>Claim</h1>' },
],
package: { entry: 'index.html' },
}));
const handle = createFileHandler(reader);
const show = (label, { status, headers }) =>
console.log(label.padEnd(11), status, headers['content-type'], headers['content-length'] ?? '-', headers['content-range'] ?? '');
const whole = handle('media/intro.mp4');
show('whole file', whole);
show('byte range', handle('media/intro.mp4', { range: 'bytes=500000-500099' }));
show('unchanged', handle('media/intro.mp4', { ifNoneMatch: whole.headers.etag })); // the ETag is the file's SHA-256
show('not there', handle('media/missing.mp4'));
console.log('headers:', Object.keys(whole.headers).join(', '));
console.log('a page gets:', handle('index.html').headers['content-security-policy']);whole file 200 video/mp4 1000000
byte range 206 video/mp4 100 bytes 500000-500099/1000000
unchanged 304 video/mp4 -
not there 404 text/plain; charset=utf-8 -
headers: content-type, accept-ranges, etag, cache-control, x-content-type-options, content-length
a page gets: default-src 'none'; script-src 'unsafe-inline' blob: 'self'; style-src 'unsafe-inline' blob: 'self'; img-src blob: data: 'self'; font-src blob: data: 'self'; media-src blob: data: 'self'; object-src blob:; frame-src blob:; worker-src blob:; connect-src blob: data: 'self'; sandbox allow-scripts allow-downloads allow-popups
-
Byte ranges (one per request) are served from the 256 KiB blocks they fall in, so video can be streamed and
sought without reading the whole file. Whole files stream block by block (
response.stream()). - Caching: each file's ETag is its SHA-256, so an unchanged file is answered with 304.
- Only what the key can see: other files are 404, as if absent.
-
Pages and SVG are sandboxed: they get the document's security policy and a sandbox, so a page from a file served
on your site can't act as your site (read its cookies, call its APIs). To show a document, open the
.jzmin the viewer instead.
The Jazmin.AspNetCore package serves embedded files from an endpoint:
dotnet add package Jazmin.AspNetCoreusing Jazmin;
using Jazmin.AspNetCore;
// not run: needs a web host and your own key lookup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
var app = builder.Build();
// /docs/42/invoices/2026-03.pdf: that embedded file of statements/42.jzm, read with this person's access key
app.MapJazminFiles("/docs/{id}", async context =>
{
var id = (string)context.Request.RouteValues["id"]!;
var key = await keys.AccessKeyFor(context.User, id); // your lookup; null: 404
return key is null ? null : new JazminFileSource($"statements/{id}.jzm", new JazminReadOptions { AccessKey = key });
}).RequireAuthorization();
app.Run();- Each request opens the file with the key the resolver returns, so it is served only what that key can see (404 otherwise). A key that can't open the file (wrong, expired, revoked) answers 403.
- Byte ranges, HEAD and 304s come from ASP.NET's own file handling, over a stream that decodes one block at a time.
-
Pages and SVG get the same policy and sandbox as in Node;
new JazminFilesOptions { Sandbox = false, Origin = "https://files.example.com" }turns the sandbox off for files served from an origin of their own.
renderPdf opens a file's document (its package's entry page) in a browser you supply, gives it the viewer's
window.jazmin API answered from the file, and prints it. One template serves both the viewer and batch PDFs.
import fs from 'node:fs';
import { chromium } from 'playwright'; // or: import puppeteer from 'puppeteer'
import { renderPdf } from '@smithsoft-studios/jazmin';
// not run: needs Playwright or Puppeteer installed
const browser = await chromium.launch(); // or: await puppeteer.launch()
for (const account of ['ACC-1', 'ACC-2']) {
const pdf = await renderPdf({ file: `statements/${account}.jzm`, key: process.env.STATEMENT_KEY, browser, pdf: { format: 'A4' } });
fs.writeFileSync(`out/${account}.pdf`, pdf);
}
await browser.close();- The browser is yours: a Playwright or Puppeteer Chromium browser. The library itself still needs no packages.
-
When it prints: when the document calls
jazmin.ready(). For a page that never does, passwaitFor: 'load'. Aftertimeout(30 s) it gives up with an error. - Locked down: each document gets its own browser context (no shared cookies or storage), and can reach only its own files and its package's allowed origins. Every other request, including to your internal network, is refused.
-
Downloads the document makes (
jazmin.download) come toonDownload({ filename, type, bytes }); what it passes tojazmin.ready()comes toonReady(info). - Speed: about 0.3–0.5 s per PDF in Chrome with the browser already running (a template with 120 rows, 4 queries, an image and a file).
-
From .NET: call
renderPdffrom a small Node script; PDF rendering isn't built into the .NET library.
New in 1.4:
-
One PDF per account from one file:
renderPdf({ file, key, browser, filter: { account } }). The document sees only those rows, andjazmin.filtertells it which.-
Several tables (1.6.0): the filter also limits every other table it fits (one with the columns it
names). A table it doesn't fit, such as exchange rates, is refused, so no other client's rows reach the PDF; give
it its own with
tableFilters: { rates: null }(nullreads it whole).
-
Several tables (1.6.0): the filter also limits every other table it fits (one with the columns it
names). A table it doesn't fit, such as exchange rates, is refused, so no other client's rows reach the PDF; give
it its own with
-
Print mode: the page sees
jazmin.mode === 'print', so it can show every row and hide its buttons. -
Page settings, each over the ones before: A4 with backgrounds;
pdfDefaults(your paper size for documents that set none); the package'spdf; the page's file'sactions.pdf; what the page set withjazmin.setActions({ pdf }); and thepdfoption (file actions). -
Settings a page asks for (with
jazmin.savePdf) are untrusted:checkPageSettings(settings)returns a clean copy with only page settings, or throws, before you pass them aspdf. -
An image instead:
renderImage({ file, key, browser, viewport: { width: 1200, height: 800 } })gives a PNG of the whole page. -
The document as one HTML file, without a browser:
portableHtml('statement.jzm')gives the viewer with the.jzminside (still encrypted); it opens offline and asks for the key.
Every read and write has an async form. Use them when a server handles other requests while one of them scans a file, or when rows come from an async source (a database cursor, a queue, a network stream). Results are exactly those of the synchronous APIs.
import { JazminWriter, openAsync, writeAsync } from '@smithsoft-studios/jazmin';
// An async source: a database cursor, a message queue, a network stream...
async function* fromDatabase() {
for (let i = 0; i < 30_000; i++) {
if (i % 5000 === 0) await new Promise((r) => setTimeout(r, 1)); // pretend to wait for the next batch
yield { account: `ACC-${String(Math.floor(i / 100)).padStart(3, '0')}`, line: i % 100, amount: (i % 89) / 4 };
}
}
const columns = [
{ name: 'account', type: 'string', nullable: false },
{ name: 'line', type: 'int', nullable: false },
{ name: 'amount', type: 'float' },
];
await writeAsync('statements.jzm', fromDatabase(), { columns, sortedBy: ['account', 'line'] }); // columns are required
// Or a writer, mixing sources
const writer = new JazminWriter('more.jzm', { columns });
await writer.writeRowsAsync(fromDatabase());
await writer.writeRowsAsync([{ account: 'ACC-999', line: 0, amount: 1 }]);
await writer.finishAsync();
const reader = await openAsync('statements.jzm'); // header read with non-blocking reads
let lines = 0;
for await (const row of reader.findAsync({ account: 'ACC-123' }, { select: ['line'] })) lines++;
// Fastest async scan: one array per chunk, so the cost of async iteration is paid per chunk, not per row
let total = 0;
for await (const batch of reader.findBatchesAsync({ amount: { gt: 20 } }, { select: ['amount'] })) {
for (const row of batch) total += row.amount;
}
console.log(`${reader.rowCount} rows; ACC-123 has ${lines} lines; amounts over 20 total ${total}`);
reader.close();30000 rows; ACC-123 has 100 lines; amounts over 20 total 56953
using Jazmin;
using Jazmin.Query;
static async IAsyncEnumerable<object?[]> FromDatabase()
{
for (int i = 0; i < 30_000; i++)
{
if (i % 5000 == 0) await Task.Delay(1);
yield return [$"ACC-{i / 100:D3}", (long)(i % 100), i % 89 / 4.0];
}
}
var columns = new[]
{
new JazminColumn("account", JazminType.String) { Nullable = false },
new JazminColumn("line", JazminType.Int) { Nullable = false },
new JazminColumn("amount", JazminType.Float),
};
await using (var writer = JazminWriter.Create("statements.jzm", columns, new JazminWriteOptions { SortedBy = ["account", "line"] }))
{
await writer.WriteValuesAsync(FromDatabase());
} // DisposeAsync finishes the file
using var reader = await JazminReader.OpenAsync("statements.jzm");
int lines = 0;
await foreach (var row in reader.FindAsync(JazminFilter.Eq("account", "ACC-123"))) lines++;
double total = 0;
await foreach (var line in reader.QueryAsync<StatementLine>(l => l.Amount > 20)) total += line.Amount;
Console.WriteLine($"{reader.RowCount} rows; ACC-123 has {lines} lines; amounts over 20 total {total}");
public class StatementLine
{
public string Account { get; set; } = "";
public long Line { get; set; }
public double Amount { get; set; }
}30000 rows; ACC-123 has 100 lines; amounts over 20 total 56953
- Node: chunks are read with non-blocking reads, two ahead of decoding; between chunks the event loop serves other work. Decoding itself runs on the main thread, a chunk (milliseconds) at a time.
-
.NET: rows are read and decoded on a thread-pool thread, 512 at a time, while you await. Cancellation (every
async method takes a
CancellationToken) stops a query between batches. -
Cost: on a 300,000-row file, reading every row took 287 ms with
findAsyncagainst 252 ms withfind; summing two of 41 columns took 75 ms withfindBatchesAsync, the same asfind. - One async query at a time per reader. To read in parallel, open the file once per task: opening is cheap.
- One reader per request, or a pool of readers. Readers aren't thread-safe, but opening one reads only the catalog (about 1–3 ms), and the OS caches the file.
-
Keep keys in memory, not in logs. Fetch them from your secret manager at start-up;
JazminKey.parse/JazminKey.Parsethejzk1-...text. -
Pick
priority: 'memory'(.NETJazminPriority.Memory) on small containers, or when many files are open at once: each reader or writer otherwise runs threads of its own. See Tuning Speed and Memory. -
Node:
--max-semi-space-size=8keeps memory low on memory-limited servers. -
Large JSON responses: stream them instead of building strings:
exportFile, or in .NETResults.Stream(new JazminJsonStream(reader, filter), "application/json")(see Converting JSON, CSV and XML). -
GraphQL: pass
wherearguments straight tofind/Find: see Filter Language.