@muhkoo/connect 0.7.0-alpha.5 → 0.8.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/connect.d.ts CHANGED
@@ -754,7 +754,13 @@ declare enum EventCoreEvents {
754
754
  DATA_SENT = "data_sent",
755
755
  MESSAGE = "message",
756
756
  GET_HISTORY = "get_history",
757
- RECEIVED_HISTORY = "received_history"
757
+ RECEIVED_HISTORY = "received_history",
758
+ ONLINE = "online",
759
+ OFFLINE = "offline",
760
+ SYNCING = "syncing",
761
+ SYNC_PROGRESS = "sync_progress",
762
+ SYNC_COMPLETE = "sync_complete",
763
+ SYNC_ERROR = "sync_error"
758
764
  }
759
765
  /**
760
766
  * @public
@@ -1399,6 +1405,16 @@ declare function manifestToStat(manifest: FileManifest): FileStat;
1399
1405
  * skip a PUT they'd otherwise dedup-rewrite. The
1400
1406
  * server can ignore the optimization if it wants.
1401
1407
  */
1408
+ /**
1409
+ * A content-addressed byte cache for shards (the offline layer's `ShardCache`
1410
+ * satisfies this structurally). Declared here as a minimal interface so the
1411
+ * storage layer doesn't import the offline module — keeping the dependency
1412
+ * arrow one-way (offline → storage).
1413
+ */
1414
+ interface ShardByteCache {
1415
+ get(hash: string): Promise<Uint8Array | null>;
1416
+ put(hash: string, bytes: Uint8Array): Promise<void>;
1417
+ }
1402
1418
  interface ShardClientOptions {
1403
1419
  /** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
1404
1420
  baseUrl: string;
@@ -1418,6 +1434,18 @@ interface ShardClientOptions {
1418
1434
  * Provided so callers can inject retries / auth / instrumentation.
1419
1435
  */
1420
1436
  fetch?: typeof fetch;
1437
+ /**
1438
+ * Optional offline byte cache. When present, GETs are served cache-first and
1439
+ * populated on success, and PUTs write the cache before the network so a
1440
+ * just-uploaded file reads back offline. See {@link ShardByteCache}.
1441
+ */
1442
+ cache?: ShardByteCache;
1443
+ /**
1444
+ * Called when a PUT can't reach the network (offline). Lets the caller queue
1445
+ * the upload for replay on reconnect; the bytes are already in {@link cache}
1446
+ * keyed by `hash`, so only the hash needs recording.
1447
+ */
1448
+ deferUpload?: (hash: string) => Promise<void>;
1421
1449
  }
1422
1450
  /**
1423
1451
  * Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
@@ -1428,6 +1456,8 @@ declare class ShardClient {
1428
1456
  private readonly baseUrl;
1429
1457
  private readonly pathPrefix;
1430
1458
  private readonly fetchFn;
1459
+ private readonly cache?;
1460
+ private readonly deferUpload?;
1431
1461
  constructor(opts: ShardClientOptions);
1432
1462
  /** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
1433
1463
  urlForHash(hash: string): string;
@@ -2717,6 +2747,410 @@ declare class HttpClient {
2717
2747
  private request;
2718
2748
  }
2719
2749
 
2750
+ /**
2751
+ * `LWWRegister` — a last-write-wins register CRDT, the conflict model for a
2752
+ * `kv` value and for an app-state snapshot. A register holds a single value
2753
+ * stamped with the {@link HlcTimestamp} of the write that produced it. Two
2754
+ * concurrent writes converge to the one with the greater HLC; the loser is
2755
+ * discarded. A delete is just a write that sets `deleted` (a tombstone), so a
2756
+ * stale write that arrives after a newer delete can't resurrect the value.
2757
+ *
2758
+ * Because the HLC total-orders every write deterministically (down to a nodeId
2759
+ * tiebreak), every replica that has seen the same set of writes lands on the
2760
+ * same value — that's the convergence guarantee.
2761
+ */
2762
+ interface LwwRegister<T> {
2763
+ /** The current value, or `null` when tombstoned / never written. */
2764
+ value: T | null;
2765
+ /** HLC of the write that produced this state. */
2766
+ hlc: string;
2767
+ /** True when the latest write was a delete. */
2768
+ deleted: boolean;
2769
+ }
2770
+ /** An empty register that loses to any real write. */
2771
+ declare function emptyRegister<T>(): LwwRegister<T>;
2772
+ /**
2773
+ * Merge two register states, returning the winner (greater HLC). Pure: neither
2774
+ * input is mutated. Used identically on the inbound change feed and on
2775
+ * sync-replay reconciliation so there's exactly one definition of "who wins".
2776
+ */
2777
+ declare function mergeRegister<T>(a: LwwRegister<T>, b: LwwRegister<T>): LwwRegister<T>;
2778
+ /** Build the register produced by a `set`. */
2779
+ declare function writeRegister<T>(value: T, hlc: string): LwwRegister<T>;
2780
+ /** Build the register produced by a `delete` (a tombstone at `hlc`). */
2781
+ declare function deleteRegister<T>(hlc: string): LwwRegister<T>;
2782
+
2783
+ /**
2784
+ * `ConnectivityManager` — the single source of truth for "are we online", which
2785
+ * the offline layer needs to decide between a network call and a cache read, and
2786
+ * to know when to flush the durable outbound queue.
2787
+ *
2788
+ * `navigator.onLine` alone lies often (captive portals, VPN flaps, a laptop
2789
+ * that's "connected" to a network with no route), so we fuse three signals:
2790
+ * 1. the browser `online`/`offline` events (coarse but free),
2791
+ * 2. real fetch outcomes reported by {@link ../core/HttpClient} and the shard
2792
+ * client — a thrown `TypeError`/abort is the most reliable "offline" tell,
2793
+ * and a 2xx is the most reliable "online" tell,
2794
+ * 3. the sync engine, which flips us to `syncing` while it drains the queue.
2795
+ *
2796
+ * The offline→online edge fires `onReconnect` (wired to the sync engine) so
2797
+ * queued writes replay automatically. Transitions are debounced to avoid
2798
+ * flapping the UI on a brief blip. State changes also emit on {@link EventCore}
2799
+ * (`ONLINE`/`OFFLINE`/`SYNCING`) so apps can render a status indicator.
2800
+ */
2801
+ type ConnectivityState = "online" | "offline" | "syncing";
2802
+ interface ConnectivityOptions {
2803
+ /** Called on each offline→online transition (drives queue replay). */
2804
+ onReconnect?: () => void;
2805
+ /** Debounce window for offline transitions, ms. Default 1500. */
2806
+ debounceMs?: number;
2807
+ }
2808
+ declare class ConnectivityManager {
2809
+ private state;
2810
+ private readonly listeners;
2811
+ private readonly onReconnect?;
2812
+ private readonly debounceMs;
2813
+ private offlineTimer;
2814
+ private started;
2815
+ private readonly handleOnline;
2816
+ private readonly handleOffline;
2817
+ constructor(opts?: ConnectivityOptions);
2818
+ /** Attach to the browser online/offline events. Safe to call in Node (no-op). */
2819
+ start(): void;
2820
+ stop(): void;
2821
+ get current(): ConnectivityState;
2822
+ get isOnline(): boolean;
2823
+ /** Subscribe to state changes. Returns an unsubscribe function. */
2824
+ onChange(cb: (state: ConnectivityState) => void): () => void;
2825
+ /** Report a failed network attempt (thrown fetch / abort). */
2826
+ reportFetchFailure(): void;
2827
+ /** Report a successful network round-trip — strongest "we're online" signal. */
2828
+ reportFetchSuccess(): void;
2829
+ /** The sync engine entering its drain phase. */
2830
+ markSyncing(): void;
2831
+ /** The sync engine finished — settle back to online. */
2832
+ markSynced(): void;
2833
+ private markOnline;
2834
+ private scheduleOffline;
2835
+ private set;
2836
+ }
2837
+
2838
+ type StoreName = "kv-cache" | "db-row-cache" | "db-query-cache" | "space-messages" | "space-cursors" | "outbound-queue" | "snapshots" | "hlc-state" | "meta";
2839
+
2840
+ /**
2841
+ * `OfflineStore` — the pluggable persistence interface the offline layer is
2842
+ * built on, mirroring the {@link ../../core/Session.SessionStore} pattern. The
2843
+ * default browser implementation is {@link ./IndexedDbStore}; environments
2844
+ * without IndexedDB (Node, Workers, SSR) get {@link ./NoopStore}, whose every
2845
+ * method resolves to empty so the namespace hooks become branch-free no-ops.
2846
+ *
2847
+ * Keys are plain strings (most are prefixed with the user's `commitment` to
2848
+ * isolate users on a shared origin). Values are whatever the caller stores —
2849
+ * the store is format-agnostic; CRDT/ciphertext shape is the namespace's
2850
+ * concern.
2851
+ */
2852
+
2853
+ /** A durable outbound mutation awaiting replay on reconnect. */
2854
+ interface QueueEntry {
2855
+ /** Auto-assigned monotonic sequence (insertion order). */
2856
+ seq: number;
2857
+ /** HLC stamp of the mutation — the order replays run in. */
2858
+ hlc: string;
2859
+ /** Client-generated id for idempotent replay / dedupe. */
2860
+ clientId: string;
2861
+ domain: "kv" | "db" | "file" | "space" | "snapshot";
2862
+ /** Namespace method to re-invoke (e.g. `"set"`, `"update"`, `"sendMessage"`). */
2863
+ method: string;
2864
+ /** Serializable arguments captured at enqueue time. */
2865
+ args: unknown;
2866
+ }
2867
+ interface OfflineEntry<T> {
2868
+ key: string;
2869
+ value: T;
2870
+ }
2871
+ interface OfflineStore {
2872
+ /** False for {@link ./NoopStore} so hooks can skip all work cheaply. */
2873
+ readonly enabled: boolean;
2874
+ /** Resolve once the backing store is open and ready (no-op when disabled). */
2875
+ ready(): Promise<void>;
2876
+ get<T>(store: StoreName, key: string): Promise<T | null>;
2877
+ put<T>(store: StoreName, key: string, value: T): Promise<void>;
2878
+ delete(store: StoreName, key: string): Promise<void>;
2879
+ /** Entries whose key starts with `prefix`, in ascending key order. */
2880
+ prefix<T>(store: StoreName, prefix: string): Promise<Array<OfflineEntry<T>>>;
2881
+ /** Entries whose key is in `[lower, upper)`, in ascending key order. */
2882
+ range<T>(store: StoreName, lower: string, upper: string): Promise<Array<OfflineEntry<T>>>;
2883
+ /** Every entry in a store, in ascending key order. */
2884
+ all<T>(store: StoreName): Promise<Array<OfflineEntry<T>>>;
2885
+ clear(store: StoreName): Promise<void>;
2886
+ /** Append a mutation to the durable outbound queue; resolves with its seq. */
2887
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
2888
+ /** Remove a drained queue entry by seq. */
2889
+ dequeue(seq: number): Promise<void>;
2890
+ /** All queued mutations, HLC-ordered (the order to replay them in). */
2891
+ queued(): Promise<QueueEntry[]>;
2892
+ }
2893
+
2894
+ /**
2895
+ * `OutboundQueue` — the durable record of writes made while offline (or that
2896
+ * failed mid-flight) plus the machinery to replay them on reconnect. This is
2897
+ * what makes offline writes *survive a reload*: unlike the in-memory buffer in
2898
+ * {@link ../transport/WSTransport}, every entry lives in IndexedDB until it has
2899
+ * been acknowledged by the server.
2900
+ *
2901
+ * Each domain registers a **replayer** — a function that re-applies one queued
2902
+ * mutation by re-invoking the real namespace method (with an `isReplay` flag so
2903
+ * it doesn't re-enqueue). `drain()` replays entries in HLC order:
2904
+ * - success → remove the entry;
2905
+ * - permanent failure (4xx) → drop it and surface a `SYNC_ERROR` (a write the
2906
+ * server will never accept must not wedge the queue forever);
2907
+ * - transient/network failure → stop draining and leave the rest for the next
2908
+ * reconnect.
2909
+ *
2910
+ * Replays are idempotent by construction (kv/db are LWW-by-key, messages dedupe
2911
+ * by `clientId`, file PUTs are content-addressed), so re-running a partially
2912
+ * drained queue is safe.
2913
+ */
2914
+
2915
+ type Replayer = (entry: QueueEntry) => Promise<void>;
2916
+ declare class OutboundQueue {
2917
+ private readonly store;
2918
+ private readonly replayers;
2919
+ private draining;
2920
+ constructor(store: OfflineStore);
2921
+ /** A domain registers how to replay its mutations. */
2922
+ register(domain: QueueEntry["domain"], replayer: Replayer): void;
2923
+ /** Record a mutation for later replay; resolves with its assigned seq. */
2924
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
2925
+ /** Number of writes still awaiting sync. */
2926
+ pending(): Promise<number>;
2927
+ /**
2928
+ * Replay queued mutations in HLC order. Coalesces concurrent calls into a
2929
+ * single in-flight drain. Stops on the first transient failure, leaving the
2930
+ * remaining entries for the next attempt.
2931
+ */
2932
+ drain(): Promise<void>;
2933
+ private runDrain;
2934
+ }
2935
+
2936
+ /**
2937
+ * `SyncEngine` — orchestrates catch-up when the client comes back online. It's
2938
+ * the thing wired to the {@link ConnectivityManager}'s offline→online edge.
2939
+ *
2940
+ * One `run()` does, in order:
2941
+ * 1. **Guard** — only proceed when the session is authenticated and unlocked
2942
+ * (replays may need to re-seal/re-encrypt, and there's no point pushing
2943
+ * writes for a logged-out user).
2944
+ * 2. **Drain** the durable {@link OutboundQueue} — push every offline write.
2945
+ * 3. **Catch-up tasks** — domain-registered pulls that reconcile *inbound*
2946
+ * state we missed while offline (e.g. a Space paging history forward to its
2947
+ * last-seen handle). Registered by the namespaces.
2948
+ *
2949
+ * Concurrent triggers coalesce into a single in-flight run (a reconnect often
2950
+ * fires several signals at once), mirroring `Client.recoverInFlight`.
2951
+ */
2952
+
2953
+ interface SyncEngineDeps {
2954
+ queue: OutboundQueue;
2955
+ connectivity: ConnectivityManager;
2956
+ /** Proceed only when this returns true (authenticated + unlocked). */
2957
+ canSync: () => boolean;
2958
+ }
2959
+ declare class SyncEngine {
2960
+ private readonly deps;
2961
+ private readonly catchUpTasks;
2962
+ private running;
2963
+ constructor(deps: SyncEngineDeps);
2964
+ /** Register an inbound reconciliation task run after the queue drains. */
2965
+ registerCatchUp(task: () => Promise<void>): () => void;
2966
+ /** Drain + catch up. Concurrent calls share one in-flight run. */
2967
+ run(): Promise<void>;
2968
+ private execute;
2969
+ }
2970
+
2971
+ /**
2972
+ * `client.offline.snapshot` — a small, encrypted, app-state cache. Apps stash
2973
+ * whatever they need to repaint instantly on a cold offline boot: the last open
2974
+ * Space, a draft message, scroll position, the current route. It's deliberately
2975
+ * separate from `client.kv`: snapshots are **local-only by default** (instant,
2976
+ * never metered, never leave the device) and hold view state rather than domain
2977
+ * data.
2978
+ *
2979
+ * await client.offline.snapshot.save('ui', { route, openSpaceId, draft })
2980
+ * const ui = await client.offline.snapshot.load<UiState>('ui')
2981
+ *
2982
+ * Values are encrypted at rest with the same identity-derived {@link StorageCipher}
2983
+ * as `client.kv`, so a snapshot is unreadable until the user unlocks — `load`
2984
+ * returns `null` when locked or absent, and `save` throws {@link OfflineLockedError}.
2985
+ * Conflict model is a single-writer LWW register (the latest save wins).
2986
+ */
2987
+
2988
+ interface SnapshotNamespaceDeps {
2989
+ store: OfflineStore;
2990
+ session: SessionState;
2991
+ /** Stamp the saved record (LWW ordering); supplied by the OfflineManager. */
2992
+ nextHlc: () => Promise<string>;
2993
+ }
2994
+ declare class SnapshotNamespace {
2995
+ private readonly deps;
2996
+ private cipher;
2997
+ private cipherIdentity;
2998
+ constructor(deps: SnapshotNamespaceDeps);
2999
+ /** Persist `state` under `name` (encrypted). No-op when offline support is off. */
3000
+ save<T = unknown>(name: string, state: T): Promise<void>;
3001
+ /** Load and decrypt the snapshot `name`, or `null` if absent/locked. */
3002
+ load<T = unknown>(name: string): Promise<T | null>;
3003
+ /** Remove the snapshot `name`. */
3004
+ delete(name: string): Promise<void>;
3005
+ /** Names of all snapshots for the current user. */
3006
+ list(): Promise<string[]>;
3007
+ private key;
3008
+ private commitment;
3009
+ private requireCipher;
3010
+ }
3011
+
3012
+ /**
3013
+ * `ShardCache` — a Cache-API-backed store for file-shard bytes. Shards are
3014
+ * content-addressed (the key IS the SHA-256 of the ciphertext), so caching them
3015
+ * is uniquely safe: a cache hit is self-validating, immutable, and shareable
3016
+ * across files. The Cache API (rather than IndexedDB) is the right home for
3017
+ * large binary blobs — it streams, it's purpose-built for response bodies, and
3018
+ * it has its own eviction.
3019
+ *
3020
+ * Keys are SYNTHETIC absolute URLs (`https://shard.muhkoo.local/<hash>`) rather
3021
+ * than the real shard endpoint, so the cache is addressable by hash alone — the
3022
+ * sync engine can re-PUT a deferred shard knowing only its hash, without
3023
+ * reconstructing the accelerator URL. We never fetch these synthetic URLs; they
3024
+ * exist only as cache keys.
3025
+ *
3026
+ * Every method degrades to a no-op / null where the Cache API is unavailable
3027
+ * (Node, Workers, insecure contexts), so callers don't need to branch.
3028
+ */
3029
+ declare class ShardCache {
3030
+ static available(): boolean;
3031
+ private key;
3032
+ private open;
3033
+ /** Cached shard bytes, or `null` on miss / when unavailable. */
3034
+ get(hash: string): Promise<Uint8Array | null>;
3035
+ /** Store shard bytes under their hash. Best-effort. */
3036
+ put(hash: string, bytes: Uint8Array): Promise<void>;
3037
+ /** Whether a shard is present in the cache. */
3038
+ has(hash: string): Promise<boolean>;
3039
+ }
3040
+
3041
+ /**
3042
+ * `OfflineManager` — the object `client.offline` points at, and the single
3043
+ * owner of everything the offline layer needs at runtime: the {@link
3044
+ * OfflineStore}, the {@link HLC} (Hybrid Logical Clock), the {@link
3045
+ * ConnectivityManager}, the durable {@link OutboundQueue}, the {@link
3046
+ * SyncEngine}, and the {@link SnapshotNamespace}.
3047
+ *
3048
+ * The {@link ../core/Client} constructs one of these (always — it's a cheap
3049
+ * no-op when offline support is off) and hands the namespaces the small set of
3050
+ * primitives they need to participate: `nextHlc()` to stamp a write,
3051
+ * `observeHlc()` to fold in a remote stamp, `enqueue()` to record an offline
3052
+ * mutation, and `registerReplayer()`/`registerCatchUp()` to define how their
3053
+ * domain re-syncs on reconnect. Keeping that wiring here means the namespaces
3054
+ * stay decoupled from IndexedDB and the clock.
3055
+ */
3056
+
3057
+ interface OfflineManagerDeps {
3058
+ store: OfflineStore;
3059
+ session: SessionState;
3060
+ /** Whether offline support is active (browser) vs a no-op (Node/Workers). */
3061
+ enabled: boolean;
3062
+ }
3063
+ declare class OfflineManager {
3064
+ /** True in browsers with IndexedDB; false when running over a NoopStore. */
3065
+ readonly enabled: boolean;
3066
+ readonly store: OfflineStore;
3067
+ readonly connectivity: ConnectivityManager;
3068
+ readonly queue: OutboundQueue;
3069
+ readonly sync: SyncEngine;
3070
+ /** `client.offline.snapshot` — encrypted app-state cache. */
3071
+ readonly snapshot: SnapshotNamespace;
3072
+ /** Cache-API store for file-shard bytes (browser only). */
3073
+ readonly fileCache?: ShardCache;
3074
+ private hlc;
3075
+ private nodeId;
3076
+ private readonly bootPromise;
3077
+ private persistTimer;
3078
+ constructor(deps: OfflineManagerDeps);
3079
+ /**
3080
+ * Queue a shard upload that couldn't reach the network. The bytes are
3081
+ * already cached (keyed by `hash`), so only the hash is recorded; the file
3082
+ * replayer re-PUTs from the cache on reconnect.
3083
+ */
3084
+ deferShardUpload(hash: string): Promise<void>;
3085
+ /** Resolve once the clock + node id are loaded. */
3086
+ ready(): Promise<void>;
3087
+ /** Current connectivity state (`online` | `offline` | `syncing`). */
3088
+ get status(): ConnectivityState;
3089
+ /** Subscribe to connectivity changes. Returns an unsubscribe function. */
3090
+ onStatusChange(cb: (state: ConnectivityState) => void): () => void;
3091
+ /** Stamp a locally-generated write with a fresh, monotonic HLC. */
3092
+ nextHlc(): Promise<string>;
3093
+ /** Fold an observed remote HLC into the clock (call for inbound stamps). */
3094
+ observeHlc(remote: string): Promise<void>;
3095
+ /** A fresh client-generated id (idempotent replay / message dedupe). */
3096
+ newClientId(): string;
3097
+ /** Record an offline mutation for replay on reconnect. */
3098
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
3099
+ /** A domain registers how to replay its queued mutations. */
3100
+ registerReplayer(domain: QueueEntry["domain"], replayer: Replayer): void;
3101
+ /** A domain registers an inbound catch-up task (run after the queue drains). */
3102
+ registerCatchUp(task: () => Promise<void>): () => void;
3103
+ /** Surface a network failure to connectivity tracking. */
3104
+ reportFetchFailure(): void;
3105
+ /** Surface a successful round-trip to connectivity tracking. */
3106
+ reportFetchSuccess(): void;
3107
+ /** Tear down listeners (tests / hot-reload). */
3108
+ dispose(): void;
3109
+ private boot;
3110
+ /** Coalesce HLC-state writes — a burst of stamps persists once. */
3111
+ private schedulePersist;
3112
+ }
3113
+
3114
+ /**
3115
+ * `KvCache` — the offline adapter for `client.kv`. Stores each value's
3116
+ * ciphertext envelope (never plaintext) under `${commitment}|${collection}/${id}`
3117
+ * as an LWW-Register stamped with an HLC, so reads work offline and concurrent
3118
+ * writes converge (see {@link ./crdt/LWWRegister}). Deletes are causal
3119
+ * tombstones — a stale write can't resurrect a key a newer delete removed.
3120
+ *
3121
+ * True cross-device convergence also needs the accelerator to honor the HLC
3122
+ * (see the server's `kvmeta` sidecar); this adapter embeds the stamp now and
3123
+ * degrades gracefully (last-writer-by-arrival) until that ships.
3124
+ */
3125
+
3126
+ type KvEntry = LwwRegister<unknown>;
3127
+ declare class KvCache {
3128
+ private readonly manager;
3129
+ private readonly store;
3130
+ constructor(manager: OfflineManager);
3131
+ get enabled(): boolean;
3132
+ nextHlc(): Promise<string>;
3133
+ newClientId(): string;
3134
+ private key;
3135
+ /** Read the cached register for `collection/id`, or null when absent. */
3136
+ read(commitment: string, fullKey: string): Promise<KvEntry | null>;
3137
+ /** Optimistically record a local `set` (ciphertext envelope + HLC). */
3138
+ writeLocal(commitment: string, fullKey: string, envelope: unknown, hlc: string): Promise<void>;
3139
+ /** Optimistically record a local `delete` (tombstone at HLC). */
3140
+ deleteLocal(commitment: string, fullKey: string, hlc: string): Promise<void>;
3141
+ /**
3142
+ * Merge an authoritative value (from a GET refresh or a realtime frame) into
3143
+ * the cache by HLC. When the server doesn't supply an HLC yet, treat it as
3144
+ * "now-ish" by stamping our clock so a server value still beats stale local
3145
+ * state but a fresh local write wins.
3146
+ */
3147
+ mergeRemote(commitment: string, fullKey: string, envelope: unknown | null, op: "set" | "delete", hlc?: string): Promise<void>;
3148
+ /** Non-tombstoned full keys for a commitment (used for offline `list`). */
3149
+ liveKeys(commitment: string): Promise<string[]>;
3150
+ /** Queue a kv mutation for replay on reconnect. */
3151
+ enqueue(method: "set" | "delete", args: unknown, hlc: string, clientId: string): Promise<void>;
3152
+ }
3153
+
2720
3154
  /**
2721
3155
  * `client.kv` — per-user key/value storage backed by the accelerator's personal
2722
3156
  * space.
@@ -2746,6 +3180,8 @@ interface KvNamespaceDeps {
2746
3180
  session: SessionState;
2747
3181
  /** WebSocket base (ws/wss) for the change feed; derived from baseUrl. */
2748
3182
  wsBaseUrl: string;
3183
+ /** Offline cache + queue. Undefined ⇒ no offline behavior (network-only). */
3184
+ offline?: KvCache;
2749
3185
  }
2750
3186
  interface SetOptions {
2751
3187
  /** Encrypt the value at rest (default: true). */
@@ -2763,6 +3199,8 @@ declare class KvNamespace {
2763
3199
  private cipher;
2764
3200
  private cipherIdentity;
2765
3201
  constructor(deps: KvNamespaceDeps);
3202
+ /** Replay one queued kv mutation (called by the sync engine). */
3203
+ replay(entry: QueueEntry): Promise<void>;
2766
3204
  /** Store `value` under `collection`/`id` (encrypted at rest by default). */
2767
3205
  set<T = unknown>(collection: string, id: string, value: T, opts?: SetOptions): Promise<void>;
2768
3206
  /** Fetch the value at `collection`/`id`, or `null` if absent. */
@@ -2791,6 +3229,45 @@ declare class KvNamespace {
2791
3229
  private getCipher;
2792
3230
  }
2793
3231
 
3232
+ /**
3233
+ * `DbCache` — the offline adapter for `client.db`. Unlike kv, db rows are
3234
+ * server-plaintext, so they're cached as-is. The SDK doesn't know a table's
3235
+ * primary-key column, so it caches schema-agnostically:
3236
+ * - `get(id)` results keyed by the requested id (`${table}|row|${id}`),
3237
+ * - `query(q)` result pages keyed by a stable hash of the query.
3238
+ *
3239
+ * Writes carry per-column HLC stamps (`_hlc`) so the accelerator's per-column
3240
+ * LWW merge can converge concurrent edits; offline writes are queued and
3241
+ * replayed. Optimistic row patches keep `get` fresh between sync cycles.
3242
+ */
3243
+
3244
+ interface DbQueryPage {
3245
+ rows: Array<Record<string, unknown>>;
3246
+ nextCursor: string | null;
3247
+ }
3248
+ /** Stable JSON for query-cache keys (sorted object keys → deterministic). */
3249
+ declare function stableStringify(value: unknown): string;
3250
+ declare class DbCache {
3251
+ private readonly manager;
3252
+ private readonly store;
3253
+ constructor(manager: OfflineManager);
3254
+ get enabled(): boolean;
3255
+ nextHlc(): Promise<string>;
3256
+ newClientId(): string;
3257
+ /** Build the `{ [col]: hlc }` map for a write (one stamp across its columns). */
3258
+ columnStamps(values: Record<string, unknown>, hlc: string): Record<string, string>;
3259
+ private rowKey;
3260
+ private queryKey;
3261
+ readRow(table: string, id: string): Promise<Record<string, unknown> | null>;
3262
+ writeRow(table: string, id: string, row: Record<string, unknown>): Promise<void>;
3263
+ /** Shallow-merge a partial update into a cached row (optimistic). */
3264
+ patchRow(table: string, id: string, values: Record<string, unknown>): Promise<void>;
3265
+ deleteRow(table: string, id: string): Promise<void>;
3266
+ readQuery(table: string, q: unknown): Promise<DbQueryPage | null>;
3267
+ writeQuery(table: string, q: unknown, page: DbQueryPage): Promise<void>;
3268
+ enqueue(method: "insert" | "update" | "delete", args: unknown, hlc: string, clientId: string): Promise<void>;
3269
+ }
3270
+
2794
3271
  /**
2795
3272
  * `client.db` — the app's scalable database. A developer defines tables in the
2796
3273
  * Muhkoo portal (columns + indexes); each table is backed by its own Durable
@@ -2822,6 +3299,8 @@ declare class KvNamespace {
2822
3299
 
2823
3300
  interface DbNamespaceDeps {
2824
3301
  http: HttpClient;
3302
+ /** Offline row/query cache + write queue. Undefined ⇒ network-only. */
3303
+ offline?: DbCache;
2825
3304
  }
2826
3305
  /** Comparison operators allowed in a {@link DbQuery} `where` clause. */
2827
3306
  type DbFilterOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "like" | "likeStartsWith" | "likeContains";
@@ -2855,8 +3334,10 @@ interface DbQueryResult<T = Record<string, unknown>> {
2855
3334
  declare class DbTable<T extends Record<string, unknown> = Record<string, unknown>> {
2856
3335
  private readonly http;
2857
3336
  private readonly name;
2858
- constructor(http: HttpClient, name: string);
3337
+ private readonly offline?;
3338
+ constructor(http: HttpClient, name: string, offline?: DbCache | undefined);
2859
3339
  private base;
3340
+ private rowUrl;
2860
3341
  /** Insert a row. Returns the created row and its primary-key value. */
2861
3342
  insert(values: Partial<T>): Promise<{
2862
3343
  row: T;
@@ -2884,6 +3365,8 @@ declare class DbNamespace {
2884
3365
  * const todos = client.db.table<Todo>('todos');
2885
3366
  */
2886
3367
  table<T extends Record<string, unknown> = Record<string, unknown>>(name: string): DbTable<T>;
3368
+ /** Replay one queued db mutation (called by the sync engine). */
3369
+ replay(entry: QueueEntry): Promise<void>;
2887
3370
  }
2888
3371
 
2889
3372
  /**
@@ -2923,6 +3406,10 @@ interface FileNamespaceDeps {
2923
3406
  * deploy-time-precompiled environments). Forwarded to {@link FileStorage}.
2924
3407
  */
2925
3408
  rsWasmModule?: WebAssembly.Module;
3409
+ /** Offline shard-byte cache (browser). Enables offline reads of cached files. */
3410
+ shardCache?: ShardByteCache;
3411
+ /** Queue a shard PUT that couldn't reach the network (offline). */
3412
+ deferShardUpload?: (hash: string) => Promise<void>;
2926
3413
  }
2927
3414
  interface WriteFileOptions {
2928
3415
  /** SharedSpace the file is written to (obtain via `client.space`). */
@@ -3308,6 +3795,46 @@ interface SpaceMessageEvent {
3308
3795
  */
3309
3796
  handle: number;
3310
3797
  message: Message;
3798
+ /**
3799
+ * Sender-generated id, present on locally-originated messages (carried in the
3800
+ * cleartext `cid` header). Lets the UI dedupe an optimistic send against its
3801
+ * server echo, and the offline cache replace a pending entry with the real one.
3802
+ */
3803
+ clientId?: string;
3804
+ /** True for an optimistic local send not yet acknowledged by the server. */
3805
+ pending?: boolean;
3806
+ }
3807
+ /**
3808
+ * One sealed message frame as cached for offline use. The `packet` is the
3809
+ * ciphertext-bearing serialized {@link Packet} (decrypted lazily on read), so
3810
+ * nothing readable is persisted. Structurally matches the offline layer's
3811
+ * `MessageEntry`; declared here so the spaces module doesn't import `../offline`.
3812
+ */
3813
+ interface CachedSpaceMessage {
3814
+ handle: string;
3815
+ packet: string | null;
3816
+ op: "msg" | "edit" | "delete";
3817
+ clientId?: string;
3818
+ hlc?: string;
3819
+ pending?: boolean;
3820
+ deleted?: boolean;
3821
+ }
3822
+ /** The offline adapter a {@link Space} drives. Implemented by `SpaceCache`. */
3823
+ interface SpaceOfflineAdapter {
3824
+ readonly enabled: boolean;
3825
+ newClientId(): string;
3826
+ nextHlc(): Promise<string>;
3827
+ putMessage(spaceId: string, entry: CachedSpaceMessage): Promise<void>;
3828
+ putDeleted(spaceId: string, handle: number): Promise<void>;
3829
+ dropPending(spaceId: string, clientId: string): Promise<void>;
3830
+ loadMessages(spaceId: string): Promise<CachedSpaceMessage[]>;
3831
+ getCursor(spaceId: string): Promise<{
3832
+ lastSeenHandle: number;
3833
+ oldestHandle: number;
3834
+ } | null>;
3835
+ observeHandle(spaceId: string, handle: number): Promise<void>;
3836
+ enqueueFrame(spaceId: string, frame: unknown, clientId: string, hlc: string): Promise<void>;
3837
+ registerCatchUp(task: () => Promise<void>): () => void;
3311
3838
  }
3312
3839
  /**
3313
3840
  * An inbound ephemeral signal — an application broadcast the server relays to
@@ -3357,6 +3884,12 @@ interface SpaceDeps {
3357
3884
  * without a live websocket.
3358
3885
  */
3359
3886
  createChannel?: (url: string, myId: string) => SpaceChannelLike;
3887
+ /** Offline shard-byte cache for room files (browser). */
3888
+ shardCache?: ShardByteCache;
3889
+ /** Queue a room-file shard PUT that couldn't reach the network. */
3890
+ deferShardUpload?: (hash: string) => Promise<void>;
3891
+ /** Offline message cache + send queue. Undefined ⇒ no offline behavior. */
3892
+ offline?: SpaceOfflineAdapter;
3360
3893
  }
3361
3894
  /** The subset of {@link BroadcastChannel} a {@link Space} drives. */
3362
3895
  interface SpaceChannelLike {
@@ -3380,6 +3913,7 @@ declare class Space {
3380
3913
  /** Per-instance event bus for fan-out events (isolated across spaces). */
3381
3914
  private readonly events;
3382
3915
  private rawWired;
3916
+ private catchUpUnsub;
3383
3917
  constructor(deps: SpaceDeps);
3384
3918
  /** The encoded space id (same as the transport name for fan-out spaces). */
3385
3919
  get id(): string;
@@ -3429,6 +3963,14 @@ declare class Space {
3429
3963
  * original author. Receivers get an {@link onMessageDeleted} event.
3430
3964
  */
3431
3965
  deleteMessage(handle: number): Promise<void>;
3966
+ /**
3967
+ * Decrypt and return the locally-cached message log for this space, in
3968
+ * handle order (pending sends last). Works offline — the source is the
3969
+ * IndexedDB cache populated by live frames + {@link history}. Apps call this
3970
+ * on boot to paint instantly, then subscribe to {@link onMessage} for live
3971
+ * updates. Returns `[]` when offline support is off.
3972
+ */
3973
+ cachedMessages(): Promise<SpaceMessageEvent[]>;
3432
3974
  /** Subscribe to in-place edits of persisted messages (new content at the same handle). */
3433
3975
  onMessageEdited(handler: (e: SpaceMessageEvent) => void): () => void;
3434
3976
  /** Subscribe to deletions of persisted messages. */
@@ -3510,6 +4052,15 @@ declare class Space {
3510
4052
  * nothing to verify against → reject.
3511
4053
  */
3512
4054
  private verifySender;
4055
+ /** Persist a decoded live/history/edit frame to the offline cache. */
4056
+ private cacheFrame;
4057
+ /**
4058
+ * Inbound reconciliation run on reconnect: page the server's forward delta
4059
+ * (`?since=<lastSeenHandle>`) to the head, pulling exactly the messages that
4060
+ * landed while we were offline into the cache. Bounded so a long absence
4061
+ * can't page forever. Falls back gracefully if the endpoint is unavailable.
4062
+ */
4063
+ private catchUp;
3513
4064
  private emitEvent;
3514
4065
  private requireKeyring;
3515
4066
  private requireCipher;
@@ -3642,9 +4193,14 @@ interface SpaceNamespaceDeps {
3642
4193
  wsBaseUrl: string;
3643
4194
  /** Optional PersonalSpace-backed cache of group keys (encrypted at rest). */
3644
4195
  cache?: SpaceKeyCache;
4196
+ /** Offline manager — enables message caching + offline send replay. */
4197
+ offline?: OfflineManager;
3645
4198
  }
3646
4199
  declare class SpaceNamespace {
3647
4200
  private readonly deps;
4201
+ private spaceCache?;
4202
+ /** Spaces built this session, by id — used to route offline send replays. */
4203
+ private readonly openSpaces;
3648
4204
  constructor(deps: SpaceNamespaceDeps);
3649
4205
  /**
3650
4206
  * Create a new space: generate its keypair (public key = id), register
@@ -4089,6 +4645,22 @@ interface ClientOptions {
4089
4645
  * staging (`auth.staging.muhkoo.dev`) or a self-hosted deployment.
4090
4646
  */
4091
4647
  authBaseUrl?: string;
4648
+ /**
4649
+ * Offline support (`client.offline`) — transparent local caching + a durable
4650
+ * write queue + CRDT sync. **On by default in browsers** (IndexedDB + Cache
4651
+ * API present), a no-op everywhere else. Override to force it on/off or to
4652
+ * supply a custom {@link OfflineStore}.
4653
+ */
4654
+ offline?: {
4655
+ /** Force enable/disable. Default: auto-detect (browser on, Node off). */
4656
+ enabled?: boolean;
4657
+ /** Pluggable persistence. Default: IndexedDB in the browser. */
4658
+ store?: OfflineStore;
4659
+ /** Cache file-shard bytes in the Cache API. Default: true in the browser. */
4660
+ cacheShards?: boolean;
4661
+ /** Soft cap on durable-queue bytes before degrading to online-only. */
4662
+ maxQueueBytes?: number;
4663
+ };
4092
4664
  }
4093
4665
  declare class Client {
4094
4666
  readonly baseUrl: string;
@@ -4108,6 +4680,8 @@ declare class Client {
4108
4680
  readonly agents: AgentsNamespace;
4109
4681
  /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
4110
4682
  readonly functions: FunctionsNamespace;
4683
+ /** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
4684
+ readonly offline: OfflineManager;
4111
4685
  private readonly session;
4112
4686
  private readonly http;
4113
4687
  /** Subscribers notified when a session expires and can't be recovered. */
@@ -4268,6 +4842,322 @@ declare function ejectAgentTools(appClass: Function, opts?: {
4268
4842
  maxIterations?: number;
4269
4843
  }): AgentToolsConfig;
4270
4844
 
4845
+ /**
4846
+ * Message-log helpers — the conflict model for a Space's message history.
4847
+ *
4848
+ * Messages are the easy CRDT: the accelerator assigns every persisted message a
4849
+ * monotonic, zero-padded `handle` that already totally-orders the log, and the
4850
+ * sealed body is immutable. So the "log" is just a grow-only set keyed by
4851
+ * `handle`, and convergence is structural — no server change needed.
4852
+ *
4853
+ * The only subtleties this module encodes:
4854
+ * - **Optimistic sends** have no server handle yet. We key them by a
4855
+ * provisional handle that sorts *after* every real one, and tag them with
4856
+ * the sender's `clientId` so the real frame (carrying the same `clientId`)
4857
+ * can replace the placeholder instead of duplicating it.
4858
+ * - **Edits** are a per-message LWW register on the body (greater HLC wins).
4859
+ * - **Deletes** are a tombstone at the message's handle.
4860
+ */
4861
+ type MessageOp = "msg" | "edit" | "delete";
4862
+ interface MessageEntry {
4863
+ /** Server-assigned handle, or a {@link provisionalHandle} while pending. */
4864
+ handle: string;
4865
+ /** Sealed packet string (ciphertext at rest); `null` for a delete tombstone. */
4866
+ packet: string | null;
4867
+ op: MessageOp;
4868
+ /** Sender-generated id, present on locally-originated sends for dedupe. */
4869
+ clientId?: string;
4870
+ /** HLC stamp — orders edits to the same message. */
4871
+ hlc?: string;
4872
+ /** True while an optimistic send has not yet been acked by the server. */
4873
+ pending?: boolean;
4874
+ deleted?: boolean;
4875
+ }
4876
+ /** IndexedDB key for a message within a space. */
4877
+ declare function messageKey(spaceId: string, handle: string): string;
4878
+ /**
4879
+ * A handle for an un-acked optimistic message. Prefixed with `"~"` (0x7E),
4880
+ * which sorts after every digit, so pending sends always appear at the tail of
4881
+ * the handle-ordered log until the real handle arrives.
4882
+ */
4883
+ declare function provisionalHandle(clientId: string): string;
4884
+ declare function isProvisional(handle: string): boolean;
4885
+ /**
4886
+ * Merge two entries for the SAME handle. A delete always wins (tombstone); an
4887
+ * edit wins over the entry it supersedes when its HLC is greater; otherwise the
4888
+ * existing entry stands (a re-delivered `msg` is idempotent).
4889
+ */
4890
+ declare function mergeMessage(local: MessageEntry | null, incoming: MessageEntry): MessageEntry;
4891
+
4892
+ /**
4893
+ * `SpaceCache` — the offline adapter a {@link ../spaces/Space} talks to. It
4894
+ * persists sealed message frames (ciphertext at rest), tracks per-space sync
4895
+ * cursors, and records offline sends in the durable queue. Kept as its own
4896
+ * object (rather than wiring the Space straight to the store + clock) so the
4897
+ * Space stays decoupled from IndexedDB and so the no-op path is a single
4898
+ * `enabled` check.
4899
+ *
4900
+ * Messages are the structural CRDT (see {@link ./crdt/MessageLog}): the server
4901
+ * assigns a monotonic `handle` that totally-orders the log, so the cache just
4902
+ * stores frames keyed by a zero-padded handle (real) or a provisional key
4903
+ * (pending sends, which sort to the tail until their echo arrives).
4904
+ */
4905
+
4906
+ /** Zero-pad a numeric server handle so string keys sort in handle order. */
4907
+ declare function padHandle(handle: number): string;
4908
+ interface SpaceCursor {
4909
+ lastSeenHandle: number;
4910
+ oldestHandle: number;
4911
+ fullyBackfilled: boolean;
4912
+ }
4913
+ /**
4914
+ * The surface a {@link ../spaces/Space} uses. Declared structurally on the
4915
+ * Space side too (as `SpaceOfflineAdapter`) so the spaces module never imports
4916
+ * the offline module.
4917
+ */
4918
+ declare class SpaceCache {
4919
+ private readonly manager;
4920
+ private readonly store;
4921
+ constructor(manager: OfflineManager);
4922
+ get enabled(): boolean;
4923
+ newClientId(): string;
4924
+ nextHlc(): Promise<string>;
4925
+ /** Persist a sealed message frame keyed by its (real or provisional) handle. */
4926
+ putMessage(spaceId: string, entry: MessageEntry): Promise<void>;
4927
+ /** Tombstone the message at `handle`. */
4928
+ putDeleted(spaceId: string, handle: number): Promise<void>;
4929
+ /** Drop a pending (optimistic) entry once its real echo has been stored. */
4930
+ dropPending(spaceId: string, clientId: string): Promise<void>;
4931
+ /** All cached frames for a space, in handle order (pending ones last). */
4932
+ loadMessages(spaceId: string): Promise<MessageEntry[]>;
4933
+ getCursor(spaceId: string): Promise<SpaceCursor | null>;
4934
+ /** Advance the last-seen handle (monotonic) and track the oldest fetched. */
4935
+ observeHandle(spaceId: string, handle: number): Promise<void>;
4936
+ /** Queue a raw space frame for replay when the connection returns. */
4937
+ enqueueFrame(spaceId: string, frame: unknown, clientId: string, hlc: string): Promise<void>;
4938
+ /** Register an inbound catch-up task (forward/back history paging). */
4939
+ registerCatchUp(task: () => Promise<void>): () => void;
4940
+ }
4941
+
4942
+ /**
4943
+ * Errors specific to the offline layer.
4944
+ */
4945
+ /**
4946
+ * Thrown when an offline read/write needs the user's identity (to decrypt or
4947
+ * re-encrypt cached data) but the client is locked — typically right after a
4948
+ * reload, where only the session token was restored and `unlock()` hasn't run
4949
+ * yet. Distinct from the generic "not signed in" identity error so apps can
4950
+ * specifically prompt the user to unlock to see their offline data.
4951
+ */
4952
+ declare class OfflineLockedError extends Error {
4953
+ constructor(message?: string);
4954
+ }
4955
+
4956
+ /**
4957
+ * Environment probe deciding whether the offline layer turns itself on. Offline
4958
+ * support is **on by default in browsers** and a no-op everywhere else, so this
4959
+ * is the gate the {@link ../core/Client} consults to pick {@link
4960
+ * ./store/IndexedDbStore} vs {@link ./store/NoopStore}.
4961
+ *
4962
+ * We require all three web APIs the layer leans on — `indexedDB` (structured
4963
+ * cache + durable queue), the Cache API (file-shard bytes), and a `window`
4964
+ * (so we're in a real document with online/offline events) — and we *touch*
4965
+ * them inside a try/catch, because some sandboxed or private-mode contexts
4966
+ * expose the globals but throw on use (mirrors `defaultSessionStore`).
4967
+ */
4968
+ declare function isOfflineCapable(): boolean;
4969
+
4970
+ /**
4971
+ * `IndexedDbStore` — the browser {@link OfflineStore}, backed by one IndexedDB
4972
+ * database per origin (see {@link ./schema}). It opens lazily on first use and
4973
+ * memoizes the connection. The queue methods sit on the autoincrement
4974
+ * `outbound-queue` store and return mutations in HLC order for the sync engine.
4975
+ *
4976
+ * Construct only where `indexedDB` exists — {@link ../detect.isOfflineCapable}
4977
+ * gates this, and the {@link ../../core/Client} falls back to {@link ./NoopStore}
4978
+ * everywhere else.
4979
+ */
4980
+
4981
+ declare class IndexedDbStore implements OfflineStore {
4982
+ readonly enabled = true;
4983
+ private conn;
4984
+ private opening;
4985
+ private connect;
4986
+ ready(): Promise<void>;
4987
+ get<T>(store: StoreName, key: string): Promise<T | null>;
4988
+ put<T>(store: StoreName, key: string, value: T): Promise<void>;
4989
+ delete(store: StoreName, key: string): Promise<void>;
4990
+ prefix<T>(store: StoreName, prefix: string): Promise<Array<OfflineEntry<T>>>;
4991
+ range<T>(store: StoreName, lower: string, upper: string): Promise<Array<OfflineEntry<T>>>;
4992
+ all<T>(store: StoreName): Promise<Array<OfflineEntry<T>>>;
4993
+ clear(store: StoreName): Promise<void>;
4994
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
4995
+ dequeue(seq: number): Promise<void>;
4996
+ queued(): Promise<QueueEntry[]>;
4997
+ }
4998
+
4999
+ /**
5000
+ * `NoopStore` — the {@link OfflineStore} used wherever offline support is off
5001
+ * (Node, Workers, SSR, or when a consumer disables it). Every read returns
5002
+ * empty and every write is dropped, so the namespace hooks that call into it
5003
+ * behave exactly as they did before the offline layer existed. `enabled` is
5004
+ * `false` so hot paths can skip the call entirely.
5005
+ */
5006
+
5007
+ declare class NoopStore implements OfflineStore {
5008
+ readonly enabled = false;
5009
+ ready(): Promise<void>;
5010
+ get<T>(_store: StoreName, _key: string): Promise<T | null>;
5011
+ put<T>(_store: StoreName, _key: string, _value: T): Promise<void>;
5012
+ delete(_store: StoreName, _key: string): Promise<void>;
5013
+ prefix<T>(_store: StoreName, _prefix: string): Promise<Array<OfflineEntry<T>>>;
5014
+ range<T>(_store: StoreName, _lower: string, _upper: string): Promise<Array<OfflineEntry<T>>>;
5015
+ all<T>(_store: StoreName): Promise<Array<OfflineEntry<T>>>;
5016
+ clear(_store: StoreName): Promise<void>;
5017
+ enqueue(_entry: Omit<QueueEntry, "seq">): Promise<number>;
5018
+ dequeue(_seq: number): Promise<void>;
5019
+ queued(): Promise<QueueEntry[]>;
5020
+ }
5021
+
5022
+ /** The smallest possible stamp — used as the "nothing stored yet" sentinel so a
5023
+ * first real write always wins (`compareHlc(real, ZERO_HLC) > 0`). */
5024
+ declare const ZERO_HLC: string;
5025
+ interface HlcParts {
5026
+ /** Physical wall-clock time, milliseconds since the Unix epoch. */
5027
+ wall: number;
5028
+ /** Logical counter within `wall`. */
5029
+ counter: number;
5030
+ /** Stable per-install node id (tiebreak). */
5031
+ nodeId: string;
5032
+ }
5033
+ /** Pack the three components into the sortable string form. */
5034
+ declare function pack(wall: number, counter: number, nodeId: string): string;
5035
+ /** Parse a packed stamp back into its components. Tolerates the empty/zero
5036
+ * sentinel. Throws on a malformed string so corruption surfaces loudly. */
5037
+ declare function unpack(stamp: string): HlcParts;
5038
+ /**
5039
+ * Total order over packed HLC strings: negative if `a < b`, positive if
5040
+ * `a > b`, `0` only when they are byte-identical. Because the fields are
5041
+ * fixed-width and ordered (wall, counter, nodeId), a single string comparison
5042
+ * already yields causal order — this helper just normalizes the result to the
5043
+ * `-1 | 0 | 1` shape callers expect.
5044
+ */
5045
+ declare function compareHlc(a: string, b: string): number;
5046
+ /** Convenience: is `a` strictly newer than `b`? */
5047
+ declare function isNewer(a: string, b: string): boolean;
5048
+
5049
+ /**
5050
+ * `HLC` — a Hybrid Logical Clock. Hands out monotonically increasing
5051
+ * {@link HlcTimestamp} stamps that (a) track real time closely enough to be
5052
+ * human-meaningful, yet (b) never go backwards, even across a reload or when a
5053
+ * peer's clock is ahead of ours.
5054
+ *
5055
+ * Two operations, both the textbook HLC algorithm (Kulkarni et al.):
5056
+ * - `now()` — stamp a local event.
5057
+ * - `update(remote)` — observe a remote stamp (e.g. arriving on the realtime
5058
+ * change feed) and advance our clock past it, so any event we generate
5059
+ * afterwards is causally `> remote`.
5060
+ *
5061
+ * State is just `{ wall, counter }`; `nodeId` is fixed for the install. The
5062
+ * owner ({@link ../OfflineManager}) seeds the clock from persisted state on
5063
+ * boot and persists `getState()` after each tick so monotonicity survives page
5064
+ * reloads — without that, a reload could mint a stamp that loses to one written
5065
+ * seconds earlier.
5066
+ */
5067
+
5068
+ interface HlcState {
5069
+ wall: number;
5070
+ counter: number;
5071
+ }
5072
+ /** Injected physical-time source — defaults to `Date.now`. Overridable so
5073
+ * tests can drive the clock deterministically. */
5074
+ type PhysicalClock = () => number;
5075
+ declare class HLC {
5076
+ private wall;
5077
+ private counter;
5078
+ private readonly nodeId;
5079
+ private readonly physical;
5080
+ constructor(nodeId: string, opts?: {
5081
+ state?: HlcState | null;
5082
+ physical?: PhysicalClock;
5083
+ });
5084
+ /** Stamp a locally-generated event. Advances the clock. */
5085
+ now(): string;
5086
+ /**
5087
+ * Merge in an observed remote stamp and advance our clock strictly past it.
5088
+ * Call this for every inbound HLC (change-feed frames, history rows) so the
5089
+ * next `now()` is causally after anything we've seen.
5090
+ */
5091
+ update(remote: HlcParts): string;
5092
+ /** The current stamp without advancing the clock. */
5093
+ current(): string;
5094
+ /** Serializable `{ wall, counter }` for persistence. */
5095
+ getState(): HlcState;
5096
+ }
5097
+
5098
+ /**
5099
+ * `LWWMap` — a map of independent last-write-wins registers, one per column.
5100
+ * This is the conflict model for a `db` row: two devices that edit *different*
5101
+ * columns of the same row while offline both keep their edits (no lost update),
5102
+ * and two that edit the *same* column converge to the greater-HLC write. A row
5103
+ * delete is a causal tombstone (see {@link ./ORSet}); a later write to any
5104
+ * column resurrects the row with just the surviving columns.
5105
+ *
5106
+ * This mirrors, on the client, exactly what the accelerator's `_muhkoo_crdt`
5107
+ * sidecar does server-side — so a row materializes identically whether it was
5108
+ * merged locally (offline) or by the server on replay.
5109
+ */
5110
+ interface LwwMap {
5111
+ /** Per-column value of the winning write. */
5112
+ fields: Record<string, unknown>;
5113
+ /** Per-column HLC of the winning write. */
5114
+ columnHlc: Record<string, string>;
5115
+ /** Row-delete tombstone HLC, or `null` when the row was never deleted. */
5116
+ tombstone: string | null;
5117
+ }
5118
+ declare function emptyMap(): LwwMap;
5119
+ /** Apply a local column write (insert/update) at the given per-column HLCs. */
5120
+ declare function writeColumns(map: LwwMap, values: Record<string, unknown>, hlc: string): LwwMap;
5121
+ /** Apply a row delete at `hlc` (causal tombstone). */
5122
+ declare function tombstoneRow(map: LwwMap, hlc: string): LwwMap;
5123
+ /** Merge two row states column-by-column. Pure; neither input is mutated. */
5124
+ declare function mergeMap(a: LwwMap, b: LwwMap): LwwMap;
5125
+ /**
5126
+ * Materialize the visible row from merged state, or `null` when the row is
5127
+ * deleted. A column is visible only when its write is causally newer than the
5128
+ * tombstone — so a delete hides every prior column, but a post-delete write to
5129
+ * a column brings the row back with just that column populated.
5130
+ */
5131
+ declare function materializeRow(map: LwwMap): Record<string, unknown> | null;
5132
+
5133
+ /**
5134
+ * Tombstone helpers shared by the LWW CRDTs. We don't need a full
5135
+ * observed-remove set — every domain that deletes (kv keys, db rows) does so
5136
+ * by stamping a delete with an HLC and keeping that stamp around. The only
5137
+ * rule that matters is the causal one:
5138
+ *
5139
+ * a delete at HLC `d` hides the value, but a *later* write at HLC `w > d`
5140
+ * resurrects it; a *stale* write at `w <= d` is dropped.
5141
+ *
5142
+ * Keeping the delete HLC (rather than physically forgetting the key) is what
5143
+ * prevents a delayed offline write from "un-deleting" something a peer already
5144
+ * removed. Callers GC tombstones older than any possible offline window.
5145
+ */
5146
+ /**
5147
+ * Does a write at `writeHlc` win against a tombstone at `tombstoneHlc`?
5148
+ * `true` ⇒ the write applies (resurrecting a deleted item if needed); `false`
5149
+ * ⇒ the tombstone still holds and the write is dropped. A missing tombstone
5150
+ * (`null`) means there's nothing suppressing the write.
5151
+ */
5152
+ declare function writeBeatsTombstone(writeHlc: string, tombstoneHlc: string | null): boolean;
5153
+ /**
5154
+ * Does a delete at `deleteHlc` win against the latest write at `writeHlc`?
5155
+ * `true` ⇒ the item is now tombstoned. Ties go to the delete (remove-wins) so
5156
+ * that a delete and a write stamped in the same instant resolve deterministically
5157
+ * toward removal.
5158
+ */
5159
+ declare function deleteBeatsWrite(deleteHlc: string, writeHlc: string | null): boolean;
5160
+
4271
5161
  /**
4272
5162
  * KeyringClient — HTTP implementation of {@link KeyringTransport} against the
4273
5163
  * accelerator's per-space keyring endpoints. All blobs are opaque to the
@@ -4328,5 +5218,5 @@ declare class KeyringClient implements KeyringTransport {
4328
5218
  fetchMetadata(): Promise<SpaceMetadata | null>;
4329
5219
  }
4330
5220
 
4331
- export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HashKnowledge, HttpClient, HttpError, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardClient, SharedSpaceClient, Space, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, VaultUnavailableError, WSTransport, ZkAuth, buildCommitment, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, generatePkce, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, manifestToStat, poseidonHash, quickVerify, randomBytes, randomState, sealSeed, shardHash, signMessage, toField, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase };
4332
- export type { ACL, ACLEntry, AgentConfig, AgentCreateInput, AgentDbToolMode, AgentProvisioned, AgentScopeOpts, AgentSkill, AgentToolsConfig, AgentTrigger, AgentTriggerType, AgentUpdateInput, AgentsNamespaceDeps, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitUrls, ClientOptions, DehydratedKeypair, DehydratedKeys, DoubleRatchetCipherDeps, ECDHCurve, EncodeResult, EphemeralEvent, EpochKeyProvider, FileManifest, FileNamespaceDeps, FileStat, FileStorageOptions, FunctionCaps, FunctionConfig, FunctionDeployInput, FunctionInvokeOptions, FunctionScopeOpts, FunctionTrigger, FunctionTriggerType, FunctionTriggers, FunctionUpdateInput, FunctionsNamespaceDeps, Groth16Proof, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, KeyringClientDeps, KeyringTransport, KvNamespaceDeps, LoginOptions, MessageBody, MessageDeletedEvent, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOptions, MessageSubscription, MuhkooAgentMeta, MuhkooAgentToolsConfig, MuhkooAppDescriptor, MuhkooDBAccess, MuhkooDBMeta, MuhkooFunctionMeta, MuhkooMessageEvent, MuhkooSpaceMeta, OAuthProvider, PacketCipher, PacketHeaders, PacketOptions, Participant, Permission, PermissionCheckRequest, PermissionCheckResult, PersonalSpaceClientOptions, Pkce, PublicKeyJWK, RatchetState, ReceiveResult, RegisterParams, Role, SpaceDeps as RoomDeps, SpaceFileMetadata as RoomFileMetadata, SealedHandoff, SealedHeaders, Session, SessionOptions, SessionStore, SetOptions, ShardClientOptions, SharedSpaceClientOptions, SnarkProof, SpaceChannelLike, SpaceDeps, SpaceFileMetadata, SpaceKeyCache, SpaceKeyringDeps, SpaceMessageEvent, SpaceNamespaceDeps, StorageChangeEvent, StoredSession, Subscription, UserIdentity, VaultFactor, VerificationKey, WSMessage, WSMessageType, WSTransportOptions, WrappedPayload, WriteFileInput, WriteFileOptions, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };
5221
+ export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DbCache, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, SyncEngine, VaultUnavailableError, WSTransport, ZERO_HLC, ZkAuth, buildCommitment, compareHlc, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, generatePkce, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isProvisional, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, poseidonHash, provisionalHandle, quickVerify, randomBytes, randomState, sealSeed, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
5222
+ export type { ACL, ACLEntry, AgentConfig, AgentCreateInput, AgentDbToolMode, AgentProvisioned, AgentScopeOpts, AgentSkill, AgentToolsConfig, AgentTrigger, AgentTriggerType, AgentUpdateInput, AgentsNamespaceDeps, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, CachedSpaceMessage, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitUrls, ClientOptions, ConnectivityOptions, ConnectivityState, DbQueryPage, DehydratedKeypair, DehydratedKeys, DoubleRatchetCipherDeps, ECDHCurve, EncodeResult, EphemeralEvent, EpochKeyProvider, FileManifest, FileNamespaceDeps, FileStat, FileStorageOptions, FunctionCaps, FunctionConfig, FunctionDeployInput, FunctionInvokeOptions, FunctionScopeOpts, FunctionTrigger, FunctionTriggerType, FunctionTriggers, FunctionUpdateInput, FunctionsNamespaceDeps, Groth16Proof, HlcParts, HlcState, HttpClientOptions, IncomingFrame, KeyExchangeFrame, KeyPair, Keypair, KeyringClientDeps, KeyringTransport, KvEntry, KvNamespaceDeps, LoginOptions, LwwMap, LwwRegister, MessageBody, MessageDeletedEvent, MessageEntry, MessageEvent, MessageHeaders, MessageNamespaceDeps, MessageOp, MessageOptions, MessageSubscription, MuhkooAgentMeta, MuhkooAgentToolsConfig, MuhkooAppDescriptor, MuhkooDBAccess, MuhkooDBMeta, MuhkooFunctionMeta, MuhkooMessageEvent, MuhkooSpaceMeta, OAuthProvider, OfflineEntry, OfflineManagerDeps, OfflineStore, PacketCipher, PacketHeaders, PacketOptions, Participant, Permission, PermissionCheckRequest, PermissionCheckResult, PersonalSpaceClientOptions, Pkce, PublicKeyJWK, QueueEntry, RatchetState, ReceiveResult, RegisterParams, Replayer, Role, SpaceDeps as RoomDeps, SpaceFileMetadata as RoomFileMetadata, SealedHandoff, SealedHeaders, Session, SessionOptions, SessionStore, SetOptions, ShardByteCache, ShardClientOptions, SharedSpaceClientOptions, SnapshotNamespaceDeps, SnarkProof, SpaceChannelLike, SpaceCursor, SpaceDeps, SpaceFileMetadata, SpaceKeyCache, SpaceKeyringDeps, SpaceMessageEvent, SpaceNamespaceDeps, SpaceOfflineAdapter, StorageChangeEvent, StoreName, StoredSession, Subscription, UserIdentity, VaultFactor, VerificationKey, WSMessage, WSMessageType, WSTransportOptions, WrappedPayload, WriteFileInput, WriteFileOptions, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };