Repository navigation
Project Tango v3.1.0
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.jsonformat — 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 instate.dband 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.jsonsnapshot tostate.db, using Node's built-innode:sqlite— no
new dependency, nothing to install, nothing to host. An existingstate.json
is imported on first run, validated record by record with exactly the rules
the JSON store used, and kept asstate.json.migrated. - Cross-process write transactions.
StateStoregained an optionalupdate,
and the SQLite store implements it asBEGIN 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 thev2.0.0lineage's
src/trie.ts— but as a ranking layer, not the filter it was there. See
the note below. PROJECT_TANGO_STORE=jsonto force the previous whole-snapshot backend,
for a runtime withoutnode: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 = WALreturnsSQLITE_BUSYimmediately, ignoring
busy_timeout, so opening the store raced: the loser gave up, fell back to
the JSON backend, and two processes then wrotestate.dbandstate.jsonas
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-tangocatalog. The store is
resolved — and therefore created — whenmockData.tsis imported, and
src/storage.test.tsasked 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.