Repository navigation
Conversation
…ce per listed code Before trying each listed concept as the same concept in another form (ICD-11's short code and entity id), checkConceptSet called sameConceptDifferentForm for every listed concept, and each call located the code to ask whether it has another form at all. That answer does not depend on the listed concept, so it is now asked once per include and the loop runs only when it is yes. Same answers. A miss against a 33,000-code VSAC RxNorm list used to get no answer within 600 s (a 1,185-code list took 99 s); a 300-code list in the new test located the code 301 times, now a handful. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
processCodes was an empty stub, so no provider was ever handed a compose even though
handlesSelecting()/processSelection() are declared. It now calls processSelection with the
compose's includes and excludes and the request's offset and count. The provider returns the
page and the total of the whole selection ({sets, total, notClosed}), or null to decline, and
then the compose takes the usual include/exclude path. Composes with a text filter, an
enumerated concept list, or a filter with no value are not handed over.
scanValueSet now records the compose's system and version (processCodes needs them) and reads
include.valueSet (it read the lowercase spelling, which never matched), so a compose that imports
a value set is not called simple. The up-front size guard in checkSource is skipped when the
provider will page the compose itself.
Main's providers return handlesSelecting() === false, so nothing changes for them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
$expand locates each concept an include or exclude lists, and $validate-code locates the listed codes when it compares them with a code in another form. For main's in-memory providers each locate is a memory read; for a database-backed provider each is a round trip, and nothing said a run of codes was coming. prefetchCodes(codes, withDesignations) is called once with the whole list before those loops, so such a provider can read them together. The default does nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
$validate-code walked an include's concept array to find the code. A value-set provider that
keeps lists as rows (VSAC lists of 33,000 to 57,000 codes) would have to build that whole array
on every request. An include may now carry a stored-list handle (tx/library/stored-concepts.js:
{count, lookup(code)}); validate.js asks it for the entries with the code, in determineSystem
and checkConceptSet, and the renderer's include summary shows the count instead of every code.
The whole list is read only when the code may be listed in another form (ICD-11), after the
once-per-include hasOtherForm question. An ordinary array behaves exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng their maps applyTHOPrecedence found THO's value sets by checking instanceof PackageValueSetProvider and walking its valueSetMap, and found the core package the same way. A value-set provider that keeps its value sets in a database has no such map. Value-set providers now answer two questions: thoVersions(map) adds the url -> versions of a THO package's value sets, and yieldToTHO(map) keeps only pinned references for a core package. The base class defaults do nothing, and PackageValueSetProvider's implementations are the old loop and the old check, so the result is the same. The startup log uses the existing vsCount() rather than valueSetMap.size (so it now shows the number of value sets, not of map keys). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FhirCodeSystemProvider.create picks a provider class from a registry keyed by the CodeSystem's url (providerClassFor: ICF today), so it can't pick one by where the content is kept. A CodeSystem object may now have a providerFactory(opContext, supplements, cls) method, asked before the registry and told the class the registry would use; the provider it returns is used, and null leaves it to the registry. A package CodeSystem kept in a database can then get a provider that reads it there, while version, THO-precedence and supplement handling stay as they are. CodeSystems without the method, which is all of main's, get the same provider as before, ICF included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…naming url|version checkSource's up-front too-costly refusal of a whole-system include is skipped for a compose handed to the provider, so an over-limit selection was refused later by includeCode, which names the plain url (tx-ecosystem loinc/loinc-expand-all expects url|version). processCodes now applies the same condition and message once the provider has reported the selection's total. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oviders may refuse as too costly
processSelection gets an optional trailing argument, context = { valueSet: { url,
version } }, the value set whose compose it is handed, so a provider that
refuses can name it without reading the request's state. The default is {} and
a provider that ignores the argument behaves as before.
cs-api.js and cs-api.md document that a provider may throw the standard
too-costly Issue, with the exact form expand.js uses, and (cs-api.md) the
over-the-limit refusal processCodes applies to a whole-system include.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A specialised provider class (ICF) must be known when the server loads, so that code storing package CodeSystems elsewhere (a database, say) can leave those to the url registry. Exported unchanged; a test pins what it returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the limit The whole-system too-costly check applied in processCodes copied the usual path's condition (offset + count must fit the limit). That condition exists because the usual path builds every code before the page; a provider that pages itself does not, so a deep page (offset 200,000, count 10) was refused although filtered selections page freely. Only count is checked now; a request with no count, or a count over the limit, is still refused as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Core interfaces for database-backed providers
This PR adds a few small hooks to FHIRsmith's code-system and value-set provider interfaces. A provider that keeps its content in a database (such as the SQLite-backed engine we have proposed) needs them to answer some questions without loading everything into memory.
Every hook defaults to today's behaviour. Main's providers don't override any of them, so they take exactly the same paths as before. The PR is interfaces only: there is no new provider and no new configuration.
Each change below is one commit with its own tests. For each one we say what we need, what the PR does, and the other options we would also be happy with. If you'd rather have one of those, tell us and we'll redo that commit in that shape.
Related PRs:
getPrepContext(iterate, languages), with the languages passed from expand.js and validate.js. Nothing here duplicates it. This branch applies cleanly on top of 2026 11 gg big bug hunt #331 and its tests pass there.scanAllValueSets()for the usage scan and the paged-searchloadPage. They belong with change 4 below, but they are not repeated here.1. Hand a whole compose to the provider
What we need. When a value set selects from one code system, the provider should get the whole compose: includes, excludes, offset and count. It should return one page with an exact total. For example,
$expand?url=http://snomed.info/sct?fhir_vs=isa/404684003&offset=50000&count=10gets a 422 "too many codes to produce" from tx-dev.fhir.org. A provider that pages by itself answers with 10 codes and a total of 129,013 in about a third of a second.handlesSelecting()andprocessSelection()are already declared. expand.js setsvsInfo.handleByCS, butprocessCodes, the method it calls next, is an empty stub, so no provider is ever handed a compose.What this PR does. It fills in
processCodes:handleByCSis set, it callscs.processSelection(params, includes, excludes, excludeInactive, offset, count, context).contextis a new optional last argument:{ valueSet: { url, version } }, the value set whose compose this is. A provider that refuses can name the value set without reading the request's state. Providers that ignore it behave as before.null, the compose takes the usual include/exclude path, unchanged.The return contract is in cs-api.js and cs-api.md:
{ sets, total, notClosed }, ornullto decline.setsholds just the page.totalis the size of the whole selection, or -1 if the provider hasn't counted it.The old comment said the method returns
FilterConceptSet[]. We wrapped the sets in an object because a page needs a total that the sets themselves can't carry. Nothing implemented the old shape.A provider may also refuse as too costly. cs-api.js and cs-api.md say it may throw the standard too-costly Issue, and show the exact form
$expanduses when it refuses a value set itself:Here
vurlisurl|versionfromcontext.valueSet.Three small fixes were needed to make this path live:
scanValueSetnow records the compose's system and version.isSimpleSelectchecked the misspelledinc.valueset, so a compose that imports a value set counted as "simple". It now checksinc.valueSet.checkSourceis skipped when the provider will page the compose itself.processCodesapplies it, with the same message, once the provider has reported the total. Only the page size counts against the limit: the usual path needs offset + count to fit because it builds every code before the page, but a provider that pages itself does not, so a deep page (offset 200,000, count 10) is answered as it is for filtered selections. A request with no count, or a count over the limit, is refused as before. Without it, an over-limit whole-system include was refused later byincludeCode, whose message names the plain url instead ofurl|version(tx-ecosystemloinc-expand-allchecks this).Tests (tests/tx/expand-process-selection.test.js) use a fake provider that handles selection. They cover:
count=0;contextthe provider is told, and a provider that throws the too-costly Issue.Other options we'd be happy with:
processSelectiononly whencountoroffsetis given. Unpaged expansions of big sets would still take the slow path.FilterConceptSet[]and read the total fromfilterSize(), with the rule that the provider returns only the page. That makesfilterSizemean two different things, which is why we didn't choose it.Possible later widenings. This PR doesn't change these, but they are where a provider still can't take a compose, or can't serve the response:
used-valueset) but ask the provider for an opaque set handle for each simple selection, and, when the pieces come from the same provider, ask it to combine them (union, intersect, exclude) and page the result. The rules stay in one place, and the work happens on sets.includeCoderefuses at the limit. The smallest change would be to skipincludeCode's limit check when the provider pages (vsInfo.csDoOffset), and cap at a limit the provider reports instead.2. Look up a list of codes together
What we need. When a value set lists codes (
compose.include.concept), a database-backed provider should be able to read them in a few statements instead of one at a time. For example, expandingus-core-documentreference-type(about 3,000 listed LOINC codes) took about 3 statements per code: 9,114 statements and 2.4 s from a cold start. With a hint before the loop, it takes 12 statements.What this PR does. It adds
async prefetchCodes(codes, withDesignations)toCodeSystemProvider. The default does nothing. expand.js calls it once, with the whole list, before the loops that locate each listed code of an include or exclude. validate.js calls it before the loop that compares each listed code with a code given in another form. A provider may read the codes together and answer thelocate()calls that follow from that read, for this request only. A test checks that the provider gets the list once, before the first locate.Other options we'd be happy with:
locateMany(codes)would return the contexts, and the workers would use them directly. It is cleaner, but it changes the loops in expand.js and validate.js.processSelection. That covers simple expansions, but not validate-code or composes that use several systems.3. Ask a value set about one code
What we need.
$validate-codeagainst a value set that lists thousands of codes should ask "is this code listed?" without reading the whole list. Today validate.js walksinclude.conceptas an array, so a provider that stores lists as rows must build that array on every request. For a code that is not in the list:What this PR does. An include may carry a stored-list handle,
{ count, lookup(code) }, under a well-known symbol (tx/library/stored-concepts.js). Where it has one:determineSystemandcheckConceptSet.hasOtherForm.A plain array behaves exactly as before. The include's
conceptproperty must still give the whole list when read, so code that needs every entry keeps working.Tests use a stored list and compare its answers with a plain array:
They also check that the list is not read when it doesn't need to be.
Overlap with #332. The "does this code have another form?" question is asked once per include (9ab694a, also in #332). This PR builds on that same logic, as described at the top.
Other options we'd be happy with:
containsCode(valueSet, includeIndex, system, code)on the value-set provider. It is more explicit, but the worker has to know which provider a value set came from.4. Ask value-set providers instead of reading their maps
What we need. Core code that reads a provider's in-memory
valueSetMapshould ask the provider instead, because a database-backed provider has no such map.applyTHOPrecedencein provider.js does this twice: it finds THO's value sets withinstanceof PackageValueSetProviderand a walk overvalueSetMap, and it finds the core package the same way.What this PR does.
Two new methods on the value-set provider API.
thoVersions(map)adds the url → versions of a THO package's value sets.yieldToTHO(map)keeps only the pinned references for a core package.Both default to doing nothing.
PackageValueSetProviderimplements them with the old loop and the old core-package check, so the result is the same, and provider.js just asks every provider.Shared THO check.
isTHOPackagemoves to tho-precedence.js so the two places that need it share it.Startup log. tx/library.js now uses the existing
vsCount()instead ofvalueSetMap.size. The "VS" column now shows the number of value sets. It used to show the number of map keys (id, url and url|version for each value set), so the figures are lower.Tests check that
thoVersionsgives exactly what the old loop gave, and thatyieldToTHOaffects core packages only. They also check that a provider with no map can take part.The concept-map side of
applyTHOPrecedencehas the same pattern. We left it alone because no current need touches it, but the same two methods would fit there.The other two reach-ins are the usage scan (
scanAllValueSets()) and theValueSet?search page (loadPage). Both are in #333. On 40,267 VSAC value sets, the scan drops from 10 s to 1.9 s, and a page of search results from 680 ms (+200 MB) to under 125 ms.Other option: a shared base class that owns the maps. We don't recommend it, because it would force every provider back into memory.
5. Let a stored CodeSystem choose its provider
What we need. A package CodeSystem kept in a database should get a provider that reads it there. Main's version, THO-precedence and supplement rules must still apply.
FhirCodeSystemProvider.createpicks a subclass from a registry keyed by url (providerClassFor, which holds ICF today). That can't pick a provider by where the content is kept.What this PR does.
FhirCodeSystemProvider.createfirst asks the CodeSystem object's optionalproviderFactory(opContext, supplements, cls). Hereclsis the class the url registry would use, so a factory can leave registered subclasses such as ICF alone.A non-null result is used.
null, or no method, leaves the choice to the registry as before.No CodeSystem in main has the method.
providerClassForis now exported from cs-cs.js. Code that stores package CodeSystems when the server loads has to know which ones need a specialised class (ICF today), so it can leave those to the registry.Tests cover the case without a factory (including ICF), a factory that supplies the provider, and a factory that returns null for ICF. A test also checks what
providerClassForreturns.Other options we'd be happy with:
providerClassForwould also take the CodeSystem object, so an entry can match on its kind as well as its url.instanceofcheck in provider.js for the stored CodeSystem class. It is the smallest change, but it ties core to our class.Testing
currenttest package was refreshed on 2026-11-02, and on it main fails 66 of 4,565 tests. This branch fails exactly the same 66, so it adds no failures. On the package before the refresh, this branch (before its last three commits) passed 4,565 / 4,565, the same as main.🤖 Generated with Claude Code