Skip to content

Project Tango v3.1.0

Choose a tag to compare

@CoderGamerUnknow CoderGamerUnknow released this 04 Oct 13:02
· 13 commits to main since this release

Upgrade: install 3.1.1 before trusting this release.

3.1.0 shipped two data-loss defects. If your saved state holds fewer products
than the seed catalog — which happens after any upgrade from the old
state.json format — this release lists all thirteen products and then
refuses to sell one of them, committing the shortfall so it survives every
restart. It also reported a rejected save as a successful one, so an order can
come back with a receipt that was never written.

3.1.1 repairs this on its own. Its first transaction re-adds the missing
seed products and writes the repaired catalog back, so no manual step is
needed — install it and restart the server. Nothing you placed is lost; orders
recorded against the missing products are still in state.db and resolve again
once the products are back.

State is now durable across processes, and the v2.0.0 lineage's two useful
ideas are in main. Tool names, parameters and response fields are unchanged
from 3.0.1, except that list_all_products now returns its matches ranked.

Added

  • A SQLite durable store (src/sqliteStore.ts). State moved from a
    state.json snapshot to state.db, using Node's built-in node:sqlite — no
    new dependency, nothing to install, nothing to host. An existing state.json
    is imported on first run, validated record by record with exactly the rules
    the JSON store used, and kept as state.json.migrated.
  • Cross-process write transactions. StateStore gained an optional update,
    and the SQLite store implements it as BEGIN IMMEDIATE — the write lock is
    taken before the state is read, so a second server validates against what
    the first one committed rather than against a snapshot it has already moved
    past. MockDataProvider.transact() uses it, so order placement's whole
    read-validate-write is exclusive. This is the change that makes concurrent
    writes safe: ten server attempts at 5 units against a stock of 42 fill
    exactly eight orders and refuse the other two with a reason, where before the
    last writer's snapshot simply overwrote the others.
  • Search ranking (src/productIndex.ts), ported from the v2.0.0 lineage's
    src/trie.ts — but as a ranking layer, not the filter it was there. See
    the note below.
  • PROJECT_TANGO_STORE=json to force the previous whole-snapshot backend,
    for a runtime without node:sqlite (Node < 22.5). It keeps publishing one
    process's complete snapshot and is still last-writer-wins across processes;
    the README says so where it documents that flag.

Fixed

  • Two servers starting together could split one catalog across two files.
    PRAGMA journal_mode = WAL returns SQLITE_BUSY immediately, ignoring
    busy_timeout, so opening the store raced: the loser gave up, fell back to
    the JSON backend, and two processes then wrote state.db and state.json as
    separate catalogs that each believed they owned the stock. The store now
    retries the open with backoff and waits out a lock another process holds
    instead of routing around it.
  • Falling back to a different on-disk store is worse than losing durability.
    Two files disagreeing about inventory is not recoverable; losing state across
    a restart is. A store that cannot be opened now degrades to memory with a
    warning on stderr, never to a second file.
  • A test run could create the real ~/.project-tango catalog. The store is
    resolved — and therefore created — when mockData.ts is imported, and
    src/storage.test.ts asked for the default directory to check its
    description. The runner now pins a throwaway data directory for every test
    process (src/testGlobalSetup.ts), and a suite-hygiene test fails if that
    wiring is ever dropped.

Known limitation

Reads are served from a running process's own in-memory catalog, so a
long-lived server can trail another process's last commit until it writes
something itself. Writes never read a stale snapshot — they re-read under the
transaction lock — and a freshly started process is always current. Two servers
driven from one client are the case this shows up in; the lifecycle suite reads
the final state through a new process for exactly that reason.

Why the v2.0.0 trie was ported as ranking

v2.0.0 replaced substring search with prefix-only matching and called it an
improvement. Measured against this catalog, it is not: "tch" matches 7
products by substring and 5 by token prefix, and "a" matches 13 against 6.
Substring matching reaches inside tech and stitch; a prefix only matches from
the start of a token. Shipping it as written would have cut recall while
looking like a speed-up.

So the index here ranks instead of filtering. filterProducts still decides the
candidate set, and the trie only orders it — a product whose own token is the
query outranks one that merely contains it, on three tiers (the query is a whole
name/SKU/category/tag, is one word of one, or starts one). The tier is what
separates Cable from Cable Management Tray: both carry a cable token, and
a token-level comparison alone lets the longer name win on accumulated prefix
matches.