Luxar Viewer API Documentation - v2026.10.19
    Preparing search index...

    OPFS persistence layer (L2 cache) with LRU eviction and shallow bucketing.

    Uses 256 bucket directories to distribute files and avoid filesystem limits. Files are stored as: {bucket}/{hash tag}.{base64-encoded-key} (see hashTag)

    Structure (everything under the viewer's luxar/ OPFS namespace dir, see opfs-store/opfs-root.ts):

    luxar/zarr-cache-{hash}/
    ├── 00/ (~250 files per bucket)
    │ ├── UG9pbnRz...
    │ └── ...
    ├── 01/
    ├── ...
    ├── ff/
    ├── _cache_identity.json
    └── _cache_meta.json

    Persistence contract (see the cache README, "L2 index persistence"): a chunk file whose write completed is served by the next session of the same dataset even if no index save recorded it. _cache_identity.json holds the content hash and is written before any chunk under that hash; a lookup of a key the index does not list reads the key's deterministic path while the open-time reconcile, which re-indexes or deletes the files the index never recorded, runs.

    Benefits:

    • Max ~250-500 files per directory instead of 65,000+
    • Only 256 bucket handles to cache (trivial memory)
    • clear() iterates 256 directories, not 65,000 files
    • Preserves fast flat access (2 async calls vs 1)
    Index
    opfsRoot: FileSystemDirectoryHandle | null = null
    index: Map<string, { size: number; order: number }> = ...
    orderCounter: number = 0
    totalSize: number = 0
    maxSize: number
    datasetId: string
    baseUrl: string
    contentHash: string | null = null
    fileTag: string = ''
    validationMode: CacheValidationMode = 'none'
    lastValidatedAt: number | null = null
    readCount: number = 0
    writeCount: number = 0
    missCount: number = 0
    canceledReadCount: number = 0
    oversizedWriteSkipped: number = 0
    quotaWriteSkipped: number = 0
    evictions: number = 0
    writeFailures: number = 0
    corruptedEntries: number = 0
    clobberBarrierWriteSkipped: number = 0
    buckets: OPFSBucketCache = ...
    metadata: OPFSMetadataManager = ...
    METADATA_SAVE_DELAY: 1000
    METADATA_SAVE_MAX_WAIT: 2000
    METADATA_SAVE_LEADING_DELAY: 150
    liveStores: Set<OPFSStore> = ...
    ORPHAN_RECONCILE_MAX_ACTIONS: 4096

    Per-open budget of the orphan reconcile: at most this many unindexed files are re-indexed or deleted per session (the crawl resumes next session). Bounded so a large backlog cannot turn a session's first seconds into a disk sweep; with the index saved at least every METADATA_SAVE_MAX_WAIT the backlog one interrupted session leaves is small.

    ORPHAN_RECONCILE_MAX_EXAMINED: 262144 = 262_144

    Per-open cap on file names the orphan crawl examines, orphan or not.

    pendingReconcile: Promise<void> | null = null
    recoveryHash: string | null = null
    probeUnindexed: boolean = false
    orphansReindexed: number = 0
    reconcileRecovered: Map<string, number> | null = null
    identityOnDisk: string | null = null
    identityWrite: Promise<void> | null = null
    QUOTA_ESTIMATE_MAX_AGE_MS: 30000 = 30_000
    QUOTA_REESTIMATE_BYTES: number = ...
    quotaEstimate: { available: number; at: number } | null = null
    quotaNetBytesSinceEstimate: number = 0
    quotaGrossBytesSinceEstimate: number = 0
    pendingWrites: Map<string, Promise<void>> = ...
    pendingDeletesByDataset: Map<string, Map<string, Set<Promise<void>>>> = ...
    generation: number = 0
    disposed: boolean = false
    initialized: boolean = false
    pendingInit: Promise<void> | null = null
    pendingClear: Promise<void> | null = null
    pendingDispose: Promise<void> | null = null
    consecutiveTimeouts: number = 0
    breakerTripped: boolean = false
    • Run one OPFS operation under the per-op timeout, feeding the circuit breaker. Counts ONLY the OPFS timeout rejection: any other settlement (success or a fast error such as NotFoundError) proves the backend is responsive and resets the count — the breaker targets systemic stalls, not error rate. Applied to the three hot real-I/O sites (get/set/delete) only: the init canary's catch already degrades the store itself, the delete-barrier awaits a delete whose own timeout already counted, and the dispose drain runs on an already-dead store.

      Type Parameters

      • T

      Parameters

      • promise: Promise<T>
      • label: string

      Returns Promise<T>

    • Disable the L2 tier for the rest of this store's lifetime after repeated consecutive timeouts. Nulling opfsRoot reuses the init-failure degradation path — every entry point already short-circuits on it, so all later ops are instant no-ops instead of serial full-timeout burns, and getStats().available flips the existing opfs-unavailable badge. The pending debounced metadata save is cancelled so its timer cannot fire a full-index write against the hung backend and stall dispose().

      Parameters

      • label: string

      Returns void

    • Enumerate every zarr-cache-* dataset present under the viewer's luxar/ OPFS namespace directory. Reads each dataset's _cache_meta.json directly — no OPFSStore instance is created. Used by the debug-cache helpers (window.__luxarDebug) and the cache E2E suite to inspect persisted datasets without mounting them.

      Never creates the namespace directory: on a cold origin (no luxar/ yet) the lookup's NotFoundError resolves to an empty list.

      Returns Promise<CachedDatasetSummary[]>

    • Prove OPFS is actually WRITABLE, not merely mounted. WebKit (Safari and the WKWebView native launcher) implements navigator.storage .getDirectory() and directory/file handles but NOT the main-thread FileSystemFileHandle.createWritable() — WebKit supports OPFS writes only through worker-side createSyncAccessHandle. Without this probe the store mounts "healthy" there and then fails EVERY put: tens of thousands of write errors, a permanently empty L2, and an all-miss read path (observed in the native macOS app's cache monitor). One tiny probe write at init converts that failure mode into the ordinary OPFS-unavailable degradation (L1-only, opfs-unavailable badge) with a single clear warning. Timeout-wrapped like every other OPFS operation so a hung handle cannot stall startup. Throws on failure — init()'s catch nulls opfsRoot.

      Returns Promise<void>

    • Reconcile chunk files the persisted index does not list (see opfs-store/orphan-reconcile.ts). Each orphan is RE-INDEXED when its name decodes to a key that hashes to the bucket it sits in, a recovery hash is set (recoveryHash), the file is non-empty, and it fits under maxSize; otherwise it is DELETED. Recovered entries join the LRU head (oldest), so they are the first evicted. Bounded per session by ORPHAN_RECONCILE_MAX_ACTIONS. The end of the crawl disarms the unindexed-read probe (probeUnindexed).

      Runs concurrently with normal traffic, so every action re-checks against the live state: a key this store is writing or has indexed is never touched, a delete is registered in the same-key delete barrier a later set() waits on (#1073), and a clear()/dispose() (generation bump) or a breaker trip stops the crawl and discards anything not yet merged.

      Returns Promise<void>

    • Delete one orphan file. A decodable key's removal is registered in the same-key delete barrier, so a set() of that key arriving mid-removal waits for it instead of being clobbered by it (#1073).

      Parameters

      Returns Promise<void>

    • Fold recovered orphans into the index as its LRU head. Synchronous, so it is atomic against every other index mutation; a key indexed meanwhile (a concurrent write) keeps its live entry. Writes during the crawl may have used the room it budgeted, so a recovery that no longer fits under maxSize is skipped (its file waits, unindexed, for the next reconcile).

      Parameters

      • recovered: Map<string, number>

      Returns void

    • Get a file from OPFS and update LRU order.

      Parameters

      • key: string
      • Optionaloptions: { signal?: AbortSignal }

      Returns Promise<Uint8Array<ArrayBufferLike> | undefined>

    • get() of a key the index lists (anything else is a miss here).

      Parameters

      • key: string
      • Optionalsignal: AbortSignal

      Returns Promise<Uint8Array<ArrayBufferLike> | undefined>

    • Whether a key the index does not list may be read from its file: recovery is armed (probeUnindexed) and nothing of this store is writing or deleting that file right now.

      Parameters

      • key: string
      • Optionalsignal: AbortSignal

      Returns boolean

    • get() for a key the index does not list, while unindexed files may still be on disk (see probeUnindexed). A key's file path is a pure function of the key, so the file is read where a write would have put it; a hit is indexed (the index never recorded it: a reload landed before the save) and served, a NotFound is an ordinary miss. Gated on the same validated hash as every indexed entry, and discarded if a clear() ran meanwhile.

      Parameters

      • key: string
      • Optionalsignal: AbortSignal

      Returns Promise<Uint8Array<ArrayBufferLike> | undefined>

    • The bytes at key's file path, or undefined (absent, unreadable, timed out).

      Parameters

      • key: string
      • Optionalsignal: AbortSignal

      Returns Promise<Uint8Array<ArrayBufferLike> | undefined>

    • Index a file found by an unindexed read, as the MRU entry. One that would push the index past maxSize is served but left unindexed, for the reconcile to decide (it deletes what does not fit).

      Parameters

      • key: string
      • size: number

      Returns void

    • Delete a file from OPFS.

      The CALLER await is bounded by opfsOperationTimeoutMs (preserving #991 — delete() runs inside doSet()'s eviction loops and get()'s corrupted-entry path, none of which may stall). The chain LINK a later same-key write waits on, however, is the REAL (un-timed-out) removeEntry settlement (doDelete), registered in pendingDeletesByDataset: when the caller returns on timeout the non-cancellable removeEntry keeps running, and a replacement write must not start until it has ACTUALLY settled (#1073 clobber invariant). Index/size are reconciled off that real settlement, never off the early timeout return.

      Parameters

      • key: string

      Returns Promise<void>

    • Record an in-flight REAL removal of key's file in the static same-key delete registry (see pendingDeletesByDataset); self-cleans on settlement. real must never reject.

      Parameters

      • key: string
      • real: Promise<void>

      Returns void

    • Real (un-timed-out) delete of a single file plus index/size reconcile. Runs to ACTUAL settlement as a link in the per-key serialization chain so a later same-key write can never start (and then be clobbered) while this removeEntry is still in flight (#1073). Never rejects.

      Parameters

      • key: string

      Returns Promise<void>

    • First key in LRU order (Map insertion order) that is not exclude. Used by doSet()'s eviction loops to skip the key currently being written (#1073) — see the loop comments for the self-deadlock rationale.

      Parameters

      • exclude: string

      Returns string | undefined

    • Clear all OPFS data for this dataset.

      Uses atomic delete-and-recreate instead of iterating entries, which avoids race conditions when a previous page context still holds open file handles (e.g., quick-succession page refreshes with fire-and-forget L2 writes).

      Returns Promise<void>

    • Check if enough storage quota is available (10% safety margin).

      Served from the cached estimate while it is fresh and its debited headroom covers the write; otherwise re-estimates (see quotaEstimate).

      Parameters

      • requiredBytes: number

      Returns Promise<boolean>

    • Account bytes this store put on (positive) or took off (negative) disk against the cached quota estimate. grossBytes counts toward the re-estimate trigger.

      Parameters

      • netBytes: number
      • grossBytes: number

      Returns void

    • Get cache statistics.

      Returns {
          size: number;
          count: number;
          reads: number;
          writes: number;
          misses: number;
          canceledReads: number;
          activeReads: number;
          queuedReads: number;
          oversizedWriteSkipped: number;
          quotaWriteSkipped: number;
          evictions: number;
          writeFailures: number;
          corruptedEntries: number;
          clobberBarrierWriteSkipped: number;
          metadataParseFailures: number;
          orphanedFilesRemoved: number;
          orphansReindexed: number;
          available: boolean;
          breakerTripped: boolean;
      }

      • size: number
      • count: number
      • reads: number
      • writes: number
      • misses: number
      • canceledReads: number
      • activeReads: number
      • queuedReads: number
      • oversizedWriteSkipped: number
      • quotaWriteSkipped: number
      • evictions: number
      • writeFailures: number
      • corruptedEntries: number
      • clobberBarrierWriteSkipped: number
      • metadataParseFailures: number
      • orphanedFilesRemoved: number
      • orphansReindexed: number

        Unindexed files recovered into the index (open-time reconcile or a first read).

      • available: boolean

        S2: true when OPFS was reachable on init and the store is still alive. false when init couldn't acquire a directory handle (browser without OPFS support, private mode in some configs), after the circuit breaker trips, or after dispose(). Drives the opfs-unavailable status badge.

      • breakerTripped: boolean

        true once the circuit breaker disabled the tier after consecutive OPFS timeouts. Distinguishes "gave up after repeated stalls" from "never had OPFS" in debug snapshots and tests; the monitor badge keys on available alone.

    • Make hash the content hash. Index entries were written under the old hash's file tag, so a different tag retires them: the index empties (their files become orphans the next reconcile deletes) and the generation bump discards any write still in flight under the old tag. In practice the index is already empty here — a hash change goes through clear() — except for entries written while no hash was known.

      Parameters

      • hash: string | null

      Returns void

    • Put hash in the identity file, chained behind any earlier identity write. Chunk writes wait for it (doSet). A failed write removes the file instead, so a stale identity can never vouch for chunks written under a new hash.

      Parameters

      • hash: string | null

      Returns void

    • Record the validation mode used for this dataset. Persisted to _cache_meta.json so a follow-up session can re-evaluate (e.g. a TTL window).

      validated (default true) means a genuine validation just succeeded, which stamps lastValidatedAt = now. The no-token/offline branch passes validated: false: it still records the mode, but must NOT slide the TTL clock forward on every revisit — it only establishes the baseline the first time (when lastValidatedAt is still unset) so a headerless-server cache can still age out.

      Parameters

      Returns void

    • Tear down the store. Marks the store disposed, drains pending writes, awaits any in-flight metadata save, then flushes a final snapshot so a read-only session's LRU order still gets persisted.

      Every caller shares ONE completion (pendingDispose): dispose() resolving is the take-over signal for a newer same-URL store, so a concurrent or repeat caller must wait for the same drain rather than resolve early on the disposed flag.

      Order matters:

      1. Set disposed = true SYNCHRONOUSLY, before the first await. dispose() itself suspends below (drain + awaitInFlight), and every disposed guard — init()'s re-checks, clear(), the validation setters — must observe the flag from the moment dispose() is invoked, not only once the final flush completes; otherwise a mid-init store could keep probe-writing or orphan-cleaning the shared directory during that window. The final flush below calls the metadata manager directly, so it is unaffected by the flag.
      2. Bump generation so any in-flight doSet that resolves afterwards detects the mismatch and skips its index update.
      3. Cancel the debounced timer (we flush directly below).
      4. Await any in-flight init() or clear(). Their disposed guards stop FURTHER mutations, but an OPFS operation already initiated at an await cannot be cancelled — dispose() resolving is the signal that a newer same-URL store may take over the shared directory, so nothing this store started may still be outstanding at that point.
      5. Drain pendingWrites so file I/O for in-flight set() calls finishes and the index reflects settled state before we snapshot it. 5b. Drain in-flight real deletes (pendingDeletesByDataset): doDelete chains behind pendingWrites, so a timed-out delete's non-cancellable removeEntry + reconcile can still be outstanding — draining lets it finish before we snapshot (#1073). Bounded by the op timeout; a delete that outlives the drain stays in the static registry, which is what actually protects a successor same-URL store's writes.
      6. Await any metadata save already mid-write so the final save wins on disk (last writer), then write the latest snapshot. The flush is unconditional (not gated on hasPendingSave) because read-driven LRU order no longer schedules its own save (see touch()); dispose is where a read-only session's order gets persisted. Deadline-bounded: a stalled backend must not leave dispose() unresolved.

      Returns Promise<void>

    • Start any pending (debounced) index save NOW. Best-effort and non-blocking: the write is started synchronously and not awaited, because the page may be frozen or killed right after the lifecycle event that asks for it. A no-op when nothing is pending or the store is not live.

      Returns void

    • flushMetadata for every live store. Wired to pagehide and visibilitychange → hidden (core/app/lifecycle/unload-handling.ts): the signals that still fire when beforeunload (and therefore dispose) does not — mobile tab switches, bfcache navigations, discarded tabs.

      Returns void