Skip to content

Standalone Modules ​

protoarray compiles each schema into specialised functions with new Function. Where compiling source text is forbidden (a Content-Security-Policy without 'unsafe-eval', Cloudflare Workers, Vercel Edge), it falls back to the closures backend, which is correct but slower (see Runtimes). A standalone module brings the generated code along instead: it is written once, at build time, and the runtime loads it like any other module.

protoarray compile ​

sh
protoarray compile src/schemas.js --out src/schemas.compiled.js

The command loads a module (CommonJS or ES), takes the schemas it exports and writes a module with their code and the code of every schema they reach: the entities they reference, recursion included. An export is a schema when it is a protoarray Schema or a metaschema Schema (which the command builds with Schema.from); a metaschema Model stands for its entities, each under its own name. The module of definitions can stay the one the server validates with:

js
// src/schemas.js
const { Schema: Definition, Model } = require('@alexify/metaschema');

const model = new Model({}, new Map([
  ['Address', { Struct: {}, city: 'string', zip: '?string' }],
  ['User', { Entity: {}, name: 'string', home: 'Address' }],
]));
const Ping = new Definition('Ping', { at: 'date' });

module.exports = { model, Ping }; // compiles to the exports Address, User and Ping

Options:

Option
--out <file>where to write; stdout without it
--format cjs | esmthe module format, cjs by default
--runtime import | inlinewhat the module needs at runtime, import by default (below)
--only A,Bthe named exports only

A module whose export is one schema gives a module with that schema as its default export; otherwise the schemas keep their export names. The generated module needs no metaschema.

toSource ​

The command is a wrapper around toSource from @alexify/protoarray/standalone, for a build script of your own:

js
const fs = require('node:fs');
const { Schema } = require('@alexify/protoarray');
const { toSource } = require('@alexify/protoarray/standalone');
const { model } = require('./schemas.js');

const { User, Address } = Schema.fromModel(model, { entities: { Address: { sparse: true } } });
fs.writeFileSync('schemas.compiled.js', toSource({ User, Address }, { format: 'esm' }));

The options a schema was built with travel with it (sparse, layout, delta, intern, references, types, validate, stringify).

toSource(schema) writes one schema as the default export, toSource({ A, B }) named exports. register() from the same subpath adds schema.toSource(options).

The two runtimes ​

runtimeThe moduleExports
'import' (default)imports @alexify/protoarray and @alexify/protoarray/standalonereal Schema instances: the whole API, instanceof the package's errors
'inline'carries the source of the package's error classes, type table, layouts and base64, and imports nothingobjects with name, id, keys, layout, backend, encode, decode, stringify, parse, toString

With import, the module embeds the snapshots its schemas were read from, cut to the entities its schemas reach (an entity nothing reaches is not read again), and builds each one from its snapshot as usual, with the generated codec passed as the compiled option: Schema.fromModel(snapshot, { entities: { User: { compiled, … } } }), the entities of one snapshot together so that their references reach one another. Its backend is 'standalone', and snapshot is the embedded one. The snapshot is checked against the code: a module older than its schema throws SchemaDefinitionError ERR_COMPILED_MISMATCH when it loads, so a stale build fails loudly instead of misreading payloads, and so does a module generated by another version of protoarray than the one it imports ("generate it again"). Nothing is compiled while it loads: a collection in a layout of its own is written in place, and the records of a rows collection carry their codec for the stream helpers. What a standalone schema derives at runtime (schema.list) uses the closures backend where code generation is blocked, and is the one thing that tries code generation where it is allowed.

inline suits a runtime where even the package itself is unwelcome. Its errors are classes of the module's own, so tell them by name and code, not by instanceof.

Either way the module compiles no source text to load, and its schemas run at the speed of the codegen backend: on the benchmark's nested record, ×1.5 faster to stringify and ×1.85 faster to parse than JSON of the keyed object, where the closures backend a runtime without code generation would otherwise use is about as fast as JSON (×1.5 and ×2 slower than the module).

What it does not write ​

  • A schema from resolve: it is built at runtime from two schemas; write the writer and the reader, and resolve them at runtime.
  • schema.list: write the schema, and take its list at runtime.
  • A custom fingerprint function: the module carries each schema's id as it was computed, and the schemas it derives at runtime (list) use the default fingerprint.
  • The metaschema schema: a standalone schema's source is null. Values are validated where metaschema runs, if anywhere; the module itself encodes and decodes.

Regenerate the module whenever a definition changes and whenever protoarray is upgraded; the ERR_COMPILED_MISMATCH check is there for the times one forgets. Run toSource (or protoarray compile) from the package as installed, not from a bundle of it: the inline runtime writes the source of the package's functions, which a bundler may rewrite with helpers of its own (keepNames, an older target) the module would not have.

Released under the MIT License.