Repository navigation
Conversation
Public Triples reads always resolve a temporal basis before reaching the
KV layer, so the entity-type fast path (basis === undefined) never ran
for them: every match({ entityType }) became a temporal scan of the
whole store, journal included. ConfigStore.load issues five of these,
so each ConfigRuntime.evaluate scanned every fact five times.
Add getByEntityTypeTemporal (and its async twin) to KvTripleStore: read
the TYPE index, then keep the datoms visible at the basis. Retraction
only rewrites META, so the index still names every datom asserted with
the type and visibleAt decides recorded and valid time as before.
On an in-memory store with 16,000 facts, ConfigRuntime.evaluate drops
from 480-985 ms to 1-3 ms. Found while running Triplex in the browser
behind WorldVM's programmable-operations demo.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every `resolveRef`, `snapshotById`, `commit` and `setRef` called `load()`, which reads every config record and rebuilds every snapshot's node tree. `ConfigRuntime.evaluate` therefore grew with the whole config history, and on a large in-memory store each of its five entity-type reads scanned every fact. - `resolveRef` reads the ref entity, then `snapshotById` reads the snapshot record and only the objects reachable from its root, one batched `entities` read per tree level. - Object bodies, type expressions and snapshot records are content-addressed or immutable, so they are cached by identity for the life of the service. A decoded node's `type` comes from `validUnder`, which later releases can extend, so decoded trees are cached only until `:triplex/valid-under` changes. One `dependencyState` call checks that, which stays correct when another process commits. - `commit` gives the pure transition a partial store: the input subtrees' stored objects and schemas, those objects' revision histories, and the latest snapshot. It checks objects the transition mints outside the inputs (the snapshot root) before persisting them. - `setRef` and `reverseDependencies` read single records instead of loading. `ConfigStore.commit` no longer returns a `store`; `load()` is unchanged for whole-store views. Tests pin the new reads to the full-load semantics: a release sequence produces the same snapshot IDs and revisions as the pure store, resolved refs and snapshots equal the full load while a second instance commits, moves refs and extends `validUnder`, and resolving a ref issues no entity-type scans. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every
ConfigStore.resolveRef,snapshotById,commitandsetRefcalledload(). That reads every config record (objects, types, revisions, snapshots, refs) and rebuilds every snapshot's node tree.ConfigRuntime.evaluateresolves a ref on every call, so its cost grew with the whole config history. On the in-memory store, each ofload()'s five entity-type reads also scanned every fact. WorldVM works around this by caching ref resolutions itself.Config staying in triples is not the problem. It keeps releases, ref moves and application facts in one transaction and one journal, with bitemporal ref history and Datalog impact queries. The access pattern was the problem.
Change
resolveRefreads the ref entity, thensnapshotById.snapshotByIdreads the snapshot record and only the objects reachable from its root, one batchedentitiesread per tree level.Caching.
:triplex/valid-underchanges. A node'stypecomes fromvalidUnder, which later releases can extend. OnedependencyStatecall checks this; it's an indexed query on SQL, and it stays correct when another process commits.commithands the pureInMemoryConfigStore.commita partial store:It checks objects the transition mints outside the inputs (the snapshot root) before persisting them.
setRefandreverseDependenciesread single records.load()is unchanged. The CLI andrecheckstill use it for the whole-store view.Breaking (pre-1.0, minor changeset):
ConfigStore.commitno longer returnsstore. Nothing in the repo used it; callload()instead.The first commit is PR #46, cherry-picked unchanged: entity-type matches at a temporal basis read the type index instead of scanning. The targeted reads still do a few typed reads, and this PR shouldn't depend on #46 landing first. If #46 merges first, this rebases cleanly.
Numbers
In-memory KV, Node 24. Each release has 21 config nodes. Each figure is the median of five warm
ConfigRuntime.evaluatecalls on one rule.mainwithout #46main+ #46dependencyStatecall, one ref read and a cache hit, whatever the history size. The first evaluate after avalidUnderchange rebuilds only the requested snapshot (12 ms at 200 releases).main's in-memory backend, and is slower with many facts (16k facts: 29 ms → 46 ms). Its targeted reads touch the entity, attribute and value indexes, and onmainthe in-memory backend re-sorts a whole index after every write.main's 60 ms).Tests
test/config/ConfigStoreReads.test.ts:InMemoryConfigStore. That includes a release that only proves an existing body valid under a wider schema, and a revert to an earlier release. Afterwards the full load's revisions, objects, schemas andseqmatch.ConfigStoreover the same triples commits, moves refs, and extendsvalidUnderon a node already in a cached snapshot. EveryresolveRefandsnapshotByIdon the first instance still deep-equals whatload()reconstructs, including node types. With thevalidUnderfreshness check disabled, this test fails.reverseDependenciesissue no entity-type reads.All existing config, CLI, HTTP, dashboard, example and integration tests pass unchanged.
pnpm checkis green.🤖 Generated with Claude Code