GeoLeaf Core API - v3.19.0
    Preparing search index...
    IndexedDB: {
        init(): Promise<StorageDBInstance>;
        cacheLayer(
            id: string,
            data: string | object | ArrayBuffer,
            profileId: string | null,
            metadata?: Record<string, unknown>,
        ): Promise<unknown>;
        getLayer(id: string): Promise<LayerRecord | null | undefined>;
        getLayerFeatureCollection(
            layerId: string,
        ): Promise<{ type: "FeatureCollection"; features: unknown[] } | null>;
        applyLocalEdit(input: LocalEditInput): Promise<LocalEditTally | null>;
        putLayerFeatures(
            records: readonly FeatureRecord[],
            tombstones?: readonly FeatureRecord[],
        ): Promise<PreservingPutTally | null>;
        sweepLayerFeatures(
            layerId: string,
            keep: ReadonlySet<string>,
            sourceKey?: string,
        ): Promise<number | null>;
        getSyncCounts(
            layerIds: readonly string[],
        ): Promise<
            | Record<
                string,
                { featureCount: number; pendingCount: number; quarantinedCount: number },
            >
            | null,
        >;
        listPendingEdits(
            options?: { allProfiles?: boolean },
        ): Promise<PendingEditItem[]>;
        purgeCachedFeatures(): Promise<{ removed: number; preserved: number }>;
        removeLayer(id: string): Promise<unknown>;
        getLayersByProfile(profileId: string): Promise<unknown>;
        clearProfile(profileId: string): Promise<unknown>;
        getStorageStats(): Promise<StorageStatsReport>;
        setPreference(key: string, value: unknown): Promise<unknown>;
        getPreference(key: string, defaultValue?: unknown): Promise<unknown>;
        storeImageLocally(imageData: LocalImageData): Promise<unknown>;
        getLocalImage(id: string): Promise<unknown>;
        getPendingImages(): Promise<unknown>;
        updateImageUploadStatus(
            id: string,
            status: { uploaded: boolean; url?: string },
        ): Promise<unknown>;
        bindLocalImage(
            id: string,
            owner: { layerId: string; localId: string },
        ): Promise<unknown>;
        deleteLocalImage(id: string): Promise<unknown>;
        cleanUploadedImages(): Promise<unknown>;
        close(): void;
    } = StorageDB

    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.ts passes to GeoLeaf.Storage.wireModules().

    Type Declaration

    • init: function
    • cacheLayer: function
      • Parameters

        • id: string
        • data: string | object | ArrayBuffer
        • profileId: string | null
        • metadata: Record<string, unknown> = {}

        Returns Promise<unknown>

    • getLayer: function
    • getLayerFeatureCollection: function
      • Reads 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.

        Parameters

        • layerId: string

          Layer identifier.

        Returns Promise<{ type: "FeatureCollection"; features: unknown[] } | null>

        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.

    • applyLocalEdit: function
      • 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.

        Parameters

        Returns Promise<LocalEditTally | null>

        What was done (merge, cancellation, new entry), or null without the module.

    • putLayerFeatures: function
      • 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.

        Parameters

        • records: readonly FeatureRecord[]

          Complete records, feature included.

        • tombstones: readonly FeatureRecord[] = []

          Entities the source served as deleted: never written, their synchronised copy removed. Optional.

        Returns Promise<PreservingPutTally | null>

        The real tally — written, preserved, unchanged, removed, and the keys the batch named (seen) — or null when the module is absent.

    • sweepLayerFeatures: function
      • 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.

        Parameters

        • layerId: string

          The layer the pull covered.

        • keep: ReadonlySet<string>

          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.

        Returns Promise<number | null>

        How many records were removed, or null when the module is absent.

    • getSyncCounts: function
      • 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.

        Parameters

        • layerIds: readonly string[]

          Layers to count. A layer with no entity yields zeros, never nothing.

        Returns Promise<
            | Record<
                string,
                { featureCount: number; pendingCount: number; quarantinedCount: number },
            >
            | null,
        >

        The tallies per layer identifier, or null when the modules are absent.

        const counts = await GeoLeaf.Storage.DB.getSyncCounts(["sites_rosario"]);
        console.info(counts?.["sites_rosario"]?.featureCount);
    • listPendingEdits: function
      • 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).

        Parameters

        • Optionaloptions: { allProfiles?: boolean }

          allProfiles: true lists the edits of every profile.

        Returns Promise<PendingEditItem[]>

        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`);
    • purgeCachedFeatures: function
      • 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.

        Returns Promise<{ removed: number; preserved: number }>

        { removed, preserved } — preserved counts the entities spared because an outbox entry still claims them. Both are displayable.

        const { removed, preserved } = await GeoLeaf.Storage.DB.purgeCachedFeatures();
        console.info(`${removed} supprimée(s), ${preserved} conservée(s)`);
    • removeLayer: function
    • getLayersByProfile: function
    • clearProfile: function
    • getStorageStats: function
      • 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.

        Returns Promise<StorageStatsReport>

        Quota, usage, and the tallies of the four data-bearing stores.

    • setPreference: function
    • getPreference: function
    • storeImageLocally: function
    • getLocalImage: function
    • getPendingImages: function
    • updateImageUploadStatus: function
      • 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).

        Parameters

        • id: string

          Local image id.

        • status: { uploaded: boolean; url?: string }

          { uploaded, url? } — an OBJECT, never a string.

        Returns Promise<unknown>

    • bindLocalImage: function
      • Binds a stored image to the feature that carries it — see the module's bindLocalImage.

        Parameters

        • id: string

          Local image id.

        • owner: { layerId: string; localId: string }

          Layer and client identity of the owning feature.

        Returns Promise<unknown>

    • deleteLocalImage: function
    • cleanUploadedImages: function
    • close: function