Loading page…
Loading page…
Supplier catalogs · transformation + delivery
Turn supplier product records into a store-ready catalog. Normalize prices and stock, choose the output format, and keep delivery controlled when the destination slows down.
The receiver starts with a 20 ms delay per output chunk. Increase the delay or pause the stream: each write is awaited before more output is delivered, keeping pending writes bounded.
01
Read supplier Product records
02
Normalize price and stock fields
03
Await each delivery before continuing
<Product> <supplier_sku>CAT-1</supplier_sku> <product_name>Desk lamp</product_name> <unit_price>29.90</unit_price> <currency>EUR</currency> <available>12</available> </Product>
{
"sku": "CAT-1",
"name": "Desk lamp",
"price": 29.9,
"currency": "EUR",
"stock": 12
}Synthetic example values. The recipe below uses this exact field mapping. Missing required fields fail explicitly.
Runs on your device · Web Worker + WASM · no source upload
Expected source fields: supplier_sku, product_name, unit_price, currency, available. XML uses Product elements; JSON uses an array. Adapt the recipe for other schemas.
Output goes to a local preview receiver: it retains five small records and up to 4,096 characters of text, then discards the rest. To save complete output incrementally, use the Node download.
1 · Read and transform
0 product records
2 · Serialize
0 MiB of CSV
3 · Await the receiver
20 ms delay per chunk
The receiver accepts one write at a time. Increasing its delay reduces throughput; it does not create a growing list of pending writes. Pause and resume use the same stream.
Ready · XML → CSV · 1,000 synthetic records · 20 ms sink delay
Live browser timings include this run’s receiver and pauses. Process RAM and the tested no-OOM workload are reported separately below. Measurement method →
Run the example to see actual output here.
Recorded evidence · separate from your browser run
v24.19.0 · win32/x64 · WASM · XML → CSV · no artificial sink delay
41.62 MiB input → 7.76 MiB output. Every emitted record was read back and checked. These are measured local results for this fixture, not predictions for your file.
13th Gen Intel(R) Core(TM) i7-13650HX · 20 logical CPUs · 65,199.66 MiB host RAM · 2026-09-09
One run per recipe; sampled process RSS every 10 ms and at awaited file writes. Incremental fixture generation and file close included. Package imports and output verification excluded. No artificial sink delay. 64 MiB V8 old-space setting is not a process memory cap. No intentional OOM sweep.
The 64 MiB V8 old-space setting limits part of the JavaScript heap; it does not cap total process RAM or WASM memory. Browser total RAM is not exposed by this demo, and its live timings are never mixed with these values.
Output SHA-256: e816a16fba36e122ccd982ae8ea0df0b579f167b30c6ac8efd7c34b83b82c504
Download the measured results and artifact fingerprints · Download the reproduction script
Run the script from apps/web in a checkout of the qualified source with the preview runtime built: node --experimental-strip-types scripts/measure-use-cases.mjs. This is a single run per fixture, not a statistically repeated benchmark.
Browser implementation: runs the selected recipe in a module worker with a bounded local preview. The website adds the controls and progress display. Copied browser code uses the selected sample source; the downloaded app also accepts a local file. Both implementations share the same transformation and selected settings. Downloads include the complete project and preview dependency setup.
import { stream } from '@diakrio/converter/browser';
const config = {
"recipe": "streaming-conversion",
"inputFormat": "xml",
"outputFormat": "csv",
"count": 1000,
"sinkDelayMs": 20
};
// Run this module inside a Web Worker.
async function runExample() {
let input;
if (config.sourceUrl) {
const response = await fetch(config.sourceUrl);
if (!response.ok || !response.body) throw new Error("Dataset could not be opened");
input = response.body;
} else input = createFixture(config.inputFormat, config.count, config.recipe);
const job = startRecipe(stream, input, { ...config, expectedRows: config.count }, { progress: console.log });
console.log(await job.result);
}
// Included shared recipe and fixture helpers.
export const recipeProfiles = {
"streaming-conversion": {
"name": "Turn a supplier catalog into store-ready CSV",
"recordElement": "Product",
"fields": [
[
"supplier_sku",
"sku"
],
[
"product_name",
"name"
],
[
"unit_price",
"price",
"f64"
],
[
"currency",
"currency"
],
[
"available",
"stock",
"i64"
]
],
"source": {
"supplier_sku": "CAT-1",
"product_name": "Desk lamp",
"unit_price": "29.90",
"currency": "EUR",
"available": "12"
},
"expected": {
"sku": "CAT-1",
"name": "Desk lamp",
"price": 29.9,
"currency": "EUR",
"stock": 12
}
}
};
export function getRecipe(id = 'large-xml') {
const recipe = recipeProfiles[id];
if (!recipe) throw new Error('Unknown recipe');
return recipe;
}
export function fixtureRecord(id, index) {
if (id === 'large-xml') return healthRecord(index);
const recipe = getRecipe(id);
const row = { ...recipe.source };
const key = recipe.fields[0][0];
row[key] = row[key].replace(/\d+$/, String(index + 1));
// Wide event records make the cost of retaining irrelevant payloads visible.
if (id === 'javascript-string-limits') row.payload = 'synthetic '.repeat(100);
return row;
}
export function recipeTransform(id) {
return { mode: 'replace', onMissingRequired: 'abort', onCoerceError: 'error', fields: getRecipe(id).fields.map(([originFieldName, targetFieldName, type]) => ({ originFieldName, targetFieldName, ...(id === 'large-xml' && targetFieldName === 'unit' ? { defaultValue: '' } : { required: true }), ...(type ? { coerce: { type } } : {}) })) };
}
// Original deterministic stress fixture, informed by the Apple Health XML shape.
// A 4,096-record cycle is repeated; this is not a person's longitudinal history.
export function healthRecord(index) {
const n = index % 4096;
const kinds = [['HKQuantityTypeIdentifierStepCount', 'count', String(50 + n % 200)], ['HKQuantityTypeIdentifierHeartRate', 'count/min', String(60 + n % 80)], ['HKQuantityTypeIdentifierBodyMass', 'kg', (65 + n % 200 / 10).toFixed(1)], ['HKCategoryTypeIdentifierSleepAnalysis', '', 'HKCategoryValueSleepAnalysisAsleepCore']];
const [type, unit, value] = kinds[n % kinds.length];
const stamp = minute => new Date(Date.UTC(2026, 0, 1, 0, minute)).toISOString().replace('T', ' ').replace('.000Z', ' +0000');
return { type, sourceName: n % 2 ? 'Synthetic Phone' : 'Synthetic Watch', sourceVersion: '10.0', unit, creationDate: stamp(n + 1), startDate: stamp(n), endDate: stamp(n + 1), value };
}
export function healthXmlRecord(index) {
const escape = value => String(value).replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
const attrs = Object.entries(healthRecord(index)).map(([key, value]) => `${key}="${escape(value)}"`).join(' ');
return index % 16 === 15 ? ` <Record ${attrs}><MetadataEntry key="HKTimeZone" value="Etc/UTC"/></Record>\n` : ` <Record ${attrs}/>\n`;
}
export function healthFixtureInfo(count = 1000) {
if (!Number.isSafeInteger(count) || count < 1 || count > 100000000) throw new Error('Invalid health record count');
const header = '<?xml version="1.0" encoding="UTF-8"?>\n<HealthData locale="en_US">\n <ExportDate value="2026-01-04 00:00:00 +0000"/>\n';
const footer = '</HealthData>\n';
const encoder = new TextEncoder();
const records = Array.from({ length: 4096 }, (_, index) => healthXmlRecord(index));
const cycle = records.join('');
const bytes = encoder.encode(header + footer).length + Math.floor(count / 4096) * encoder.encode(cycle).length + encoder.encode(records.slice(0, count % 4096).join('')).length;
return { count, bytes, header, footer, records };
}
export function createHealthFixture(count = 1000) {
const info = healthFixtureInfo(count);
const encoder = new TextEncoder();
const batches = Array.from({ length: 4 }, (_, batch) => encoder.encode(info.records.slice(batch * 1024, (batch + 1) * 1024).join('')));
let index = -1;
return new ReadableStream({ pull(controller) {
if (index === -1) { index = 0; controller.enqueue(encoder.encode(info.header)); return; }
if (index === count) { controller.enqueue(encoder.encode(info.footer)); controller.close(); return; }
const size = Math.min(1024, count - index);
controller.enqueue(size === 1024 ? batches[(index / 1024) % 4] : encoder.encode(info.records.slice(index % 4096, index % 4096 + size).join('')));
index += size;
} });
}
// Shared by the website worker and downloaded Node/browser examples.
// The preview is bounded; the complete output goes to the supplied incremental sink.
export function createFixture(format, count = 1000, recipeId = 'large-xml') {
if (recipeId === 'large-xml' && format === 'xml') return createHealthFixture(count);
const recipe = getRecipe(recipeId);
let index = 0;
let opened = false;
const encoder = new TextEncoder();
return new ReadableStream({
pull(target) {
let text = '';
if (!opened) {
opened = true;
text = format === 'xml' ? '<records>' : format === 'json' ? '[' : Object.keys(recipe.source).join(',') + '\n';
}
const end = Math.min(index + 64, count);
for (; index < end; index++) {
const row = fixtureRecord(recipeId, index);
text += format === 'xml'
? '<' + recipe.recordElement + '>' + Object.entries(row).map(([key, value]) => '<' + key + '>' + value + '</' + key + '>').join('') + '</' + recipe.recordElement + '>'
: format === 'json' ? (index ? ',' : '') + JSON.stringify(row)
: Object.values(row).map(value => '"' + String(value).replaceAll('"', '""') + '"').join(',') + '\n';
}
if (index === count) text += format === 'xml' ? '</records>' : format === 'json' ? ']' : '';
target.enqueue(encoder.encode(text));
if (index === count) target.close();
}
});
}
export function startRecipe(stream, input, config = {}, hooks = {}) {
const recipeId = config.recipe ?? 'large-xml';
const clock = () => performance.timeOrigin + performance.now();
const startedAt = config.startedAt ?? clock();
const decoder = new TextDecoder();
let firstRowMs = null;
let rows = 0;
let preview = '';
let lastReport = 0;
let current = { bytesIn: 0, bytesOut: 0, recordsOut: 0 };
const outputOnly = recipeId === 'large-xml' && config.outputFormat === 'csv';
const samples = [];
const snapshot = () => ({
rows, bytesIn: current.bytesIn, bytesOut: current.bytesOut,
firstRowMs, elapsedMs: clock() - startedAt, outputPreview: preview, samples,
wasmMemoryBytes: current.wasmMemoryBytes ?? null,
});
const report = (force = false) => {
if (force || clock() - lastReport >= 100) { lastReport = clock(); hooks.progress?.(snapshot()); }
};
const controller = stream(input, {
backend: config.backend ?? 'auto',
inputFormat: config.inputFormat ?? 'xml',
outputFormat: config.outputFormat ?? 'json',
...(outputOnly ? { csvOutput: { columns: getRecipe(recipeId).fields.map(([, target]) => target) } } : {}),
xmlConfig: { recordElement: getRecipe(recipeId).recordElement, trimText: true, includeAttributes: true },
recordBatchSize: config.recordBatchSize ?? 256,
...(config.xmlParallel === undefined ? {} : { xmlParallel: config.xmlParallel }),
...(config.inputChunkBytes === undefined ? {} : { inputChunkBytes: config.inputChunkBytes }),
...(config.maxMemoryMB === undefined
? { limits: { maxRecordBytes: 1048576, maxOutputBytes: 4194304, maxRecordBatchBytes: 4194304, maxRecordsPerChunk: 4096 } }
: { maxMemoryMB: config.maxMemoryMB }),
transform: recipeTransform(recipeId),
onRecords: outputOnly ? undefined : async (_job, records, stats) => {
current = stats;
if (records.length && firstRowMs === null) firstRowMs = clock() - startedAt;
rows += records.length;
for (const record of records) {
if (samples.length === 5) break;
if (JSON.stringify(record).length <= 1024) samples.push(record);
}
report();
},
async onBytes(bytes, stats) {
current = stats;
if (outputOnly) {
rows = stats.recordsOut;
if (rows && firstRowMs === null) firstRowMs = clock() - startedAt;
}
if (preview.length < 4096) preview += decoder.decode(bytes.subarray(0, Math.min(bytes.length, 4096 - preview.length)), { stream: true });
if (config.sinkDelayMs) await new Promise(resolve => setTimeout(resolve, config.sinkDelayMs));
await hooks.write?.(bytes);
report();
},
onError() { /* result rejects; the host owns visible error handling */ }
});
const result = controller.done.then(async stats => {
current = stats;
await hooks.close?.();
if (config.expectedRows !== undefined && rows !== config.expectedRows) throw new Error('Output record count does not match the fixture.');
const final = snapshot();
hooks.progress?.(final);
return final;
});
return { controller, result };
}
await runExample();
// This browser example discards output after a bounded preview.
The live receiver simulates a slower destination and retains only a small preview. The Node implementation writes the complete output to disk. This synthetic Product schema is not an ONIX compatibility claim. A particular serverless runtime or process RAM cap requires its own benchmark.
Bounded working memory depends on record size, nesting, transform state and queues. This recipe limits individual records and output batches; it does not retain the complete dataset.
Read the measurement contract →Start with the SDK recipe, or give users a guided importer with mapping and validation.