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

    Interface OutboxEntry

    One queued operation.

    It references localId and never serverId: an entity created offline has no server identity yet, and an outbox that could not name it would be unable to queue its own creation.

    ⚠️ Named OutboxEntry and not SyncQueueEntry deliberately: two types already carry that second name with two different shapes — one in the POI plugin's sync types, one in the v3 queue module of this capability. A third homonym would have been indistinguishable from them at a call site, and the identical name IS the defect — so it is not reused.

    ⚠️ Both homonyms are removed, together with the sync_queue store they described. The name nevertheless stays collision-free by construction, and that is the point: it was not reused because the defect was the shared name, not its rarity.

    interface OutboxEntry {
        id: string;
        kind: SyncOperationKind;
        layerId: string;
        localId: string;
        baseVersion: VersionMarker | null;
        state: SyncState;
        attempts: number;
        createdAt: number;
        quarantine?: QuarantineReason;
        quarantineStatus?: number;
        inFlightAt?: number;
        nextAttemptAt?: number;
        profileId?: string;
    }
    Index
    id: string
    layerId: string
    localId: string
    baseVersion: VersionMarker | null

    Marker the edit was based on, sent back at push so the server can detect a conflict. An entry stacked while a write of the same entity was on the wire is based on that write: it moves to the marker the write returned.

    state: SyncState
    attempts: number

    Replay attempts so far. The drain's failures increment it until the budget is spent. It goes back to 0 when the entry is requeued out of quarantine, or re-armed by a new local edit it absorbs.

    createdAt: number
    quarantine?: QuarantineReason
    quarantineStatus?: number

    HTTP status of the refusal that caused the quarantine — absent when it does not come from a server response (layerNoLongerWritable, networkError at the cap).

    🛑 WHY A FIELD BESIDE, AND NOT AN ENRICHED QuarantineReason. The instruction asked to "make QuarantineReason carry the status". Taken literally, that turns a union of literals into an object: a break of a surface published on npm since 12/08, while DEPRECATIONS.json is empty — so no removal is authorised. An optional field renders the same service and adds nothing to what an existing consumer must read.

    ⚠️ The motive: on offline fieldwork, the diagnosis must TRAVEL WITH THE ENTRY. pushOne knows the status at the decision point and logs it (Log.warn(… refused (403))) — but a log is volatile and nobody opens a console in the field. rejectedByServer alone does not distinguish a missing right (403, which the operator can fix) from a malformed request (400, which is our bug). Without the status the two look alike, and the entry stays stuck with nobody knowing which of the two causes to treat.

    📌 Same consequence as the eviction alert without a listener, from the other end: a loss the user has no way to know happened, nor why.

    inFlightAt?: number

    When the entry went on the wire, milliseconds since epoch. Present only while state is inFlight.

    🛑 IT EXISTS BECAUSE AN ENTRY IN FLIGHT HAD NO WAY BACK. inFlight is written before the call and cleared by its outcome; a session that dies in between — a tab killed while the device sleeps mid-sync, the normal case on a phone — left the entry in a state NO path replays (REPLAYABLE holds pending and failed) and no gesture exits. The capture stayed visible, counted as due, and definitively stuck.

    ⚠️ A timestamp and not a flag, because "in flight" is not decidable without one. Two tabs share the database, not the network: reclaiming every inFlight would re-send what another tab has on the wire, and a second DELETE gets a 404 — i.e. an immediate quarantine on a push that had succeeded. Only its AGE separates "the session died" from "another tab is sending it", and the transport gives the threshold: fetchBounded caps at 15 s, so past a wide multiple of that, nothing is still in flight.

    ⚠️ Absent means abandoned, and that is not an omission: an entry stamped by no writer predates this field, hence a previous session. Reading its absence as "just departed" would keep exactly the entries this field exists to free.

    nextAttemptAt?: number

    Earliest date this entry may be replayed, milliseconds since epoch. Absent when it is replayable now.

    🛑 WITHOUT IT, THE BUDGET WAS NOT A BUDGET. MAX_REPLAY_ATTEMPTS counts TOTAL attempts — four — and attempts persists in the database, while the drain fires on network return and on the operator's "Retry" button. As many clicks inside a maintenance window therefore spent a field capture's whole budget in under a minute and set it aside — the cap measured a number of gestures, never a duration.

    ⚠️ The deferral belongs to the ENTRY, never to the transport. fetchBounded is deliberately retry-free (push-engine.ts says why: two retry authorities count each other). Waiting inside the send would put the delay back into the transport and make the drain block on a queue it should be walking past.

    profileId?: string

    Identifier of the profile active when the edit was captured.

    🛑 THE WRITE TARGET IS RESOLVED AT DRAIN TIME, FROM THE ACTIVE PROFILE. A profile switch reloads the page, and the first drain of the new page replays what the previous one left. Without this field, an entry captured under profile A was replayed under B: sent to the endpoint of B's layer of the same id, with B's credentials, and nothing said so — or set aside on the spot as layerNoLongerWritable when B has no such layer. The drain now replays only the active profile's entries; the others wait, untouched, until their profile is active again.

    ⚠️ Absent means captured before the field existed (core 3.18.0), and such an entry keeps the former behaviour: it is replayed under whichever profile is active. Its profile is not knowable, and holding it forever would strand field work that, in the usual case, belongs to the profile still in use. The database schema does not change: the field is optional, so no upgrade rewrites the entries already queued.