Skip to content

Engine

The engine is the artifact behind every Telixon query: Google’s libphonenumber metadata compiled, at build time, into one deterministic automaton plus the lookup tables around it. It ships inside the npm package as compressed ESM modules and decodes once per process when ensureEngineReady is called; after that gate, digits drive one deterministic automaton compiled from Google’s metadata, read in batch by parsePhoneNumber and per keystroke by the input controller. This page covers the artifact and the machinery that loads it. What the automaton computes is on The model.

The artifact is generated, not handwritten. A build pipeline reads Google’s libphonenumber metadata at a pinned commit and compiles it into the automaton’s transition tables; the pinned inputs and the pipeline are documented on Provenance. All compilation happens at build time. The runtime decodes and reads the result; it never parses metadata.

It is one automaton, not a bundle of per-region tables. A single trie spans the calling-code digits and the national digits after them, and states are shared wherever digit prefixes are shared, across regions and across calling codes. Which region a walk lands in is an output of the walk, not an input to it, so no per-region subset of the tables exists.

The automaton and its lookup tables ship as nine layers. Each layer parses to a shallow record of flat typed arrays:

Module Layer Holds
walk.bin.js trie The transition table: per-state digit transitions, state flags, and profile ids.
callingCodes Per-state region data for the calling-code zone: reachable regions, the primary region, validity flags.
regionSelect Region disambiguation for shared calling codes: region slots plus a leading-digits trie per calling code.
verdict.bin.js verdict Verdict vectors: region, number type, and a validity bit per national length, for states that carry them.
exact Exact-acceptance entries: per-region masks of number types whose pattern matches the walk in full.
scope.bin.js scope Per-state region scope: number-type and terminal masks, profile ids, format and length mask pools.
regionMasks Flat per-region tables: length masks per number type and the national-prefix quick-reject mask.
metadata.bin.js metadata Territory records, format templates, example numbers, number-type names, placeholders, the string pool.
formatSelect Regex-free format selection: per-calling-code format records and a leading-digits trie.

Four of the nine (trie, verdict, scope, exact) are slices of the walk core; assembly snaps them back into one structure and fails if any layer is missing. The other five map across unchanged.

For the current artifact (see Provenance for its version), the nine payloads total 163 KB of base64 text over 122 KB of gzip, and decode to 642 KB of typed arrays.

The layers ship grouped into the four ESM modules in the table above. Each module default-exports a plain object mapping layer names to base64-of-gzip payload strings:

// walk.bin.js: one of the four engine modules
export default {
trie: 'H4sIAAAA...', // base64 of gzipped layer bytes
callingCodes: 'H4sIAAAA...',
regionSelect: 'H4sIAAAA...',
};

JavaScript modules, rather than binary asset files, because import is the one loading primitive every bundler and runtime resolves without configuration. A binary asset would need a fetch URL in the browser, a file path on Node, and asset-pipeline setup in every bundler; a module needs none of that. On the web, the four modules become code-split chunks; on Node and edge runtimes, they resolve as local files.

Gzip sits under the base64 because base64 alone inflates bytes by a third: compressing first is what makes the shipped text (163 KB) a quarter of the decoded tables (642 KB) instead of a third larger.

Two loader strategies produce the same decoded layer bytes and hand them to the same parser.

LazyResourceLoader backs the async entry points. It dynamic-imports the four modules in parallel and decodes each layer with an injected async decode function. Because the imports are dynamic, the engine stays out of the initial bundle on the web, and nothing downloads until ensureEngineReady is called. A transient import failure, such as a flaky fetch of one chunk, is retried up to three times with linear backoff before the load rejects. On-demand loading cannot be synchronous, so this loader has no sync channel.

EmbeddedResourceLoader backs the @telixon/core/sync-init entry. It imports the four modules statically, so the payloads are already present in the deployed script when it evaluates, and decodes them synchronously on the calling thread. This is what makes ensureEngineReadySync possible, and why the sync initializer lives on a separate entry: a static import forces the payload into the bundle, and that cost must be opt-in.

Which entry point pairs which loader with which decode is the environment matrix on ensureEngineReady.

Each layer decodes in two steps: base64 to gzip bytes, then gunzip to the raw layer bytes. Three strategies cover every runtime:

  • Native. Node’s zlib. The async variant runs gunzip on the libuv threadpool, so inflating the engine never blocks the event loop; the sync variant serves the Node sync-init entry.
  • Stream. DecompressionStream on the web and edge, which runs the inflate off the main thread. Base64 decodes through the runtime’s native Uint8Array.fromBase64 where present, else the pure-JS decoder. Where DecompressionStream is absent, the strategy falls back to the pure path.
  • Pure. Pure-JS base64 and gunzip with no platform dependencies. The floor that runs anywhere, and the default for the sync entry outside Node, including edge global scope, where async APIs are unavailable.

The decoded bytes parse into typed arrays either all at once or module by module as chunks arrive, with the four core slices assembled back into the walk core at the end. Parsing is pure: no I/O, no decompression.

Every layer opens with a header the parser verifies before reading a single table:

  • a magic number, rejecting bytes that are not a Telixon engine binary;
  • a layer id, rejecting a layer handed to the wrong parser;
  • a format version, rejecting an artifact from a different release with “engine and artifacts must come from the same release” rather than misreading its tables.

After parsing, the provider cross-checks the artifact against the region table compiled into the runtime: the metadata must contain exactly the known regions, in the same order, and any drift throws instead of returning shifted lookups. It then derives the small index tables the hot path needs, such as region code to index, calling code to index, and the formatting placeholders, once, so no query rebuilds them.

The parsed engine lives in a process-wide provider registered on globalThis under a Symbol.for key. Duplicate copies of the library in one process, such as two bundles or a dependency that bundled its own copy, resolve the same symbol and share one provider, so the tables decode once no matter how many copies run.

The observable contract, idempotency, one shared in-flight load, and retry after a failed load, is specified on ensureEngineReady; the provider implements it with a single in-flight promise and a ready flag. One interleaving is worth spelling out: if ensureEngineReadySync completes while an async load is still in flight, the sync engine wins, and the async load discards its result when it lands. Both paths end at the same materialized engine.

Two design decisions follow from what the tables are.

Out of bundle, loaded whole. Since the automaton shares states across regions and region is an output of the walk, there is no meaningful fragment to lazy-load per region; the engine loads as one artifact, and every query after the gate is synchronous. Keeping that artifact out of the main bundle on the web is what the dynamic imports are for.

Consumer-triggered. Importing @telixon/core never fetches or decodes anything; loading starts only when an initializer is called. The load moment is the consumer’s decision: at startup, behind a route, or on first focus of a phone input. Load the engine covers choosing it per environment.

  • ensureEngineReady: the API over this machinery, with the per-environment entry and decode matrix.
  • The model: what the automaton computes; validity is an accepting state, and every ValidationError variant is a named way of not being in one.
  • Performance: what initialization and per-query reads cost.
  • Provenance: the pinned Google libphonenumber commit and the generation pipeline.