Server Side - SmithSoft-Studios/JAZMIN GitHub Wiki

Server Side

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

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 .jzm in the viewer instead.

ASP.NET Core (New in 1.2)

The Jazmin.AspNetCore package serves embedded files from an endpoint:

dotnet add package Jazmin.AspNetCore
using 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.

Rendering documents to PDF

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, pass waitFor: 'load'. After timeout (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 to onDownload({ filename, type, bytes }); what it passes to jazmin.ready() comes to onReady(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 renderPdf from 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, and jazmin.filter tells 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 } (null reads it whole).
  • 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's pdf; the page's file's actions.pdf; what the page set with jazmin.setActions({ pdf }); and the pdf option (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 as pdf.
  • 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 .jzm inside (still encrypted); it opens offline and asks for the key.

Async reading and writing

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 findAsync against 252 ms with find; summing two of 41 columns took 75 ms with findBatchesAsync, the same as find.
  • One async query at a time per reader. To read in parallel, open the file once per task: opening is cheap.

Servers with many requests

  • 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.Parse the jzk1-... text.
  • Pick priority: 'memory' (.NET JazminPriority.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=8 keeps memory low on memory-limited servers.
  • Large JSON responses: stream them instead of building strings: exportFile, or in .NET Results.Stream(new JazminJsonStream(reader, filter), "application/json") (see Converting JSON, CSV and XML).
  • GraphQL: pass where arguments straight to find / Find: see Filter Language.
⚠️ **GitHub.com Fallback** ⚠️