ConstReads a layer's offline entities back as a GeoJSON FeatureCollection.
🛑 FIRST READER OF THE features STORE. The store existed with neither
producer nor consumer: DBFeatures was referenced only by
db-modules-registry.ts. Its writer came later — see
IndexedDB.putLayerFeatures just below.
⚠️ Returns a FeatureCollection and not the raw records, because the caller is
the kernel's layer loader: it expects the same shape a fetch would have handed
it, and diverging would force the seam to distinguish two shapes — the very
distinction this read exists to remove.
⚠️ Returns null — not an EMPTY collection — when the layer was NEVER PULLED, so the
caller can fall back to the network rather than display zero entities believing it has
read.
🛑 An empty store is not an unread one, and reading them alike showed STALE DATA. A
layer whose source no longer holds anything — or whose download zone holds nothing — left
an empty store; the loader read that null as "never pulled" and fell back to data.*, a
static display file possibly months old, presented as the user's own entities, silently.
The convergence of 19/09/2026 made it frequent, a complete pull now removing what it did
not return. The distinction lived in offline.pullState and nothing read it: a COMPLETE
run (outcome: "ok") speaks for the layer's content, empty included. A run that failed or
was cut does not — its store is empty because it stopped — and the fallback stays.
🛑 Only the active profile's work is drawn. The store is shared by every profile of the origin and keyed by layer id; the queue says whose capture an entity holds. An entity holding only another profile's queued work is left out — and a layer that work alone fills reads as never pulled —, and only this profile's deletions are hidden. An entry captured before 3.18.0 carries no profile and counts for every one. ⚠️ Known limit: an entity the other profile edited, and that this profile's own pull also serves under the same key, stays out of this layer until that edit leaves the queue.
🛑 Only the cache of this layer's own source is drawn. A synced record carries the
source that served it (FeatureRecord.sourceKey): what another profile's layer of the same
id pulled from another source is left out, and a pull concluded from that other source does
not make this layer read as pulled (offline.pullState carries its source too). A record
written before 3.18.0 carries none and is drawn, as before.
Layer identifier.
The collection — empty when a complete pull of this layer's source returned
nothing — or null when no pull of it ever concluded, or when only another profile's
work or another source's cache fills it.
Applies a local edit — the entity AND its enqueueing, in ONE transaction.
Write mirror of IndexedDB.getLayerFeatureCollection: the facade delegates,
it does not arbitrate. Coalescing and cancellation live in db/local-edit.ts,
the only place where both stores fit in a single transaction.
⚠️ Checks NO edit right. The standing invariant — pulling never confers editability — is held one layer up, where the layer's declaration is readable. Putting it here would make it a storage rule, hence bypassable by any caller speaking to the database directly.
The edit to apply.
What was done (merge, cancellation, new entry), or null without the module.
Writes a pulled batch into the features store — the store's FIRST writer.
Mirror of IndexedDB.getLayerFeatureCollection: the facade delegates, it
does not arbitrate. The rule "never overwrite an unsynchronised capture" lives in
db/features.ts, the only place where read and write fit in one transaction.
⚠️ Every record must carry a defined feature. getLayerFeatureCollection
decides its null on records.length === 0, before filtering undefined
features: a batch written without geometry would make it return a collection
that is empty rather than null, and the loader would display zero entities
believing it has read.
Complete records, feature included.
Entities the source served as deleted: never written, their synchronised copy removed. Optional.
The real tally — written, preserved, unchanged, removed, and the keys the
batch named (seen) — or null when the module is absent.
Removes what a complete pull did not return — the second half of its convergence.
Mirror of IndexedDB.putLayerFeatures: the facade delegates, it does not
arbitrate. What is removed is decided in db/features.ts (sweepSynced), in one
transaction: synced records carrying a server identity that keep does not name.
Local work and records without a server identity are never touched.
The layer the pull covered.
The keys every page of the pull named.
OptionalsourceKey: string
The source pulled: only its records, and those written before records carried one, are candidates. Omitted, every record of the layer is.
How many records were removed, or null when the module is absent.
Counts what the sync report needs, per layer, in one pass.
🛑 ONE method rather than four. Composing the report needs three per-layer
tallies; exposing them one by one (countByLayer, listByState…) would widen the
facade by four members for a single consumer, and move knowledge of the stores
out of the layer that owns them. The facade delegates, it does not arbitrate —
but what it delegates is the tally, not the stores.
⚠️ Only the active profile's entries are counted: an entry captured under another profile cannot leave from here, and a layer of the same id would otherwise count it.
⚠️ pendingCount aggregates pending + inFlight + failed. It is "what is
still owed to the server", and failed belongs there: it was explicitly put back
into the replayable set, precisely because a failed entry is a field capture with
no other copy. quarantined is counted apart — the contract describes it as
"kept, visible, but not replayable as-is", so it is not the same debt.
Layers to count. A layer with no entity yields zeros, never nothing.
The tallies per layer identifier, or null when the modules are absent.
Lists the edits still owed to the server, each joined to the entity it edits (4.10).
This is what the "local POIs" panel exports: the WORK, not the cache. An outbox
entry does not carry the payload — it references [layerId, localId] — so the
join towards features happens here, where both stores are open.
⚠️ An entry whose entity has vanished from the store is returned with
feature: null rather than discarded. A capture we can no longer describe is
still a capture owed to the server; silencing it in an export whose very role is
to get everything out would be losing it.
An entry SET ASIDE says why: quarantine carries the motive, and quarantineStatus the
HTTP status of the refusal when a server answer caused it. Both are absent from an entry
the drain still holds — an interface can tell "stuck, and for this reason" from "waiting".
🛑 By default, only the ACTIVE profile's edits. Its readers count and act —
a pending tally, a quarantine panel —, and an edit captured under another profile can
neither leave nor be judged from here. An EXPORT, whose role is to get everything out,
passes allProfiles: true; each edit then says whose it is (profileId, absent on an
edit captured before the field existed, which every profile lists).
Optionaloptions: { allProfiles?: boolean }
allProfiles: true lists the edits of every profile.
One entry per pending edit, oldest first; [] without the module.
const pending = (await GeoLeaf?.Storage?.DB?.listPendingEdits?.()) ?? [];
console.info(`${pending.length} edit(s) not yet accepted by the server`);
for (const edit of pending) {
if (edit.state === "quarantined") console.warn(edit.localId, edit.quarantine);
}
const everything = (await GeoLeaf?.Storage?.DB?.listPendingEdits?.({ allProfiles: true })) ?? [];
console.info(`${everything.length - pending.length} edit(s) of another profile`);
Removes the entities that are pure CACHE — synchronised, and re-pullable (4.10).
🛑 WHAT MAKES THE BUTTON'S NAME TRUE. The features store IS the cache: its
synced records re-pull through pullLayer(). The outbox, by contrast, carries
field work that exists nowhere else. A purge announcing "clear the cache" must
therefore touch only the former — cache vocabulary, re-downloadable data, and
nothing else.
⚠️ The outbox guard is here although the single-transaction invariant makes it
theoretically useless: applyEdit writes the entity as pending together with
the entry, so a synced record should have no pending entry. "Should not"
guards nothing — and the stake here is an irreversible destruction of capture.
{ removed, preserved } — preserved counts the entities spared
because an outbox entry still claims them. Both are displayable.
Relays the storage report of DB.Preferences.
⚠️ This relay declared LESS than what the module returns, and declared it
wrong. Its shape was { used, quota, percentage, layersCount, syncQueueCount }:
it named a counter of the v3 store — since removed — and omitted
featuresCount and outboxCount, which preferences.ts fills. A caller on
the degraded path thus received an object with two fields missing without the
type saying so.
Quota, usage, and the tallies of the four data-bearing stores.
Reads one stored image back — the preview path for a locally-held capture.
Local image id.
Marks a local image uploaded, and records the URL the server gave it.
🛑 THE status PARAMETER WAS TYPED string, AND THAT IS WHAT MADE THE BUG INVISIBLE.
The module behind this relay reads status.uploaded and status.url; the editor
plugin passed the STRING "uploaded". ("uploaded").uploaded is undefined, so the
record was rewritten as still pending and the URL was never stored — the same photo
left again on every reconnection and every boot, and cleanUploadedImages (a cursor
over the index at 1) found nothing to purge, forever. Three layers declared it loose:
this signature, the plugin's own local interface, and the index signature every module
was typed with ((...args: unknown[]) => unknown). That last one is gone: each relay is
now checked against the module it forwards to (DBModuleInstances).
Local image id.
{ uploaded, url? } — an OBJECT, never a string.
Binds a stored image to the feature that carries it — see the module's bindLocalImage.
Local image id.
Layer and client identity of the owning feature.
Closes the database connection
Public name of the StorageDB façade: opens the offline database, wires the
db/modules through DBModulesRegistry, and forwards the layer/sync/image/ backup/preference operations to whichever module owns each object store.This is the handle
offline-engine-entry.tspasses toGeoLeaf.Storage.wireModules().