GeoLeaf Core API - v3.19.0
    Preparing search index...

    Variable StorageConst

    Storage: {
        _modules: {
            db?: DBLike;
            cacheManager?: CacheManagerLike;
            cache?: unknown;
            pull?: PullLike;
            report?: ReportLike;
            edit?: EditLike;
            ui?: OfflineUiLike;
            options?: OptionsLike;
        };
        wireModules(
            modules: {
                db?: unknown;
                cacheManager?: unknown;
                cache?: unknown;
                pull?: unknown;
                report?: unknown;
                edit?: unknown;
                ui?: unknown;
                options?: unknown;
            },
        ): void;
        get DB(): DBLike | undefined;
        get CacheManager(): CacheManagerLike | undefined;
        get OfflineDetector(): OfflineDetectorLike | undefined;
        get Cache(): unknown;
        get db(): DBLike | undefined;
        get cacheManager(): CacheManagerLike | undefined;
        get cache(): unknown;
        init(options?: StorageInitOptions): Promise<boolean>;
        isAvailable(): boolean;
        isPluginLoaded(): boolean;
        whenReady(): Promise<void>;
        pullLayer(
            layerId: string,
            options?: {
                bbox?: [number, number, number, number];
                signal?: AbortSignal;
            },
        ): Promise<StorageLayerPullReport>;
        applyEdit(
            input: {
                layerId: string;
                kind: "delete" | "create" | "update";
                localId?: string;
                feature?: unknown;
                baseVersion?: { kind: "timestamp" | "etag"; value: string } | null;
            },
        ): Promise<StorageEditReport>;
        mayEdit(layerId: string, kind: "delete" | "create" | "update"): boolean;
        pushOutbox(): Promise<StoragePushReport>;
        _armOutboxDrain(options?: { pollIntervalMs?: number }): void;
        canQueueWrites(layerId: string): boolean;
        _disarmOutboxDrain(): void;
        _mountSyncBanner(options?: { enabled?: boolean }): void;
        _unmountSyncBanner(): void;
        _requestOutboxDrain(cause?: string): void;
        requeueQuarantined(id: string): Promise<StorageQuarantineOutcome>;
        requeueAll(reason?: string): Promise<StorageBatchRequeueOutcome>;
        discardQuarantined(
            id: string,
            confirmedLocalId: string,
        ): Promise<StorageQuarantineOutcome>;
        requeueableReasons(): readonly string[];
        listConflicts(
            layerId?: string,
            scope?: { allProfiles?: boolean },
        ): Promise<StorageConflictRecord[]>;
        clearConflicts(
            layerId?: string,
            scope?: { allProfiles?: boolean },
        ): Promise<boolean>;
        getSyncReport(): Promise<readonly LayerSyncReport[]>;
        preflight(): Promise<PreflightReport | null>;
        prefetchVerdict(url: string): StoragePrefetchVerdict;
        getSyncStatus(): Promise<SyncStatus>;
        resolveOptions(
            url: string,
        ): Promise<{ value: string; label: string }[] | null>;
        isOffline(): boolean;
        getStats(): Promise<
            {
                storage: { used: number; quota: number; percentage: number };
                layers: { count: number; byProfile: Record<string, number> };
                features: { count: number };
                outbox: { count: number };
                conflicts: { count: number };
                cache: { profiles: string[] };
                online: boolean;
            },
        >;
        clearAll(): Promise<void>;
        close(): void;
        isProfileAvailableOffline(profileId: string): Promise<boolean>;
        getOfflineProfiles(): Promise<string[]>;
    } = ...

    Type Declaration

    • _modules: {
          db?: DBLike;
          cacheManager?: CacheManagerLike;
          cache?: unknown;
          pull?: PullLike;
          report?: ReportLike;
          edit?: EditLike;
          ui?: OfflineUiLike;
          options?: OptionsLike;
      }

      Implementation modules injected by entry.ts (composition root) at boot.

    • wireModules: function
      • Wire the assembled implementation modules into the facade. Called once by entry.ts after every sub-module has been imported and assembled.

        Parameters

        • modules: {
              db?: unknown;
              cacheManager?: unknown;
              cache?: unknown;
              pull?: unknown;
              report?: unknown;
              edit?: unknown;
              ui?: unknown;
              options?: unknown;
          }

        Returns void

    • get DB(): DBLike | undefined

      IndexedDB layer accessor — read by StorageContract.DB.

    • get CacheManager(): CacheManagerLike | undefined

      CacheAPI cache manager accessor — read by StorageContract.CacheManager.

    • get OfflineDetector(): OfflineDetectorLike | undefined

      Network state detector resolved lazily from globalThis.GeoLeaf.

    • get Cache(): unknown

      Cache namespace ({ Storage, LayerSelector }) — read by StorageContract.Cache (download-handler, selection-cache, cache-control-zone).

    • get db(): DBLike | undefined

      Plugin Contract v1 accessor for the IndexedDB layer.

    • get cacheManager(): CacheManagerLike | undefined

      Plugin Contract v1 accessor for the CacheAPI cache manager.

    • get cache(): unknown

      Plugin Contract v1 accessor for the cache namespace ({ Storage, LayerSelector }).

    • init: function
      • Initialise every available storage sub-module (IndexedDB, CacheManager, and optionally the offline detector).

        Parameters

        • options: StorageInitOptions = {}

          per-module options; enableOfflineDetector is opt-in (default false). The Service Worker is not started from here: it is the PWA capability's.

        Returns Promise<boolean>

        true once all available modules are initialised.

        if a sub-module initialisation fails.

        Emits geoleaf:storage:initialized on success.

    • isAvailable: function
    • isPluginLoaded: function
      • true as soon as the offline engine registered — independently of IndexedDB opening, which isAvailable tests.

        ⚠️ Delegation added so @geoleaf-plugins/offline-ui stops importing StorageContract. The contract is a SINGLETON whose state is a module-scoped let: a plugin loaded as <script type="module"> has its own graph and cannot share it. The copy it embedded was never initialised — this member gives it the real one, through the namespace.

        Returns boolean

    • whenReady: function
      • Resolves when the offline engine is ready to drive.

        ⚠️ NEVER resolves while modules.offline is disabled — the engine does not load, and UI actions awaiting it defer indefinitely. That is the contract's behaviour, taken as-is. Same delegation motive as isPluginLoaded.

        Returns Promise<void>

    • pullLayer: function
      • Pulls a declared layer's entities into the local features store.

        Bounded by the layer's offline.maxFeatures and, optionally, by a bounding box. Downloading NEVER grants write access: the records land as synced and no queue entry is created (invariant S6 of the sync contract).

        A layer that is on the map and declares offline.enabled is given what the store holds as the pull ends: the map follows without a reload, and geoleaf:layer:updated announces it. Since 3.15.0 — the layer kept its previous drawing until the next load.

        ⏱ Waits for the engine, but not indefinitely. The implementation lives in the offline chunk, loaded via import() after boot — calling it at the first frame would find it absent. The race is bounded by PULL_ENGINE_WAIT_MS because StorageContract.whenReady() never resolves when modules.offline is disabled: unbounded, the call would hang forever on an engine-less variant.

        ⚠️ Unlike the layer read, there is no network fallback here. A pull without an engine must SAY SO — refused: "engineUnavailable" — and not return a zero nothing distinguishes from an empty layer.

        Parameters

        • layerId: string

          Identifier of the layer to pull.

        • Optionaloptions: { bbox?: [number, number, number, number]; signal?: AbortSignal }

          bbox extent and abort signal.

        Returns Promise<StorageLayerPullReport>

        The pull report; refused is non-null when nothing was written.

        const report = await GeoLeaf?.Storage?.pullLayer?.("sites_rosario");
        console.info(report?.written, report?.preserved, report?.capped);
    • applyEdit: function
      • Applies an edit locally AND queues it for the server — the optimistic write.

        The entity goes into the features store and the operation into the outbox, in a single transaction: a field capture has no other copy, and a half-done write would be undetectable after the fact.

        ⚠️ Nothing written here confers editability — on the contrary, the call is REFUSED, with its motive, when the layer is not modifiable online. Downloading a layer never made a layer modifiable.

        Bounded engine wait, same motive as Storage.pullLayer: the offline chunk is deferred, and whenReady() never resolves without modules.offline.

        Parameters

        • input: {
              layerId: string;
              kind: "delete" | "create" | "update";
              localId?: string;
              feature?: unknown;
              baseVersion?: { kind: "timestamp" | "etag"; value: string } | null;
          }

          The layer, operation kind, identity and entity. For an existing entity the identity may be the server one the map shows: the store resolves it to its own key. The entity may be partial: its properties are merged over the stored ones, and the stored geometry is kept when it carries none.

        Returns Promise<StorageEditReport>

        The report: entry created, merged, or cancelled; localId is the key the edit landed on; refused carries the motive.

        const report = await GeoLeaf?.Storage?.applyEdit?.({
        layerId: "sites_rosario", kind: "update", localId: "loc:abc", feature
        });
        console.info(report?.queued, report?.coalescedInto);
    • mayEdit: function
      • Does the layer grant this operation? — to consult BEFORE writing.

        🛑 It is SYNCHRONOUS AND ENGINE-LESS, deliberately. The permission is read from the active profile, not IndexedDB: routing it through the edit bag (like Storage.applyEdit) would have made it unavailable when the offline chunk is not loaded — i.e. exactly the case that carried the hole, @geoleaf-plugins/editor declaring requires: [] and running in persistence.mode: "online" without this engine.

        ⚠️ Does not replace applyEdit's guard, it precedes it. applyEdit keeps refusing on its own account: a caller not consulting this predicate bypasses nothing. Both apply grantsEdition, the same function.

        ⚠️ An unknown layer yields false — refuse the unknown, otherwise a typo in an identifier would amount to authorisation.

        Parameters

        • layerId: string

          The layer identifier from the active profile.

        • kind: "delete" | "create" | "update"

          The submitted operation.

        Returns boolean

        true only when the layer literally grants this operation.

        if (GeoLeaf?.Storage?.mayEdit?.("reference-points", "delete")) {
        console.info("la couche accorde la suppression");
        }
    • pushOutbox: function
      • Drains the outbox: pushes every queued edit and reconciles server identities (4.5).

        ⚠️ Runs in the PAGE, never in the Service Worker — contract point 5. The connector's fetch patch lives in the page; a replay from the worker does not see it and would leave without a token. That is the motive that removed the Background Sync path.

        Does not throw: a failing entry stays queued, failed not being terminal.

        Returns Promise<StoragePushReport>

        The drain tally; refused is non-null when the engine is not wired.

        const report = await GeoLeaf?.Storage?.pushOutbox?.();
        console.info(report?.pushed, report?.failed);
    • _armOutboxDrain: function
      • Arms the drain triggers — lifecycle only, hence the underscore.

        🛑 UNDERSCORED ON PURPOSE, AND IT IS NOT SHYNESS. namespace-surface.mjs drops _-prefixed members from the measured surface, so the golden master and EXPECTED_FACADE_MEMBERS do not move for a lifecycle concern. A public armOutboxDrain() would be a promise to integrators that arming is theirs to make — it is not: the capability arms once its storage is ready, and calling it from outside could only duplicate that.

        Parameters

        • Optionaloptions: { pollIntervalMs?: number }

          { pollIntervalMs }, from modules.offline.drain.

        Returns void

    • canQueueWrites: function
      • Can a write to layerId be HELD here, to be sent later?

        🛑 THE PREDICATE THAT MAKES ONE WRITE PATH SAFE — and its first draft asked the wrong question. It asked whether the DRAIN could push the layer, and answered false when the layer declared no write target, so a caller sent those writes down its own online path. The E2E suite refuted it on the shipped bundle: deliverables have their write endpoints stripped, so nothing was holdable there and an offline save stopped reaching the outbox. Holding beats losing — a held capture is visible, counted, and eventually set aside with a named motive; one sent to a dead endpoint is gone.

        false when the engine is not wired at all — the capability is opt-in, so a caller must be able to learn that the queue simply does not exist here.

        Parameters

        • layerId: string

          The layer about to be written to.

        Returns boolean

        true when a local write will be accepted and kept.

        const held = GeoLeaf?.Storage?.canQueueWrites?.("sites_rosario") ?? false;
        if (!held) console.warn("no local queue for this layer");
    • _disarmOutboxDrain: function
    • _mountSyncBanner: function
      • Mounts the permanent sync strip — lifecycle only, hence the underscore.

        Parameters

        • Optionaloptions: { enabled?: boolean }

          { enabled }, from modules.offline.banner.

        Returns void

    • _unmountSyncBanner: function
    • _requestOutboxDrain: function
      • Asks for a drain now — used by a write that has just landed in the queue.

        ⚠️ Fire-and-forget by design: a caller that awaited it would tie its own latency to the network, which is the very coupling the outbox exists to break.

        Parameters

        • cause: string = "write"

          What asked, for the log.

        Returns void

    • requeueQuarantined: function
      • Requeues a quarantined entry, when its cause is LIFTED.

        Reserved for motives whose cause can be observed as lifted — retryBudgetExhausted, layerNoLongerWritable, notImplementedByServer, authRequired and dialectNotSupported. The other two, deletedOnServer and rejectedByServer, name a server fact no local gesture undoes: their exit is Storage.discardQuarantined. A requeued entry starts a fresh replay budget.

        An entry that goes back is announced — geoleaf:offline:quarantine-exited, with exit: "requeued" — and a pass is asked for: no pushOutbox() has to follow.

        Parameters

        • id: string

          The entry's contract identifier.

        Returns Promise<StorageQuarantineOutcome>

        {ok} and, on refusal, its motive.

        const out = await GeoLeaf?.Storage?.requeueQuarantined?.("create:sites:loc:abc:1");
        if (!out?.ok) console.warn(out?.refused);
    • requeueAll: function
      • Requeues EVERY requeueable quarantined entry, in one gesture.

        🛑 The per-entry exit takes a contract id, which nothing displays — so the only way to use it was to list the queue in a console. Yet what produces quarantines is never one entry: an expired token or a maintenance window sets aside a whole tour's captures at once.

        The rule that decides stays Storage.requeueQuarantined's — motive requeueable, cause observed as lifted — so what it refuses is COUNTED in skipped, never silently taken.

        The gesture is announced ONCE, whatever the number of entries it brings back (geoleaf:offline:quarantine-exited), and asks for one pass. Bringing nothing back announces nothing.

        Parameters

        • Optionalreason: string

          Restrict to this motive; omitted, every requeueable entry.

        Returns Promise<StorageBatchRequeueOutcome>

        How many came back, how many the rule left, or the refusal.

        const out = await GeoLeaf?.Storage?.requeueAll?.("retryBudgetExhausted");
        console.info(`${out?.requeued ?? 0} saisie(s) remise(s) en file`);
    • discardQuarantined: function
      • Destroys a quarantined entry, on EXPLICIT confirmation.

        🛑 confirmedLocalId must be the entry's localId — a value the caller only knows by having LISTED it. That is what makes it structurally true that the capture was enumerated before being discarded, as ServerDeletionPolicy requires since its amendment: what the contract forbids is the loss the operator did not SEE.

        The entity's local record then returns to the server's truth, unless another queue entry still names it: removed when the server has nothing to give back (a creation that never landed, deletedOnServer), marked synced otherwise, so the next pull replaces it.

        A destroyed entry is announced — geoleaf:offline:quarantine-exited, with exit: "discarded" — once the local record has settled. No pass follows: nothing is left to send.

        Parameters

        • id: string

          The entry's contract identifier.

        • confirmedLocalId: string

          This entry's localId, as the caller read it.

        Returns Promise<StorageQuarantineOutcome>

        {ok} and, on refusal, its motive.

        const listed = (await GeoLeaf?.Storage?.DB?.listPendingEdits?.()) ?? [];
        const e = listed.find((p) => p.state === "quarantined");
        if (e) await GeoLeaf?.Storage?.discardQuarantined?.(e.entryId, e.localId);
    • requeueableReasons: function
      • Which quarantine motives an operator's gesture can lift.

        🛑 ONE AUTHOR FOR THE RULE. A window offering "retry all (motive X)" must know which motives that sentence is true for BEFORE the click — a button that does nothing when pressed teaches the user to stop pressing it. Without this read every surface would hard-code the list, and a plugin cannot import the core (INV-NS), so its copy would diverge unfalsifiably.

        ⚠️ It does NOT promise an entry will come back: Storage.requeueQuarantined still verifies that the cause is observed as lifted where that is checkable. It says which motives have an exit at all — the others exit through Storage.discardQuarantined.

        Returns readonly string[]

        The requeueable motives — empty when the offline engine is not wired.

        const exits = GeoLeaf?.Storage?.requeueableReasons?.() ?? [];
        if (exits.includes("retryBudgetExhausted")) {
        await GeoLeaf?.Storage?.requeueAll?.("retryBudgetExhausted");
        }
    • listConflicts: function
      • The versions a settled conflict CRUSHED, and that the device kept (v6 store).

        🛑 lastWriteWins overwrites, and until 17/09/2026 it overwrote BLIND. The drain detected the conflict and logged it, then re-sent the write unfiltered without ever reading the row it was about to destroy. After a settled update the server still holds a row; after a settled DELETE it holds nothing, and neither did anything else.

        ⚠️ One record per entity: a second conflict on the same entity replaces the first. That is the store's bound — nothing evicts it, so the key is what keeps it finite.

        ⚠️ readOutcome says how much of the crushed row could be established. "unreadable" means the overwrite happened anyway and the version is lost — recorded rather than invented, because making the write depend on a read the server may not grant would repeal the policy in silence.

        ⚠️ Only the active profile's conflicts, unless asked otherwise. The archive is shared by every profile of the origin; a conflict belongs to the profile its write was queued under (profileId). One archived before 3.18.0 has none, and is listed under every profile.

        Parameters

        • OptionallayerId: string

          Restrict to this layer; omitted, every layer.

        • Optionalscope: { allProfiles?: boolean }

          allProfiles: true lists every profile's conflicts, each with its profileId.

        Returns Promise<StorageConflictRecord[]>

        The archived conflicts — empty when the offline engine is not wired.

        const crushed = await GeoLeaf?.Storage?.listConflicts?.("sites");
        console.info(`${crushed?.length ?? 0} version(s) écrasée(s) conservée(s)`);
    • clearConflicts: function
      • Drops the archived conflicts of one layer, or all of them.

        ⚠️ The only purge there is, and its being manual is deliberate. db/eviction.ts names one store (layers), so nothing reclaims this one on its own — the same property that keeps unsynchronised work out of eviction's reach.

        ⚠️ Only the active profile's conflicts, unless asked otherwise — the scope Storage.listConflicts reads: a purge under one profile must not erase what another profile's operator has not looked at yet.

        Parameters

        • OptionallayerId: string

          Restrict to this layer; omitted, every layer.

        • Optionalscope: { allProfiles?: boolean }

          allProfiles: true purges every profile's conflicts.

        Returns Promise<boolean>

        true when the purge ran, false when the engine is not wired.

        await GeoLeaf?.Storage?.clearConflicts?.("sites");
        
    • getSyncReport: function
      • Reports, layer by layer, what offline really has at hand.

        🛑 The case it exists to make visible: a layer that declares a pull source (offline.source) but was never pulled. The contract describes it as "the case with no observable until the outage" — everything works, until the field. It shows up here as declaredNeverPulled. A layer read from the local store with no source has nothing to pull, and is notDeclared.

        Never throws. With no engine wired, returns an empty array rather than a reassuring report: a report saying "all is well" having read nothing would be worse than no report, since it would believe itself complete.

        Returns Promise<readonly LayerSyncReport[]>

        One report per active-profile layer; empty when the engine is not wired.

        const report = await GeoLeaf?.Storage?.getSyncReport?.();
        const jamais = report?.filter((r) => r.status === "declaredNeverPulled") ?? [];
        if (jamais.length) console.warn("déclarées hors-ligne, jamais rapatriées :", jamais);
    • preflight: function
      • "Can I leave?" — what a device must be read for before going off-network, in one read.

        Assembles facts that each had their own door — the per-layer report, the queue, the quota, the last preparation (when it ran, what failed, which zooms of a basemap were left out) — and one that had none: whether the browser keeps this origin's data (navigator.storage.persisted(), which the PWA requested at boot and only logged). verdict says what they mean together: notReady when a layer declaring something to pull is not on the device, degraded when the device can leave with something missing or evictable, ready otherwise. The write session is not in it: its validity is the connector's knowledge.

        Never throws. With no engine wired, it answers null rather than a report that would reassure without having read anything. Since 3.10.0.

        Returns Promise<PreflightReport | null>

        The check, or null when the engine is not wired.

        const check = await GeoLeaf?.Storage?.preflight?.();
        if (check?.verdict === "notReady") {
        const missing = check.layers.filter((l) => l.status === "declaredNeverPulled");
        console.warn("not on the device:", missing.map((l) => l.layerId));
        }
    • prefetchVerdict: function
      • Will the offline preparation download this URL — and if not, why?

        The preparation only downloads a basemap or a tiled layer from the application's own origin while it is undeclared, or from an origin declared cacheable: true and prefetch: true in modules.offline.dataOrigins. The refusal used to be a console warning during the download: an interface offering the basemap could neither know it beforehand nor say it afterwards. This is the same rule, asked before.

        url may be a tile URL TEMPLATE: it is judged on the concrete tile the preparation would request ({s} read as a).

        ⚠️ One URL, one verdict. A vector basemap is judged twice by the preparation — its style, then the tiles, glyphs and sprite the style names, which may live on other origins: a favourable verdict on the style URL does not promise everything behind it. What a download actually left out is in its result, preparation.refusedOrigins.

        Never throws. With no engine wired it answers a refusal — nothing is prepared at all. Since 3.15.0.

        Parameters

        • url: string

          A resource URL, a tile URL template, or a style URL.

        Returns StoragePrefetchVerdict

        allowed, the origin judged and, on a refusal, its reason.

        const verdict = GeoLeaf?.Storage?.prefetchVerdict?.(
        "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        );
        if (verdict && !verdict.allowed) {
        console.warn(`not prepared: ${verdict.origin} (${verdict.reason})`);
        }
    • getSyncStatus: function
      • What the write queue still owes, as a whole.

        🛑 THE SINGLE READ, AND IT IS PUBLIC BECAUSE A PLUGIN NEEDED IT. The sync strip had its own private version; offline-ui's modal has to show the same four numbers, and it cannot reach that one — deep imports of @geoleaf/core are bundled as a COPY, whose StorageContract singleton nothing in the plugin's graph initialises, so the copy would read an outbox that stays empty forever. Publishing the read is what stops the two from drifting.

        ⚠️ owed and quarantined are disjoint. An entry set aside is blocked, not owed; adding them would report work the drain will never do.

        Never throws. With no engine wired it answers zeros and null — which is what "there is no queue on this device" actually looks like, the capability being opt-in.

        Returns Promise<SyncStatus>

        Network state, entries owed, entries set aside, and the last accepted push.

        const s = await GeoLeaf?.Storage?.getSyncStatus?.();
        if (s?.owed) console.warn(`${s.owed} écriture(s) encore dues`);
    • resolveOptions: function
      • The choices of a dropdown declared by URL (fetchOptions) — held on the device first, fetched and kept otherwise when the network is there.

        A form that works off-network needs its lists: they are kept where the cache budget evicts nothing (the preferences store), filled by the offline preparation (CacheManager.cacheProfile) and by any form opened online. A held list is answered at once, and refreshed behind the answer while online. The request is the page's own fetch, so a connector that authenticates its origin authenticates it too.

        Never throws. It answers null — with no engine wired, when nothing is held off-network, or when the list cannot be fetched — and the caller then fetches the URL itself, which is what a dropdown with no resolver does. Since 3.10.0.

        Parameters

        • url: string

          The list's URL, as the field declares it.

        Returns Promise<{ value: string; label: string }[] | null>

        The choices, or null.

        const choices = await GeoLeaf?.Storage?.resolveOptions?.("lists/statut.json");
        if (choices) console.log(choices.map((c) => c.label));
    • isOffline: function
    • getStats: function
      • Aggregate storage usage across IndexedDB and the cache manager.

        ⚠️ features and outbox are counted since 03/08/2026. This method read only layersCount and syncQueueCount — the two v3 stores — so after a pull of 27 entities it still reported 0, and offline-ui displayed that zero. Not observable while features had no writer: the zero was TRUE. It became false the day the pull landed.

        🛑 The sync: { pending, failed } block is REMOVED. Its only source was syncQueueCount, i.e. the v3 store nobody wrote any more: it reported 0 in all circumstances, and failed was never assigned at all. The real tally of owed writes is outbox.count above, and the per-state breakdown is StorageDB.getSyncCounts, which yields pendingCount and quarantinedCount per layer.

        Returns Promise<
            {
                storage: { used: number; quota: number; percentage: number };
                layers: { count: number; byProfile: Record<string, number> };
                features: { count: number };
                outbox: { count: number };
                conflicts: { count: number };
                cache: { profiles: string[] };
                online: boolean;
            },
        >

        storage quota/usage, layer counts (total + per profile), entity, outbox and conflict counts, cached profile ids and the online flag.

        Never throws — errors are logged and partial stats returned.

    • clearAll: function
      • Wipe every cached profile plus the preferences and metadata IndexedDB stores. Field captures are never touched: features and outbox are left as they are.

        Returns Promise<void>

        if the IndexedDB transaction fails.

        Emits geoleaf:storage:cleared on success.

    • close: function
    • isProfileAvailableOffline: function
      • Parameters

        • profileId: string

          the profile to check.

        Returns Promise<boolean>

        true if the profile is fully cached for offline use.

    • getOfflineProfiles: function