l21x-snapshot
Encode any document to a self-describing base64 snapshot, keep a catalog of them with pure save/load/validate/sort, and fold a whole catalog into one portable blob — a zero-dependency save-file/catalog/archive layer for apps with no backend. No DOM, no filesystem, no network.
The honest edge
It persists structure, not identity — you supply ids and timestamps; snapshots are base64 (not compressed, not encrypted). It's a layer that hands you strings, not a store: it never touches localStorage, the disk, or the DOM itself.
Run it
node l21x-snapshot.js --demo
test_l21x-snapshot.js (41/41)
JavaScript, zero dependencies (Node + browser)
The code — every file that ships
l21x-snapshot.js332 lineson GitHub →
#!/usr/bin/env node
// SPDX-License-Identifier: MIT
"use strict";
/* l21x-snapshot.js — a zero-dependency browser document persistence layer.
A save-file / catalog / archive layer for browser apps that hold documents in
memory and need to persist, list, and move them without a backend. Three tiers,
all pure functions over values you already hold — no DOM, no filesystem, no fetch:
1. SNAPSHOT CODEC. encodeSnapshot(doc) -> a self-describing base64 string
(an ".l21x" snapshot); decodeSnapshot(str) -> the document back, verified.
A document is any JSON-serializable object; the codec stamps a format tag
and a version so a snapshot names its own shape.
2. CATALOG. An in-memory list of snapshot entries with pure save/load/validate/
sort operations. catalogSave returns a NEW catalog (never mutates its input),
keyed by a caller-supplied id; catalogLoad decodes an entry back to a doc;
catalogValidate reports honest structural errors; catalogSort orders by a
named field without touching the input.
3. ARCHIVE. archiveExport(catalog) -> ONE base64 blob (a whole catalog as a
single portable string, for a download-one-file / paste-into-another-tab
move); archiveImport(blob) -> the catalog back. Round-trips exactly.
The same code runs in Node (module.exports) or a browser
(window.LoopGifts.l21x). It persists data you already hold as objects; it does
not fetch, connect, read files, or touch the DOM. Base64 is computed in pure JS
so the behavior is identical in both runtimes (no atob/btoa, no Buffer needed). */
// ---- UTF-8 <-> bytes (pure; full multibyte) --------------------------------
function utf8Encode(str) {
var out = [], i, c;
for (i = 0; i < str.length; i++) {
c = str.charCodeAt(i);
if (c < 0x80) { out.push(c); }
else if (c < 0x800) {
out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
} else if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) {
// surrogate pair -> full code point
var c2 = str.charCodeAt(i + 1);
if (c2 >= 0xdc00 && c2 <= 0xdfff) {
var cp = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f),
0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
i++;
} else {
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
}
} else {
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
}
}
return out;
}
function utf8Decode(bytes) {
var out = "", i = 0, b, cp, n;
while (i < bytes.length) {
b = bytes[i];
if (b < 0x80) { out += String.fromCharCode(b); i += 1; continue; }
else if ((b & 0xe0) === 0xc0) { cp = b & 0x1f; n = 1; }
else if ((b & 0xf0) === 0xe0) { cp = b & 0x0f; n = 2; }
else if ((b & 0xf8) === 0xf0) { cp = b & 0x07; n = 3; }
else { out += "\ufffd"; i += 1; continue; }
if (i + n >= bytes.length + 1 && i + n > bytes.length) { out += "\ufffd"; break; }
var ok = true;
for (var k = 1; k <= n; k++) {
var cont = bytes[i + k];
if (cont === undefined || (cont & 0xc0) !== 0x80) { ok = false; break; }
cp = (cp << 6) | (cont & 0x3f);
}
if (!ok) { out += "\ufffd"; i += 1; continue; }
if (cp > 0xffff) {
cp -= 0x10000;
out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
} else {
out += String.fromCharCode(cp);
}
i += n + 1;
}
return out;
}
// ---- base64 (pure; url-safe-tolerant on decode) ----------------------------
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function base64FromBytes(bytes) {
var out = "", i;
for (i = 0; i < bytes.length; i += 3) {
var b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2];
var t = (b0 << 16) | ((b1 || 0) << 8) | (b2 || 0);
out += B64.charAt((t >> 18) & 0x3f) + B64.charAt((t >> 12) & 0x3f);
out += (i + 1 < bytes.length) ? B64.charAt((t >> 6) & 0x3f) : "=";
out += (i + 2 < bytes.length) ? B64.charAt(t & 0x3f) : "=";
}
return out;
}
function bytesFromBase64(str) {
var lut = bytesFromBase64._lut;
if (!lut) {
lut = bytesFromBase64._lut = {};
for (var i = 0; i < B64.length; i++) lut[B64.charAt(i)] = i;
lut["-"] = 62; lut["_"] = 63; // tolerate url-safe alphabet
}
var out = [], buffer = 0, bits = 0;
for (var j = 0; j < str.length; j++) {
var c = str.charAt(j);
if (c === "=") break;
var v = lut[c];
if (v === undefined) continue; // skip whitespace/newlines
buffer = (buffer << 6) | v; bits += 6;
if (bits >= 8) { bits -= 8; out.push((buffer >> bits) & 0xff); }
}
return out;
}
// ---- snapshot codec --------------------------------------------------------
var FORMAT = "l21x";
var FORMAT_VERSION = 1;
/* encodeSnapshot(doc[, meta]) -> base64 string.
doc: any JSON-serializable value (the document body).
meta: optional {title, schema} recorded alongside. */
function encodeSnapshot(doc, meta) {
if (doc === undefined) throw new Error("encodeSnapshot: doc is required");
meta = meta || {};
var envelope = {
fmt: FORMAT,
v: FORMAT_VERSION,
title: (meta.title === undefined) ? null : String(meta.title),
schema: (meta.schema === undefined) ? null : String(meta.schema),
doc: doc
};
var json;
try { json = JSON.stringify(envelope); }
catch (e) { throw new Error("encodeSnapshot: doc is not JSON-serializable (" + e.message + ")"); }
return base64FromBytes(utf8Encode(json));
}
/* decodeSnapshot(str) -> {title, schema, doc, v}. Throws on a malformed or
foreign-format snapshot rather than returning a half-parsed value. */
function decodeSnapshot(str) {
if (typeof str !== "string" || str.length === 0)
throw new Error("decodeSnapshot: expected a non-empty base64 string");
var json = utf8Decode(bytesFromBase64(str));
var env;
try { env = JSON.parse(json); }
catch (e) { throw new Error("decodeSnapshot: not a valid l21x snapshot (bad base64/JSON)"); }
if (!env || env.fmt !== FORMAT)
throw new Error("decodeSnapshot: not an l21x snapshot (fmt=" + (env && env.fmt) + ")");
if (typeof env.v !== "number" || env.v > FORMAT_VERSION)
throw new Error("decodeSnapshot: unsupported snapshot version " + (env && env.v));
return { title: env.title, schema: env.schema, doc: env.doc, v: env.v };
}
// ---- catalog (pure; never mutates its input) -------------------------------
/* A catalog is an array of entries: {id, title, schema, updated, snapshot}.
snapshot is the base64 string; updated is a caller-supplied sortable value. */
/* catalogSave(catalog, id, doc[, meta]) -> a NEW catalog with id saved/replaced.
meta.updated (optional) is recorded for sorting; defaults to null. */
function catalogSave(catalog, id, doc, meta) {
if (!isArray(catalog)) throw new Error("catalogSave: catalog must be an array");
if (id === undefined || id === null || String(id) === "")
throw new Error("catalogSave: id is required");
meta = meta || {};
var entry = {
id: String(id),
title: (meta.title === undefined) ? null : String(meta.title),
schema: (meta.schema === undefined) ? null : String(meta.schema),
updated: (meta.updated === undefined) ? null : meta.updated,
snapshot: encodeSnapshot(doc, meta)
};
var out = [], replaced = false, i;
for (i = 0; i < catalog.length; i++) {
if (catalog[i] && catalog[i].id === entry.id) { out.push(entry); replaced = true; }
else { out.push(catalog[i]); }
}
if (!replaced) out.push(entry);
return out;
}
/* catalogLoad(catalog, id) -> the decoded {title, schema, doc, v}, or null if
the id is not present. */
function catalogLoad(catalog, id) {
if (!isArray(catalog)) throw new Error("catalogLoad: catalog must be an array");
for (var i = 0; i < catalog.length; i++) {
if (catalog[i] && catalog[i].id === String(id)) return decodeSnapshot(catalog[i].snapshot);
}
return null;
}
/* catalogValidate(catalog) -> {ok, errors[]}. Honest structural check: each entry
has a non-empty id, ids are unique, and every snapshot decodes. Reports every
problem it finds rather than throwing on the first. */
function catalogValidate(catalog) {
var errors = [];
if (!isArray(catalog)) return { ok: false, errors: ["catalog is not an array"] };
var seen = {};
for (var i = 0; i < catalog.length; i++) {
var e = catalog[i];
if (!e || typeof e !== "object") { errors.push("entry " + i + ": not an object"); continue; }
if (e.id === undefined || e.id === null || String(e.id) === "")
errors.push("entry " + i + ": missing id");
else if (seen[e.id]) errors.push("entry " + i + ": duplicate id '" + e.id + "'");
else seen[e.id] = true;
if (typeof e.snapshot !== "string") { errors.push("entry " + i + ": snapshot is not a string"); }
else { try { decodeSnapshot(e.snapshot); } catch (err) { errors.push("entry " + i + ": " + err.message); } }
}
return { ok: errors.length === 0, errors: errors };
}
/* catalogSort(catalog, key[, desc]) -> a NEW sorted catalog. key is an entry
field ('title'|'id'|'updated'|'schema'). Stable, input untouched. */
function catalogSort(catalog, key, desc) {
if (!isArray(catalog)) throw new Error("catalogSort: catalog must be an array");
key = key || "id";
var indexed = [], i;
for (i = 0; i < catalog.length; i++) indexed.push([catalog[i], i]);
indexed.sort(function (a, b) {
var av = a[0] ? a[0][key] : undefined, bv = b[0] ? b[0][key] : undefined;
var aNull = (av === null || av === undefined), bNull = (bv === null || bv === undefined);
// nulls ALWAYS last, independent of sort direction
if (aNull && bNull) return a[1] - b[1];
if (aNull) return 1;
if (bNull) return -1;
var cmp;
if (av === bv) cmp = a[1] - b[1]; // stable on ties
else cmp = (av < bv) ? -1 : 1;
if (cmp === 0) return 0;
return desc ? -cmp : cmp;
});
var out = [];
for (i = 0; i < indexed.length; i++) out.push(indexed[i][0]);
return out;
}
// ---- archive (whole catalog <-> one base64 blob) ---------------------------
var ARCHIVE_FORMAT = "l21x-archive";
/* archiveExport(catalog) -> ONE base64 blob carrying the whole catalog. */
function archiveExport(catalog) {
if (!isArray(catalog)) throw new Error("archiveExport: catalog must be an array");
var v = catalogValidate(catalog);
if (!v.ok) throw new Error("archiveExport: refusing to export an invalid catalog (" + v.errors[0] + ")");
var envelope = { fmt: ARCHIVE_FORMAT, v: FORMAT_VERSION, entries: catalog };
return base64FromBytes(utf8Encode(JSON.stringify(envelope)));
}
/* archiveImport(blob) -> the catalog array. Throws on a foreign/malformed blob. */
function archiveImport(blob) {
if (typeof blob !== "string" || blob.length === 0)
throw new Error("archiveImport: expected a non-empty base64 string");
var env;
try { env = JSON.parse(utf8Decode(bytesFromBase64(blob))); }
catch (e) { throw new Error("archiveImport: not a valid l21x archive (bad base64/JSON)"); }
if (!env || env.fmt !== ARCHIVE_FORMAT)
throw new Error("archiveImport: not an l21x archive (fmt=" + (env && env.fmt) + ")");
if (!isArray(env.entries)) throw new Error("archiveImport: archive has no entries array");
var v = catalogValidate(env.entries);
if (!v.ok) throw new Error("archiveImport: archive failed validation (" + v.errors[0] + ")");
return env.entries;
}
// ---- small helper ----------------------------------------------------------
function isArray(x) { return Object.prototype.toString.call(x) === "[object Array]"; }
// ---- dual-runtime export ---------------------------------------------------
if (typeof window !== "undefined") {
window.LoopGifts = window.LoopGifts || {};
window.LoopGifts.l21x = {
encodeSnapshot: encodeSnapshot, decodeSnapshot: decodeSnapshot,
catalogSave: catalogSave, catalogLoad: catalogLoad,
catalogValidate: catalogValidate, catalogSort: catalogSort,
archiveExport: archiveExport, archiveImport: archiveImport
};
}
if (typeof module !== "undefined" && module.exports) {
module.exports = {
encodeSnapshot: encodeSnapshot, decodeSnapshot: decodeSnapshot,
catalogSave: catalogSave, catalogLoad: catalogLoad,
catalogValidate: catalogValidate, catalogSort: catalogSort,
archiveExport: archiveExport, archiveImport: archiveImport,
FORMAT: FORMAT, FORMAT_VERSION: FORMAT_VERSION
};
}
// ---- CLI (value-arg + --demo; reads NO files) ------------------------------
if (typeof require !== "undefined" && require.main === module) {
var args = process.argv.slice(2);
var DEMO_CATALOG = catalogSave(
catalogSave([], "note-1", { title: "Groceries", items: ["milk", "eggs"] },
{ title: "Groceries", updated: "2026-08-01" }),
"note-2", { title: "Ideas — café ☕", body: "persistence without a backend" },
{ title: "Ideas", updated: "2026-08-15" });
function printDemo() {
var archive = archiveExport(DEMO_CATALOG);
var back = archiveImport(archive);
var sorted = catalogSort(back, "updated", true);
process.stdout.write("# l21x-snapshot demo\n");
process.stdout.write("catalog entries: " + back.length + "\n");
process.stdout.write("archive blob length (base64): " + archive.length + "\n");
process.stdout.write("sorted by updated (desc): " +
sorted.map(function (e) { return e.id; }).join(", ") + "\n");
var loaded = catalogLoad(back, "note-2");
process.stdout.write("loaded note-2 title: " + loaded.doc.title + "\n");
process.stdout.write("round-trip ok: " +
(archiveExport(archiveImport(archive)) === archive) + "\n");
process.exit(0);
}
if (args[0] === "--help" || args[0] === "-h") {
process.stdout.write(
"l21x-snapshot — a zero-dependency browser document persistence layer\n\n" +
" node l21x-snapshot.js --demo build a sample catalog, archive it,\n" +
" round-trip it, and print the result\n" +
" node l21x-snapshot.js '<json doc>' encode one JSON document as an .l21x\n" +
" snapshot and print the base64\n\n" +
"Library: encodeSnapshot/decodeSnapshot, catalogSave/Load/Validate/Sort,\n" +
"archiveExport/archiveImport. Pure functions over values — no files, no DOM,\n" +
"no network. Exit 0.\n");
process.exit(0);
}
if (args[0] === "--demo" || args.length === 0) { printDemo(); }
else {
// value-arg: treat the argument as a literal JSON document, encode it.
var doc;
try { doc = JSON.parse(args[0]); }
catch (e) { process.stdout.write("not valid JSON; wrapping as a string document\n"); doc = args[0]; }
process.stdout.write(encodeSnapshot(doc, { title: "cli" }) + "\n");
process.exit(0);
}
}
test_l21x-snapshot.js128 lineson GitHub →
#!/usr/bin/env node
// SPDX-License-Identifier: MIT
/* test_l21x-snapshot.js — proves the persistence layer implements its contract.
THE ORACLE. There is no stdlib that says "is this catalog persisted right?" — so
the oracle is CURATED VECTORS expressed directly against what the functions
promise: (1) snapshot encode->decode round-trips an arbitrary document, including
full multibyte UTF-8; (2) decodeSnapshot REJECTS a foreign/garbage string rather
than half-parsing; (3) catalogSave returns a NEW catalog, replaces by id, and
never mutates its input; (4) catalogLoad returns the doc or null; (5)
catalogValidate reports duplicate-id and bad-snapshot honestly; (6) catalogSort
is stable, nulls-last, and leaves its input untouched; (7) archiveExport/Import
round-trips a whole catalog exactly. Plus determinism (encode is a pure function)
and a mutation-bite so a degenerate core (constant output / ignores input) fails.
Exit 0 = all pass; exit 1 = a failure (loud). stdlib only. */
"use strict";
var m = require("./l21x-snapshot.js");
var pass = 0, fail = 0;
function ok(label, cond) { if (cond) { pass++; } else { fail++; console.error("FAIL " + label); } }
function eq(label, a, b) { ok(label + " (=" + JSON.stringify(b) + ")", a === b); }
function threw(label, fn) {
var did = false;
try { fn(); } catch (e) { did = true; }
ok(label + " (throws)", did);
}
function deepEq(a, b) { return JSON.stringify(a) === JSON.stringify(b); }
// ---- 1. snapshot codec round-trip (incl. multibyte) ------------------------
var doc1 = { title: "Café ☕ — 日本語", n: 42, list: [1, 2, { deep: true }], nil: null };
var snap1 = m.encodeSnapshot(doc1, { title: "t", schema: "note" });
var back1 = m.decodeSnapshot(snap1);
ok("snapshot round-trips the doc", deepEq(back1.doc, doc1));
eq("snapshot carries title", back1.title, "t");
eq("snapshot carries schema", back1.schema, "note");
ok("snapshot is base64-ish (no raw braces)", snap1.indexOf("{") === -1);
// primitive & array docs also round-trip
ok("string doc round-trips", m.decodeSnapshot(m.encodeSnapshot("hi")).doc === "hi");
ok("array doc round-trips", deepEq(m.decodeSnapshot(m.encodeSnapshot([1, "two", 3])).doc, [1, "two", 3]));
// ---- 2. decode REJECTS foreign / malformed input ---------------------------
threw("decode rejects empty string", function () { m.decodeSnapshot(""); });
threw("decode rejects non-string", function () { m.decodeSnapshot(null); });
threw("decode rejects garbage base64->json", function () { m.decodeSnapshot("bm90IGpzb24="); }); // "not json"
threw("decode rejects a foreign-fmt envelope", function () {
// valid base64 of a JSON object whose fmt is wrong
var foreign = Buffer ? Buffer.from(JSON.stringify({ fmt: "other", v: 1, doc: 1 })).toString("base64")
: m.encodeSnapshot({}); // fallback; still non-l21x-fmt below
m.decodeSnapshot(foreign);
});
threw("encode rejects undefined doc", function () { m.encodeSnapshot(undefined); });
// ---- 3. catalogSave: new catalog, replace-by-id, no mutation ---------------
var cat0 = [];
var cat1 = m.catalogSave(cat0, "a", { v: 1 }, { title: "A", updated: "2026-01-01" });
eq("save grows the catalog", cat1.length, 1);
eq("save does not mutate input", cat0.length, 0);
ok("saved returns a NEW array", cat1 !== cat0);
var cat2 = m.catalogSave(cat1, "b", { v: 2 }, { title: "B", updated: "2026-03-01" });
eq("second save grows", cat2.length, 2);
var cat2b = m.catalogSave(cat2, "a", { v: 99 }, { title: "A2", updated: "2026-02-01" });
eq("replace keeps length", cat2b.length, 2);
eq("replace updates in place by id", m.catalogLoad(cat2b, "a").doc.v, 99);
eq("replace does not mutate prior catalog", m.catalogLoad(cat2, "a").doc.v, 1);
threw("save rejects empty id", function () { m.catalogSave([], "", {}); });
// ---- 4. catalogLoad --------------------------------------------------------
ok("load returns null for missing id", m.catalogLoad(cat2b, "zzz") === null);
ok("load decodes the stored doc", deepEq(m.catalogLoad(cat2b, "b").doc, { v: 2 }));
// ---- 5. catalogValidate: honest errors -------------------------------------
var good = m.catalogValidate(cat2b);
ok("valid catalog validates ok", good.ok === true && good.errors.length === 0);
var dupe = cat2b.concat([{ id: "a", snapshot: cat2b[0].snapshot }]);
var dupeRes = m.catalogValidate(dupe);
ok("duplicate id is reported", dupeRes.ok === false &&
dupeRes.errors.join("|").indexOf("duplicate") !== -1);
var badSnap = [{ id: "x", snapshot: "@@not-base64-json@@" }];
ok("bad snapshot is reported", m.catalogValidate(badSnap).ok === false);
var missingId = [{ snapshot: cat2b[0].snapshot }];
ok("missing id is reported", m.catalogValidate(missingId).ok === false);
// ---- 6. catalogSort: stable, nulls-last, input untouched -------------------
var sortSrc = [
{ id: "c", title: "Cherry", updated: "2026-03" },
{ id: "a", title: "Apple", updated: null },
{ id: "b", title: "Banana", updated: "2026-01" }
];
var byTitle = m.catalogSort(sortSrc, "title");
eq("sort by title asc first", byTitle[0].id, "a");
eq("sort by title asc last", byTitle[2].id, "c");
ok("sort does not mutate input", sortSrc[0].id === "c");
var byUpdated = m.catalogSort(sortSrc, "updated");
eq("nulls sort last on updated", byUpdated[2].id, "a");
var byUpdatedDesc = m.catalogSort(sortSrc, "updated", true);
eq("desc reverses order", byUpdatedDesc[0].id, "c");
// stability: equal keys keep input order
var ties = [{ id: "p", k: 1 }, { id: "q", k: 1 }, { id: "r", k: 1 }];
var tied = m.catalogSort(ties, "k");
ok("sort is stable on ties", tied[0].id === "p" && tied[1].id === "q" && tied[2].id === "r");
// ---- 7. archive round-trip -------------------------------------------------
var arch = m.archiveExport(cat2b);
ok("archive is one string", typeof arch === "string" && arch.length > 0);
var imported = m.archiveImport(arch);
ok("archive round-trips the catalog", deepEq(imported, cat2b));
eq("imported catalog is usable", m.catalogLoad(imported, "a").doc.v, 99);
threw("archiveExport refuses an invalid catalog", function () { m.archiveExport(dupe); });
threw("archiveImport rejects garbage", function () { m.archiveImport("bm90IGFuIGFyY2hpdmU="); });
threw("archiveImport rejects a snapshot-as-archive", function () { m.archiveImport(snap1); });
// ---- 8. determinism (encode is a pure function of its input) ---------------
ok("encode is deterministic", m.encodeSnapshot(doc1, { title: "t", schema: "note" }) === snap1);
ok("archive is deterministic", m.archiveExport(cat2b) === arch);
// ---- 9. mutation-bite ------------------------------------------------------
// A degenerate encode that ignored its input (constant output) would make two
// different docs encode identically. Prove they DON'T.
ok("MUTATION-BITE: different docs encode differently",
m.encodeSnapshot({ a: 1 }) !== m.encodeSnapshot({ a: 2 }));
// A degenerate catalogLoad that returned a constant would fail id discrimination.
ok("MUTATION-BITE: load discriminates by id",
m.catalogLoad(cat2b, "a").doc.v !== m.catalogLoad(cat2b, "b").doc.v);
// ---- report ----------------------------------------------------------------
console.log((fail === 0 ? "PASS" : "FAIL") + " " + pass + "/" + (pass + fail));
process.exit(fail === 0 ? 0 : 1);