@muhkoo/connect 0.7.0-alpha.5 → 0.9.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,28 @@ 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
+ }
1418
+ /**
1419
+ * A peer block source (the p2p layer's `PeerExchange` satisfies this
1420
+ * structurally). Consulted between the local cache and origin: a block fetched
1421
+ * from a peer is verified by hash before use. Declared here so the storage layer
1422
+ * doesn't import the p2p module (dependency arrow stays p2p → storage).
1423
+ */
1424
+ interface PeerBlockSource {
1425
+ getBlock(hash: string, opts?: {
1426
+ timeoutMs?: number;
1427
+ }): Promise<Uint8Array | null>;
1428
+ announce(hash: string): void;
1429
+ }
1402
1430
  interface ShardClientOptions {
1403
1431
  /** Base URL of the accelerator (e.g. `https://muhkoo-accelerator.workers.dev`). */
1404
1432
  baseUrl: string;
@@ -1418,6 +1446,24 @@ interface ShardClientOptions {
1418
1446
  * Provided so callers can inject retries / auth / instrumentation.
1419
1447
  */
1420
1448
  fetch?: typeof fetch;
1449
+ /**
1450
+ * Optional offline byte cache. When present, GETs are served cache-first and
1451
+ * populated on success, and PUTs write the cache before the network so a
1452
+ * just-uploaded file reads back offline. See {@link ShardByteCache}.
1453
+ */
1454
+ cache?: ShardByteCache;
1455
+ /**
1456
+ * Called when a PUT can't reach the network (offline). Lets the caller queue
1457
+ * the upload for replay on reconnect; the bytes are already in {@link cache}
1458
+ * keyed by `hash`, so only the hash needs recording.
1459
+ */
1460
+ deferUpload?: (hash: string) => Promise<void>;
1461
+ /**
1462
+ * Optional peer block source. When present, GETs try peers between the local
1463
+ * cache and origin (peer blocks are verified by hash); PUTs announce the new
1464
+ * block to peers. Best-effort — any failure falls through to origin.
1465
+ */
1466
+ peers?: PeerBlockSource;
1421
1467
  }
1422
1468
  /**
1423
1469
  * Compute the lowercase hex SHA-256 of `bytes`. Available via WebCrypto in
@@ -1428,6 +1474,9 @@ declare class ShardClient {
1428
1474
  private readonly baseUrl;
1429
1475
  private readonly pathPrefix;
1430
1476
  private readonly fetchFn;
1477
+ private readonly cache?;
1478
+ private readonly deferUpload?;
1479
+ private readonly peers?;
1431
1480
  constructor(opts: ShardClientOptions);
1432
1481
  /** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
1433
1482
  urlForHash(hash: string): string;
@@ -2717,6 +2766,410 @@ declare class HttpClient {
2717
2766
  private request;
2718
2767
  }
2719
2768
 
2769
+ /**
2770
+ * `LWWRegister` — a last-write-wins register CRDT, the conflict model for a
2771
+ * `kv` value and for an app-state snapshot. A register holds a single value
2772
+ * stamped with the {@link HlcTimestamp} of the write that produced it. Two
2773
+ * concurrent writes converge to the one with the greater HLC; the loser is
2774
+ * discarded. A delete is just a write that sets `deleted` (a tombstone), so a
2775
+ * stale write that arrives after a newer delete can't resurrect the value.
2776
+ *
2777
+ * Because the HLC total-orders every write deterministically (down to a nodeId
2778
+ * tiebreak), every replica that has seen the same set of writes lands on the
2779
+ * same value — that's the convergence guarantee.
2780
+ */
2781
+ interface LwwRegister<T> {
2782
+ /** The current value, or `null` when tombstoned / never written. */
2783
+ value: T | null;
2784
+ /** HLC of the write that produced this state. */
2785
+ hlc: string;
2786
+ /** True when the latest write was a delete. */
2787
+ deleted: boolean;
2788
+ }
2789
+ /** An empty register that loses to any real write. */
2790
+ declare function emptyRegister<T>(): LwwRegister<T>;
2791
+ /**
2792
+ * Merge two register states, returning the winner (greater HLC). Pure: neither
2793
+ * input is mutated. Used identically on the inbound change feed and on
2794
+ * sync-replay reconciliation so there's exactly one definition of "who wins".
2795
+ */
2796
+ declare function mergeRegister<T>(a: LwwRegister<T>, b: LwwRegister<T>): LwwRegister<T>;
2797
+ /** Build the register produced by a `set`. */
2798
+ declare function writeRegister<T>(value: T, hlc: string): LwwRegister<T>;
2799
+ /** Build the register produced by a `delete` (a tombstone at `hlc`). */
2800
+ declare function deleteRegister<T>(hlc: string): LwwRegister<T>;
2801
+
2802
+ /**
2803
+ * `ConnectivityManager` — the single source of truth for "are we online", which
2804
+ * the offline layer needs to decide between a network call and a cache read, and
2805
+ * to know when to flush the durable outbound queue.
2806
+ *
2807
+ * `navigator.onLine` alone lies often (captive portals, VPN flaps, a laptop
2808
+ * that's "connected" to a network with no route), so we fuse three signals:
2809
+ * 1. the browser `online`/`offline` events (coarse but free),
2810
+ * 2. real fetch outcomes reported by {@link ../core/HttpClient} and the shard
2811
+ * client — a thrown `TypeError`/abort is the most reliable "offline" tell,
2812
+ * and a 2xx is the most reliable "online" tell,
2813
+ * 3. the sync engine, which flips us to `syncing` while it drains the queue.
2814
+ *
2815
+ * The offline→online edge fires `onReconnect` (wired to the sync engine) so
2816
+ * queued writes replay automatically. Transitions are debounced to avoid
2817
+ * flapping the UI on a brief blip. State changes also emit on {@link EventCore}
2818
+ * (`ONLINE`/`OFFLINE`/`SYNCING`) so apps can render a status indicator.
2819
+ */
2820
+ type ConnectivityState = "online" | "offline" | "syncing";
2821
+ interface ConnectivityOptions {
2822
+ /** Called on each offline→online transition (drives queue replay). */
2823
+ onReconnect?: () => void;
2824
+ /** Debounce window for offline transitions, ms. Default 1500. */
2825
+ debounceMs?: number;
2826
+ }
2827
+ declare class ConnectivityManager {
2828
+ private state;
2829
+ private readonly listeners;
2830
+ private readonly onReconnect?;
2831
+ private readonly debounceMs;
2832
+ private offlineTimer;
2833
+ private started;
2834
+ private readonly handleOnline;
2835
+ private readonly handleOffline;
2836
+ constructor(opts?: ConnectivityOptions);
2837
+ /** Attach to the browser online/offline events. Safe to call in Node (no-op). */
2838
+ start(): void;
2839
+ stop(): void;
2840
+ get current(): ConnectivityState;
2841
+ get isOnline(): boolean;
2842
+ /** Subscribe to state changes. Returns an unsubscribe function. */
2843
+ onChange(cb: (state: ConnectivityState) => void): () => void;
2844
+ /** Report a failed network attempt (thrown fetch / abort). */
2845
+ reportFetchFailure(): void;
2846
+ /** Report a successful network round-trip — strongest "we're online" signal. */
2847
+ reportFetchSuccess(): void;
2848
+ /** The sync engine entering its drain phase. */
2849
+ markSyncing(): void;
2850
+ /** The sync engine finished — settle back to online. */
2851
+ markSynced(): void;
2852
+ private markOnline;
2853
+ private scheduleOffline;
2854
+ private set;
2855
+ }
2856
+
2857
+ type StoreName = "kv-cache" | "db-row-cache" | "db-query-cache" | "space-messages" | "space-cursors" | "outbound-queue" | "snapshots" | "hlc-state" | "meta";
2858
+
2859
+ /**
2860
+ * `OfflineStore` — the pluggable persistence interface the offline layer is
2861
+ * built on, mirroring the {@link ../../core/Session.SessionStore} pattern. The
2862
+ * default browser implementation is {@link ./IndexedDbStore}; environments
2863
+ * without IndexedDB (Node, Workers, SSR) get {@link ./NoopStore}, whose every
2864
+ * method resolves to empty so the namespace hooks become branch-free no-ops.
2865
+ *
2866
+ * Keys are plain strings (most are prefixed with the user's `commitment` to
2867
+ * isolate users on a shared origin). Values are whatever the caller stores —
2868
+ * the store is format-agnostic; CRDT/ciphertext shape is the namespace's
2869
+ * concern.
2870
+ */
2871
+
2872
+ /** A durable outbound mutation awaiting replay on reconnect. */
2873
+ interface QueueEntry {
2874
+ /** Auto-assigned monotonic sequence (insertion order). */
2875
+ seq: number;
2876
+ /** HLC stamp of the mutation — the order replays run in. */
2877
+ hlc: string;
2878
+ /** Client-generated id for idempotent replay / dedupe. */
2879
+ clientId: string;
2880
+ domain: "kv" | "db" | "file" | "space" | "snapshot";
2881
+ /** Namespace method to re-invoke (e.g. `"set"`, `"update"`, `"sendMessage"`). */
2882
+ method: string;
2883
+ /** Serializable arguments captured at enqueue time. */
2884
+ args: unknown;
2885
+ }
2886
+ interface OfflineEntry<T> {
2887
+ key: string;
2888
+ value: T;
2889
+ }
2890
+ interface OfflineStore {
2891
+ /** False for {@link ./NoopStore} so hooks can skip all work cheaply. */
2892
+ readonly enabled: boolean;
2893
+ /** Resolve once the backing store is open and ready (no-op when disabled). */
2894
+ ready(): Promise<void>;
2895
+ get<T>(store: StoreName, key: string): Promise<T | null>;
2896
+ put<T>(store: StoreName, key: string, value: T): Promise<void>;
2897
+ delete(store: StoreName, key: string): Promise<void>;
2898
+ /** Entries whose key starts with `prefix`, in ascending key order. */
2899
+ prefix<T>(store: StoreName, prefix: string): Promise<Array<OfflineEntry<T>>>;
2900
+ /** Entries whose key is in `[lower, upper)`, in ascending key order. */
2901
+ range<T>(store: StoreName, lower: string, upper: string): Promise<Array<OfflineEntry<T>>>;
2902
+ /** Every entry in a store, in ascending key order. */
2903
+ all<T>(store: StoreName): Promise<Array<OfflineEntry<T>>>;
2904
+ clear(store: StoreName): Promise<void>;
2905
+ /** Append a mutation to the durable outbound queue; resolves with its seq. */
2906
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
2907
+ /** Remove a drained queue entry by seq. */
2908
+ dequeue(seq: number): Promise<void>;
2909
+ /** All queued mutations, HLC-ordered (the order to replay them in). */
2910
+ queued(): Promise<QueueEntry[]>;
2911
+ }
2912
+
2913
+ /**
2914
+ * `OutboundQueue` — the durable record of writes made while offline (or that
2915
+ * failed mid-flight) plus the machinery to replay them on reconnect. This is
2916
+ * what makes offline writes *survive a reload*: unlike the in-memory buffer in
2917
+ * {@link ../transport/WSTransport}, every entry lives in IndexedDB until it has
2918
+ * been acknowledged by the server.
2919
+ *
2920
+ * Each domain registers a **replayer** — a function that re-applies one queued
2921
+ * mutation by re-invoking the real namespace method (with an `isReplay` flag so
2922
+ * it doesn't re-enqueue). `drain()` replays entries in HLC order:
2923
+ * - success → remove the entry;
2924
+ * - permanent failure (4xx) → drop it and surface a `SYNC_ERROR` (a write the
2925
+ * server will never accept must not wedge the queue forever);
2926
+ * - transient/network failure → stop draining and leave the rest for the next
2927
+ * reconnect.
2928
+ *
2929
+ * Replays are idempotent by construction (kv/db are LWW-by-key, messages dedupe
2930
+ * by `clientId`, file PUTs are content-addressed), so re-running a partially
2931
+ * drained queue is safe.
2932
+ */
2933
+
2934
+ type Replayer = (entry: QueueEntry) => Promise<void>;
2935
+ declare class OutboundQueue {
2936
+ private readonly store;
2937
+ private readonly replayers;
2938
+ private draining;
2939
+ constructor(store: OfflineStore);
2940
+ /** A domain registers how to replay its mutations. */
2941
+ register(domain: QueueEntry["domain"], replayer: Replayer): void;
2942
+ /** Record a mutation for later replay; resolves with its assigned seq. */
2943
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
2944
+ /** Number of writes still awaiting sync. */
2945
+ pending(): Promise<number>;
2946
+ /**
2947
+ * Replay queued mutations in HLC order. Coalesces concurrent calls into a
2948
+ * single in-flight drain. Stops on the first transient failure, leaving the
2949
+ * remaining entries for the next attempt.
2950
+ */
2951
+ drain(): Promise<void>;
2952
+ private runDrain;
2953
+ }
2954
+
2955
+ /**
2956
+ * `SyncEngine` — orchestrates catch-up when the client comes back online. It's
2957
+ * the thing wired to the {@link ConnectivityManager}'s offline→online edge.
2958
+ *
2959
+ * One `run()` does, in order:
2960
+ * 1. **Guard** — only proceed when the session is authenticated and unlocked
2961
+ * (replays may need to re-seal/re-encrypt, and there's no point pushing
2962
+ * writes for a logged-out user).
2963
+ * 2. **Drain** the durable {@link OutboundQueue} — push every offline write.
2964
+ * 3. **Catch-up tasks** — domain-registered pulls that reconcile *inbound*
2965
+ * state we missed while offline (e.g. a Space paging history forward to its
2966
+ * last-seen handle). Registered by the namespaces.
2967
+ *
2968
+ * Concurrent triggers coalesce into a single in-flight run (a reconnect often
2969
+ * fires several signals at once), mirroring `Client.recoverInFlight`.
2970
+ */
2971
+
2972
+ interface SyncEngineDeps {
2973
+ queue: OutboundQueue;
2974
+ connectivity: ConnectivityManager;
2975
+ /** Proceed only when this returns true (authenticated + unlocked). */
2976
+ canSync: () => boolean;
2977
+ }
2978
+ declare class SyncEngine {
2979
+ private readonly deps;
2980
+ private readonly catchUpTasks;
2981
+ private running;
2982
+ constructor(deps: SyncEngineDeps);
2983
+ /** Register an inbound reconciliation task run after the queue drains. */
2984
+ registerCatchUp(task: () => Promise<void>): () => void;
2985
+ /** Drain + catch up. Concurrent calls share one in-flight run. */
2986
+ run(): Promise<void>;
2987
+ private execute;
2988
+ }
2989
+
2990
+ /**
2991
+ * `client.offline.snapshot` — a small, encrypted, app-state cache. Apps stash
2992
+ * whatever they need to repaint instantly on a cold offline boot: the last open
2993
+ * Space, a draft message, scroll position, the current route. It's deliberately
2994
+ * separate from `client.kv`: snapshots are **local-only by default** (instant,
2995
+ * never metered, never leave the device) and hold view state rather than domain
2996
+ * data.
2997
+ *
2998
+ * await client.offline.snapshot.save('ui', { route, openSpaceId, draft })
2999
+ * const ui = await client.offline.snapshot.load<UiState>('ui')
3000
+ *
3001
+ * Values are encrypted at rest with the same identity-derived {@link StorageCipher}
3002
+ * as `client.kv`, so a snapshot is unreadable until the user unlocks — `load`
3003
+ * returns `null` when locked or absent, and `save` throws {@link OfflineLockedError}.
3004
+ * Conflict model is a single-writer LWW register (the latest save wins).
3005
+ */
3006
+
3007
+ interface SnapshotNamespaceDeps {
3008
+ store: OfflineStore;
3009
+ session: SessionState;
3010
+ /** Stamp the saved record (LWW ordering); supplied by the OfflineManager. */
3011
+ nextHlc: () => Promise<string>;
3012
+ }
3013
+ declare class SnapshotNamespace {
3014
+ private readonly deps;
3015
+ private cipher;
3016
+ private cipherIdentity;
3017
+ constructor(deps: SnapshotNamespaceDeps);
3018
+ /** Persist `state` under `name` (encrypted). No-op when offline support is off. */
3019
+ save<T = unknown>(name: string, state: T): Promise<void>;
3020
+ /** Load and decrypt the snapshot `name`, or `null` if absent/locked. */
3021
+ load<T = unknown>(name: string): Promise<T | null>;
3022
+ /** Remove the snapshot `name`. */
3023
+ delete(name: string): Promise<void>;
3024
+ /** Names of all snapshots for the current user. */
3025
+ list(): Promise<string[]>;
3026
+ private key;
3027
+ private commitment;
3028
+ private requireCipher;
3029
+ }
3030
+
3031
+ /**
3032
+ * `ShardCache` — a Cache-API-backed store for file-shard bytes. Shards are
3033
+ * content-addressed (the key IS the SHA-256 of the ciphertext), so caching them
3034
+ * is uniquely safe: a cache hit is self-validating, immutable, and shareable
3035
+ * across files. The Cache API (rather than IndexedDB) is the right home for
3036
+ * large binary blobs — it streams, it's purpose-built for response bodies, and
3037
+ * it has its own eviction.
3038
+ *
3039
+ * Keys are SYNTHETIC absolute URLs (`https://shard.muhkoo.local/<hash>`) rather
3040
+ * than the real shard endpoint, so the cache is addressable by hash alone — the
3041
+ * sync engine can re-PUT a deferred shard knowing only its hash, without
3042
+ * reconstructing the accelerator URL. We never fetch these synthetic URLs; they
3043
+ * exist only as cache keys.
3044
+ *
3045
+ * Every method degrades to a no-op / null where the Cache API is unavailable
3046
+ * (Node, Workers, insecure contexts), so callers don't need to branch.
3047
+ */
3048
+ declare class ShardCache {
3049
+ static available(): boolean;
3050
+ private key;
3051
+ private open;
3052
+ /** Cached shard bytes, or `null` on miss / when unavailable. */
3053
+ get(hash: string): Promise<Uint8Array | null>;
3054
+ /** Store shard bytes under their hash. Best-effort. */
3055
+ put(hash: string, bytes: Uint8Array): Promise<void>;
3056
+ /** Whether a shard is present in the cache. */
3057
+ has(hash: string): Promise<boolean>;
3058
+ }
3059
+
3060
+ /**
3061
+ * `OfflineManager` — the object `client.offline` points at, and the single
3062
+ * owner of everything the offline layer needs at runtime: the {@link
3063
+ * OfflineStore}, the {@link HLC} (Hybrid Logical Clock), the {@link
3064
+ * ConnectivityManager}, the durable {@link OutboundQueue}, the {@link
3065
+ * SyncEngine}, and the {@link SnapshotNamespace}.
3066
+ *
3067
+ * The {@link ../core/Client} constructs one of these (always — it's a cheap
3068
+ * no-op when offline support is off) and hands the namespaces the small set of
3069
+ * primitives they need to participate: `nextHlc()` to stamp a write,
3070
+ * `observeHlc()` to fold in a remote stamp, `enqueue()` to record an offline
3071
+ * mutation, and `registerReplayer()`/`registerCatchUp()` to define how their
3072
+ * domain re-syncs on reconnect. Keeping that wiring here means the namespaces
3073
+ * stay decoupled from IndexedDB and the clock.
3074
+ */
3075
+
3076
+ interface OfflineManagerDeps {
3077
+ store: OfflineStore;
3078
+ session: SessionState;
3079
+ /** Whether offline support is active (browser) vs a no-op (Node/Workers). */
3080
+ enabled: boolean;
3081
+ }
3082
+ declare class OfflineManager {
3083
+ /** True in browsers with IndexedDB; false when running over a NoopStore. */
3084
+ readonly enabled: boolean;
3085
+ readonly store: OfflineStore;
3086
+ readonly connectivity: ConnectivityManager;
3087
+ readonly queue: OutboundQueue;
3088
+ readonly sync: SyncEngine;
3089
+ /** `client.offline.snapshot` — encrypted app-state cache. */
3090
+ readonly snapshot: SnapshotNamespace;
3091
+ /** Cache-API store for file-shard bytes (browser only). */
3092
+ readonly fileCache?: ShardCache;
3093
+ private hlc;
3094
+ private nodeId;
3095
+ private readonly bootPromise;
3096
+ private persistTimer;
3097
+ constructor(deps: OfflineManagerDeps);
3098
+ /**
3099
+ * Queue a shard upload that couldn't reach the network. The bytes are
3100
+ * already cached (keyed by `hash`), so only the hash is recorded; the file
3101
+ * replayer re-PUTs from the cache on reconnect.
3102
+ */
3103
+ deferShardUpload(hash: string): Promise<void>;
3104
+ /** Resolve once the clock + node id are loaded. */
3105
+ ready(): Promise<void>;
3106
+ /** Current connectivity state (`online` | `offline` | `syncing`). */
3107
+ get status(): ConnectivityState;
3108
+ /** Subscribe to connectivity changes. Returns an unsubscribe function. */
3109
+ onStatusChange(cb: (state: ConnectivityState) => void): () => void;
3110
+ /** Stamp a locally-generated write with a fresh, monotonic HLC. */
3111
+ nextHlc(): Promise<string>;
3112
+ /** Fold an observed remote HLC into the clock (call for inbound stamps). */
3113
+ observeHlc(remote: string): Promise<void>;
3114
+ /** A fresh client-generated id (idempotent replay / message dedupe). */
3115
+ newClientId(): string;
3116
+ /** Record an offline mutation for replay on reconnect. */
3117
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
3118
+ /** A domain registers how to replay its queued mutations. */
3119
+ registerReplayer(domain: QueueEntry["domain"], replayer: Replayer): void;
3120
+ /** A domain registers an inbound catch-up task (run after the queue drains). */
3121
+ registerCatchUp(task: () => Promise<void>): () => void;
3122
+ /** Surface a network failure to connectivity tracking. */
3123
+ reportFetchFailure(): void;
3124
+ /** Surface a successful round-trip to connectivity tracking. */
3125
+ reportFetchSuccess(): void;
3126
+ /** Tear down listeners (tests / hot-reload). */
3127
+ dispose(): void;
3128
+ private boot;
3129
+ /** Coalesce HLC-state writes — a burst of stamps persists once. */
3130
+ private schedulePersist;
3131
+ }
3132
+
3133
+ /**
3134
+ * `KvCache` — the offline adapter for `client.kv`. Stores each value's
3135
+ * ciphertext envelope (never plaintext) under `${commitment}|${collection}/${id}`
3136
+ * as an LWW-Register stamped with an HLC, so reads work offline and concurrent
3137
+ * writes converge (see {@link ./crdt/LWWRegister}). Deletes are causal
3138
+ * tombstones — a stale write can't resurrect a key a newer delete removed.
3139
+ *
3140
+ * True cross-device convergence also needs the accelerator to honor the HLC
3141
+ * (see the server's `kvmeta` sidecar); this adapter embeds the stamp now and
3142
+ * degrades gracefully (last-writer-by-arrival) until that ships.
3143
+ */
3144
+
3145
+ type KvEntry = LwwRegister<unknown>;
3146
+ declare class KvCache {
3147
+ private readonly manager;
3148
+ private readonly store;
3149
+ constructor(manager: OfflineManager);
3150
+ get enabled(): boolean;
3151
+ nextHlc(): Promise<string>;
3152
+ newClientId(): string;
3153
+ private key;
3154
+ /** Read the cached register for `collection/id`, or null when absent. */
3155
+ read(commitment: string, fullKey: string): Promise<KvEntry | null>;
3156
+ /** Optimistically record a local `set` (ciphertext envelope + HLC). */
3157
+ writeLocal(commitment: string, fullKey: string, envelope: unknown, hlc: string): Promise<void>;
3158
+ /** Optimistically record a local `delete` (tombstone at HLC). */
3159
+ deleteLocal(commitment: string, fullKey: string, hlc: string): Promise<void>;
3160
+ /**
3161
+ * Merge an authoritative value (from a GET refresh or a realtime frame) into
3162
+ * the cache by HLC. When the server doesn't supply an HLC yet, treat it as
3163
+ * "now-ish" by stamping our clock so a server value still beats stale local
3164
+ * state but a fresh local write wins.
3165
+ */
3166
+ mergeRemote(commitment: string, fullKey: string, envelope: unknown | null, op: "set" | "delete", hlc?: string): Promise<void>;
3167
+ /** Non-tombstoned full keys for a commitment (used for offline `list`). */
3168
+ liveKeys(commitment: string): Promise<string[]>;
3169
+ /** Queue a kv mutation for replay on reconnect. */
3170
+ enqueue(method: "set" | "delete", args: unknown, hlc: string, clientId: string): Promise<void>;
3171
+ }
3172
+
2720
3173
  /**
2721
3174
  * `client.kv` — per-user key/value storage backed by the accelerator's personal
2722
3175
  * space.
@@ -2746,6 +3199,8 @@ interface KvNamespaceDeps {
2746
3199
  session: SessionState;
2747
3200
  /** WebSocket base (ws/wss) for the change feed; derived from baseUrl. */
2748
3201
  wsBaseUrl: string;
3202
+ /** Offline cache + queue. Undefined ⇒ no offline behavior (network-only). */
3203
+ offline?: KvCache;
2749
3204
  }
2750
3205
  interface SetOptions {
2751
3206
  /** Encrypt the value at rest (default: true). */
@@ -2763,6 +3218,8 @@ declare class KvNamespace {
2763
3218
  private cipher;
2764
3219
  private cipherIdentity;
2765
3220
  constructor(deps: KvNamespaceDeps);
3221
+ /** Replay one queued kv mutation (called by the sync engine). */
3222
+ replay(entry: QueueEntry): Promise<void>;
2766
3223
  /** Store `value` under `collection`/`id` (encrypted at rest by default). */
2767
3224
  set<T = unknown>(collection: string, id: string, value: T, opts?: SetOptions): Promise<void>;
2768
3225
  /** Fetch the value at `collection`/`id`, or `null` if absent. */
@@ -2791,6 +3248,45 @@ declare class KvNamespace {
2791
3248
  private getCipher;
2792
3249
  }
2793
3250
 
3251
+ /**
3252
+ * `DbCache` — the offline adapter for `client.db`. Unlike kv, db rows are
3253
+ * server-plaintext, so they're cached as-is. The SDK doesn't know a table's
3254
+ * primary-key column, so it caches schema-agnostically:
3255
+ * - `get(id)` results keyed by the requested id (`${table}|row|${id}`),
3256
+ * - `query(q)` result pages keyed by a stable hash of the query.
3257
+ *
3258
+ * Writes carry per-column HLC stamps (`_hlc`) so the accelerator's per-column
3259
+ * LWW merge can converge concurrent edits; offline writes are queued and
3260
+ * replayed. Optimistic row patches keep `get` fresh between sync cycles.
3261
+ */
3262
+
3263
+ interface DbQueryPage {
3264
+ rows: Array<Record<string, unknown>>;
3265
+ nextCursor: string | null;
3266
+ }
3267
+ /** Stable JSON for query-cache keys (sorted object keys → deterministic). */
3268
+ declare function stableStringify(value: unknown): string;
3269
+ declare class DbCache {
3270
+ private readonly manager;
3271
+ private readonly store;
3272
+ constructor(manager: OfflineManager);
3273
+ get enabled(): boolean;
3274
+ nextHlc(): Promise<string>;
3275
+ newClientId(): string;
3276
+ /** Build the `{ [col]: hlc }` map for a write (one stamp across its columns). */
3277
+ columnStamps(values: Record<string, unknown>, hlc: string): Record<string, string>;
3278
+ private rowKey;
3279
+ private queryKey;
3280
+ readRow(table: string, id: string): Promise<Record<string, unknown> | null>;
3281
+ writeRow(table: string, id: string, row: Record<string, unknown>): Promise<void>;
3282
+ /** Shallow-merge a partial update into a cached row (optimistic). */
3283
+ patchRow(table: string, id: string, values: Record<string, unknown>): Promise<void>;
3284
+ deleteRow(table: string, id: string): Promise<void>;
3285
+ readQuery(table: string, q: unknown): Promise<DbQueryPage | null>;
3286
+ writeQuery(table: string, q: unknown, page: DbQueryPage): Promise<void>;
3287
+ enqueue(method: "insert" | "update" | "delete", args: unknown, hlc: string, clientId: string): Promise<void>;
3288
+ }
3289
+
2794
3290
  /**
2795
3291
  * `client.db` — the app's scalable database. A developer defines tables in the
2796
3292
  * Muhkoo portal (columns + indexes); each table is backed by its own Durable
@@ -2822,6 +3318,8 @@ declare class KvNamespace {
2822
3318
 
2823
3319
  interface DbNamespaceDeps {
2824
3320
  http: HttpClient;
3321
+ /** Offline row/query cache + write queue. Undefined ⇒ network-only. */
3322
+ offline?: DbCache;
2825
3323
  }
2826
3324
  /** Comparison operators allowed in a {@link DbQuery} `where` clause. */
2827
3325
  type DbFilterOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "like" | "likeStartsWith" | "likeContains";
@@ -2855,8 +3353,10 @@ interface DbQueryResult<T = Record<string, unknown>> {
2855
3353
  declare class DbTable<T extends Record<string, unknown> = Record<string, unknown>> {
2856
3354
  private readonly http;
2857
3355
  private readonly name;
2858
- constructor(http: HttpClient, name: string);
3356
+ private readonly offline?;
3357
+ constructor(http: HttpClient, name: string, offline?: DbCache | undefined);
2859
3358
  private base;
3359
+ private rowUrl;
2860
3360
  /** Insert a row. Returns the created row and its primary-key value. */
2861
3361
  insert(values: Partial<T>): Promise<{
2862
3362
  row: T;
@@ -2884,6 +3384,8 @@ declare class DbNamespace {
2884
3384
  * const todos = client.db.table<Todo>('todos');
2885
3385
  */
2886
3386
  table<T extends Record<string, unknown> = Record<string, unknown>>(name: string): DbTable<T>;
3387
+ /** Replay one queued db mutation (called by the sync engine). */
3388
+ replay(entry: QueueEntry): Promise<void>;
2887
3389
  }
2888
3390
 
2889
3391
  /**
@@ -2923,6 +3425,10 @@ interface FileNamespaceDeps {
2923
3425
  * deploy-time-precompiled environments). Forwarded to {@link FileStorage}.
2924
3426
  */
2925
3427
  rsWasmModule?: WebAssembly.Module;
3428
+ /** Offline shard-byte cache (browser). Enables offline reads of cached files. */
3429
+ shardCache?: ShardByteCache;
3430
+ /** Queue a shard PUT that couldn't reach the network (offline). */
3431
+ deferShardUpload?: (hash: string) => Promise<void>;
2926
3432
  }
2927
3433
  interface WriteFileOptions {
2928
3434
  /** SharedSpace the file is written to (obtain via `client.space`). */
@@ -3288,6 +3794,19 @@ declare class SpaceKeyring implements EpochKeyProvider {
3288
3794
  */
3289
3795
 
3290
3796
  type Listener = (e: CustomEvent) => void;
3797
+ /**
3798
+ * A P2P swarm attached to a Space (the p2p layer's `PeerNetwork` satisfies this
3799
+ * structurally — Space never imports the p2p module). `exchange` is consulted by
3800
+ * the Space's file `ShardClient`; `start` runs peer discovery on connect; `close`
3801
+ * tears the mesh down.
3802
+ */
3803
+ interface AttachedPeerNetwork {
3804
+ readonly exchange: PeerBlockSource;
3805
+ start(): void;
3806
+ close(): void;
3807
+ gossip(data: Uint8Array): void;
3808
+ onGossip(cb: (from: string, data: Uint8Array) => void): () => void;
3809
+ }
3291
3810
  interface SpaceFileMetadata {
3292
3811
  name: string;
3293
3812
  type: string;
@@ -3308,6 +3827,46 @@ interface SpaceMessageEvent {
3308
3827
  */
3309
3828
  handle: number;
3310
3829
  message: Message;
3830
+ /**
3831
+ * Sender-generated id, present on locally-originated messages (carried in the
3832
+ * cleartext `cid` header). Lets the UI dedupe an optimistic send against its
3833
+ * server echo, and the offline cache replace a pending entry with the real one.
3834
+ */
3835
+ clientId?: string;
3836
+ /** True for an optimistic local send not yet acknowledged by the server. */
3837
+ pending?: boolean;
3838
+ }
3839
+ /**
3840
+ * One sealed message frame as cached for offline use. The `packet` is the
3841
+ * ciphertext-bearing serialized {@link Packet} (decrypted lazily on read), so
3842
+ * nothing readable is persisted. Structurally matches the offline layer's
3843
+ * `MessageEntry`; declared here so the spaces module doesn't import `../offline`.
3844
+ */
3845
+ interface CachedSpaceMessage {
3846
+ handle: string;
3847
+ packet: string | null;
3848
+ op: "msg" | "edit" | "delete";
3849
+ clientId?: string;
3850
+ hlc?: string;
3851
+ pending?: boolean;
3852
+ deleted?: boolean;
3853
+ }
3854
+ /** The offline adapter a {@link Space} drives. Implemented by `SpaceCache`. */
3855
+ interface SpaceOfflineAdapter {
3856
+ readonly enabled: boolean;
3857
+ newClientId(): string;
3858
+ nextHlc(): Promise<string>;
3859
+ putMessage(spaceId: string, entry: CachedSpaceMessage): Promise<void>;
3860
+ putDeleted(spaceId: string, handle: number): Promise<void>;
3861
+ dropPending(spaceId: string, clientId: string): Promise<void>;
3862
+ loadMessages(spaceId: string): Promise<CachedSpaceMessage[]>;
3863
+ getCursor(spaceId: string): Promise<{
3864
+ lastSeenHandle: number;
3865
+ oldestHandle: number;
3866
+ } | null>;
3867
+ observeHandle(spaceId: string, handle: number): Promise<void>;
3868
+ enqueueFrame(spaceId: string, frame: unknown, clientId: string, hlc: string): Promise<void>;
3869
+ registerCatchUp(task: () => Promise<void>): () => void;
3311
3870
  }
3312
3871
  /**
3313
3872
  * An inbound ephemeral signal — an application broadcast the server relays to
@@ -3357,6 +3916,12 @@ interface SpaceDeps {
3357
3916
  * without a live websocket.
3358
3917
  */
3359
3918
  createChannel?: (url: string, myId: string) => SpaceChannelLike;
3919
+ /** Offline shard-byte cache for room files (browser). */
3920
+ shardCache?: ShardByteCache;
3921
+ /** Queue a room-file shard PUT that couldn't reach the network. */
3922
+ deferShardUpload?: (hash: string) => Promise<void>;
3923
+ /** Offline message cache + send queue. Undefined ⇒ no offline behavior. */
3924
+ offline?: SpaceOfflineAdapter;
3360
3925
  }
3361
3926
  /** The subset of {@link BroadcastChannel} a {@link Space} drives. */
3362
3927
  interface SpaceChannelLike {
@@ -3380,6 +3945,9 @@ declare class Space {
3380
3945
  /** Per-instance event bus for fan-out events (isolated across spaces). */
3381
3946
  private readonly events;
3382
3947
  private rawWired;
3948
+ private catchUpUnsub;
3949
+ /** Optional P2P swarm for this Space (block exchange among members). */
3950
+ private peerNet;
3383
3951
  constructor(deps: SpaceDeps);
3384
3952
  /** The encoded space id (same as the transport name for fan-out spaces). */
3385
3953
  get id(): string;
@@ -3388,6 +3956,19 @@ declare class Space {
3388
3956
  /** Open the space websocket (resolves the ticket + identity lazily). */
3389
3957
  connect(): Promise<void>;
3390
3958
  disconnect(): void;
3959
+ /** Attach a P2P swarm so this Space's file reads/writes can use peers. */
3960
+ attachPeerNetwork(net: AttachedPeerNetwork): void;
3961
+ /**
3962
+ * Broadcast an app payload directly to connected Space peers over P2P,
3963
+ * **bypassing the server** — for CRDT ops / high-frequency state (live
3964
+ * cursors, presence, collaborative edits) you don't want to push through the
3965
+ * relay. No-op when no P2P mesh is active (requires `client` `p2p`).
3966
+ * Unlike {@link sendEphemeral} (server-relayed, metered, size-limited), this
3967
+ * is peer-direct and fragments large payloads.
3968
+ */
3969
+ gossipToPeers(data: Uint8Array): void;
3970
+ /** Subscribe to peer gossip ({@link gossipToPeers}). Returns an unsubscribe fn. */
3971
+ onPeerGossip(handler: (from: string, data: Uint8Array) => void): () => void;
3391
3972
  /** Subscribe to a raw channel event. Buffered until `connect()` if early. */
3392
3973
  on(event: string, handler: Listener): void;
3393
3974
  off(event: string, handler: Listener): void;
@@ -3429,6 +4010,14 @@ declare class Space {
3429
4010
  * original author. Receivers get an {@link onMessageDeleted} event.
3430
4011
  */
3431
4012
  deleteMessage(handle: number): Promise<void>;
4013
+ /**
4014
+ * Decrypt and return the locally-cached message log for this space, in
4015
+ * handle order (pending sends last). Works offline — the source is the
4016
+ * IndexedDB cache populated by live frames + {@link history}. Apps call this
4017
+ * on boot to paint instantly, then subscribe to {@link onMessage} for live
4018
+ * updates. Returns `[]` when offline support is off.
4019
+ */
4020
+ cachedMessages(): Promise<SpaceMessageEvent[]>;
3432
4021
  /** Subscribe to in-place edits of persisted messages (new content at the same handle). */
3433
4022
  onMessageEdited(handler: (e: SpaceMessageEvent) => void): () => void;
3434
4023
  /** Subscribe to deletions of persisted messages. */
@@ -3440,7 +4029,7 @@ declare class Space {
3440
4029
  * indicators, presence, or live cursors are built on top of this. No-op if
3441
4030
  * the space isn't connected (ephemeral signals aren't worth queuing).
3442
4031
  */
3443
- sendEphemeral(subject: string, data: unknown): void;
4032
+ sendEphemeral(subject: string, data: unknown, to?: string): void;
3444
4033
  /** Subscribe to inbound ephemeral signals. `from` is the authenticated sender. */
3445
4034
  onEphemeral(handler: (e: EphemeralEvent) => void): () => void;
3446
4035
  /** Subscribe to inbound join requests (for a key-holder to admit). */
@@ -3510,6 +4099,15 @@ declare class Space {
3510
4099
  * nothing to verify against → reject.
3511
4100
  */
3512
4101
  private verifySender;
4102
+ /** Persist a decoded live/history/edit frame to the offline cache. */
4103
+ private cacheFrame;
4104
+ /**
4105
+ * Inbound reconciliation run on reconnect: page the server's forward delta
4106
+ * (`?since=<lastSeenHandle>`) to the head, pulling exactly the messages that
4107
+ * landed while we were offline into the cache. Bounded so a long absence
4108
+ * can't page forever. Falls back gracefully if the endpoint is unavailable.
4109
+ */
4110
+ private catchUp;
3513
4111
  private emitEvent;
3514
4112
  private requireKeyring;
3515
4113
  private requireCipher;
@@ -3642,9 +4240,22 @@ interface SpaceNamespaceDeps {
3642
4240
  wsBaseUrl: string;
3643
4241
  /** Optional PersonalSpace-backed cache of group keys (encrypted at rest). */
3644
4242
  cache?: SpaceKeyCache;
4243
+ /** Offline manager — enables message caching + offline send replay. */
4244
+ offline?: OfflineManager;
4245
+ /** P2P swarm config — enables peer block exchange among Space members. */
4246
+ p2p?: {
4247
+ enabled?: boolean;
4248
+ /** `() => new Worker(new URL(...))` to run the block engine off-thread. */
4249
+ workerFactory?: () => Worker;
4250
+ iceServers?: RTCIceServer[];
4251
+ maxPeers?: number;
4252
+ };
3645
4253
  }
3646
4254
  declare class SpaceNamespace {
3647
4255
  private readonly deps;
4256
+ private spaceCache?;
4257
+ /** Spaces built this session, by id — used to route offline send replays. */
4258
+ private readonly openSpaces;
3648
4259
  constructor(deps: SpaceNamespaceDeps);
3649
4260
  /**
3650
4261
  * Create a new space: generate its keypair (public key = id), register
@@ -4089,6 +4700,34 @@ interface ClientOptions {
4089
4700
  * staging (`auth.staging.muhkoo.dev`) or a self-hosted deployment.
4090
4701
  */
4091
4702
  authBaseUrl?: string;
4703
+ /**
4704
+ * Offline support (`client.offline`) — transparent local caching + a durable
4705
+ * write queue + CRDT sync. **On by default in browsers** (IndexedDB + Cache
4706
+ * API present), a no-op everywhere else. Override to force it on/off or to
4707
+ * supply a custom {@link OfflineStore}.
4708
+ */
4709
+ offline?: {
4710
+ /** Force enable/disable. Default: auto-detect (browser on, Node off). */
4711
+ enabled?: boolean;
4712
+ /** Pluggable persistence. Default: IndexedDB in the browser. */
4713
+ store?: OfflineStore;
4714
+ /** Cache file-shard bytes in the Cache API. Default: true in the browser. */
4715
+ cacheShards?: boolean;
4716
+ /** Soft cap on durable-queue bytes before degrading to online-only. */
4717
+ maxQueueBytes?: number;
4718
+ };
4719
+ /**
4720
+ * Peer-to-peer block exchange (`client.space` files) among Space members,
4721
+ * over WebRTC signaled on the Space relay. Best-effort — falls back to
4722
+ * origin. **Opt-in** (browser only). Pass `workerFactory` to run the block
4723
+ * engine off the main thread.
4724
+ */
4725
+ p2p?: {
4726
+ enabled?: boolean;
4727
+ workerFactory?: () => Worker;
4728
+ iceServers?: RTCIceServer[];
4729
+ maxPeers?: number;
4730
+ };
4092
4731
  }
4093
4732
  declare class Client {
4094
4733
  readonly baseUrl: string;
@@ -4108,6 +4747,8 @@ declare class Client {
4108
4747
  readonly agents: AgentsNamespace;
4109
4748
  /** Serverless functions — `client.functions.deploy(appId, …)`, etc. */
4110
4749
  readonly functions: FunctionsNamespace;
4750
+ /** Offline cache + sync — `client.offline.status`, `client.offline.snapshot`, etc. */
4751
+ readonly offline: OfflineManager;
4111
4752
  private readonly session;
4112
4753
  private readonly http;
4113
4754
  /** Subscribers notified when a session expires and can't be recovered. */
@@ -4268,6 +4909,697 @@ declare function ejectAgentTools(appClass: Function, opts?: {
4268
4909
  maxIterations?: number;
4269
4910
  }): AgentToolsConfig;
4270
4911
 
4912
+ /**
4913
+ * Message-log helpers — the conflict model for a Space's message history.
4914
+ *
4915
+ * Messages are the easy CRDT: the accelerator assigns every persisted message a
4916
+ * monotonic, zero-padded `handle` that already totally-orders the log, and the
4917
+ * sealed body is immutable. So the "log" is just a grow-only set keyed by
4918
+ * `handle`, and convergence is structural — no server change needed.
4919
+ *
4920
+ * The only subtleties this module encodes:
4921
+ * - **Optimistic sends** have no server handle yet. We key them by a
4922
+ * provisional handle that sorts *after* every real one, and tag them with
4923
+ * the sender's `clientId` so the real frame (carrying the same `clientId`)
4924
+ * can replace the placeholder instead of duplicating it.
4925
+ * - **Edits** are a per-message LWW register on the body (greater HLC wins).
4926
+ * - **Deletes** are a tombstone at the message's handle.
4927
+ */
4928
+ type MessageOp = "msg" | "edit" | "delete";
4929
+ interface MessageEntry {
4930
+ /** Server-assigned handle, or a {@link provisionalHandle} while pending. */
4931
+ handle: string;
4932
+ /** Sealed packet string (ciphertext at rest); `null` for a delete tombstone. */
4933
+ packet: string | null;
4934
+ op: MessageOp;
4935
+ /** Sender-generated id, present on locally-originated sends for dedupe. */
4936
+ clientId?: string;
4937
+ /** HLC stamp — orders edits to the same message. */
4938
+ hlc?: string;
4939
+ /** True while an optimistic send has not yet been acked by the server. */
4940
+ pending?: boolean;
4941
+ deleted?: boolean;
4942
+ }
4943
+ /** IndexedDB key for a message within a space. */
4944
+ declare function messageKey(spaceId: string, handle: string): string;
4945
+ /**
4946
+ * A handle for an un-acked optimistic message. Prefixed with `"~"` (0x7E),
4947
+ * which sorts after every digit, so pending sends always appear at the tail of
4948
+ * the handle-ordered log until the real handle arrives.
4949
+ */
4950
+ declare function provisionalHandle(clientId: string): string;
4951
+ declare function isProvisional(handle: string): boolean;
4952
+ /**
4953
+ * Merge two entries for the SAME handle. A delete always wins (tombstone); an
4954
+ * edit wins over the entry it supersedes when its HLC is greater; otherwise the
4955
+ * existing entry stands (a re-delivered `msg` is idempotent).
4956
+ */
4957
+ declare function mergeMessage(local: MessageEntry | null, incoming: MessageEntry): MessageEntry;
4958
+
4959
+ /**
4960
+ * `SpaceCache` — the offline adapter a {@link ../spaces/Space} talks to. It
4961
+ * persists sealed message frames (ciphertext at rest), tracks per-space sync
4962
+ * cursors, and records offline sends in the durable queue. Kept as its own
4963
+ * object (rather than wiring the Space straight to the store + clock) so the
4964
+ * Space stays decoupled from IndexedDB and so the no-op path is a single
4965
+ * `enabled` check.
4966
+ *
4967
+ * Messages are the structural CRDT (see {@link ./crdt/MessageLog}): the server
4968
+ * assigns a monotonic `handle` that totally-orders the log, so the cache just
4969
+ * stores frames keyed by a zero-padded handle (real) or a provisional key
4970
+ * (pending sends, which sort to the tail until their echo arrives).
4971
+ */
4972
+
4973
+ /** Zero-pad a numeric server handle so string keys sort in handle order. */
4974
+ declare function padHandle(handle: number): string;
4975
+ interface SpaceCursor {
4976
+ lastSeenHandle: number;
4977
+ oldestHandle: number;
4978
+ fullyBackfilled: boolean;
4979
+ }
4980
+ /**
4981
+ * The surface a {@link ../spaces/Space} uses. Declared structurally on the
4982
+ * Space side too (as `SpaceOfflineAdapter`) so the spaces module never imports
4983
+ * the offline module.
4984
+ */
4985
+ declare class SpaceCache {
4986
+ private readonly manager;
4987
+ private readonly store;
4988
+ constructor(manager: OfflineManager);
4989
+ get enabled(): boolean;
4990
+ newClientId(): string;
4991
+ nextHlc(): Promise<string>;
4992
+ /** Persist a sealed message frame keyed by its (real or provisional) handle. */
4993
+ putMessage(spaceId: string, entry: MessageEntry): Promise<void>;
4994
+ /** Tombstone the message at `handle`. */
4995
+ putDeleted(spaceId: string, handle: number): Promise<void>;
4996
+ /** Drop a pending (optimistic) entry once its real echo has been stored. */
4997
+ dropPending(spaceId: string, clientId: string): Promise<void>;
4998
+ /** All cached frames for a space, in handle order (pending ones last). */
4999
+ loadMessages(spaceId: string): Promise<MessageEntry[]>;
5000
+ getCursor(spaceId: string): Promise<SpaceCursor | null>;
5001
+ /** Advance the last-seen handle (monotonic) and track the oldest fetched. */
5002
+ observeHandle(spaceId: string, handle: number): Promise<void>;
5003
+ /** Queue a raw space frame for replay when the connection returns. */
5004
+ enqueueFrame(spaceId: string, frame: unknown, clientId: string, hlc: string): Promise<void>;
5005
+ /** Register an inbound catch-up task (forward/back history paging). */
5006
+ registerCatchUp(task: () => Promise<void>): () => void;
5007
+ }
5008
+
5009
+ /**
5010
+ * Errors specific to the offline layer.
5011
+ */
5012
+ /**
5013
+ * Thrown when an offline read/write needs the user's identity (to decrypt or
5014
+ * re-encrypt cached data) but the client is locked — typically right after a
5015
+ * reload, where only the session token was restored and `unlock()` hasn't run
5016
+ * yet. Distinct from the generic "not signed in" identity error so apps can
5017
+ * specifically prompt the user to unlock to see their offline data.
5018
+ */
5019
+ declare class OfflineLockedError extends Error {
5020
+ constructor(message?: string);
5021
+ }
5022
+
5023
+ /**
5024
+ * Environment probe deciding whether the offline layer turns itself on. Offline
5025
+ * support is **on by default in browsers** and a no-op everywhere else, so this
5026
+ * is the gate the {@link ../core/Client} consults to pick {@link
5027
+ * ./store/IndexedDbStore} vs {@link ./store/NoopStore}.
5028
+ *
5029
+ * We require all three web APIs the layer leans on — `indexedDB` (structured
5030
+ * cache + durable queue), the Cache API (file-shard bytes), and a `window`
5031
+ * (so we're in a real document with online/offline events) — and we *touch*
5032
+ * them inside a try/catch, because some sandboxed or private-mode contexts
5033
+ * expose the globals but throw on use (mirrors `defaultSessionStore`).
5034
+ */
5035
+ declare function isOfflineCapable(): boolean;
5036
+
5037
+ /**
5038
+ * `IndexedDbStore` — the browser {@link OfflineStore}, backed by one IndexedDB
5039
+ * database per origin (see {@link ./schema}). It opens lazily on first use and
5040
+ * memoizes the connection. The queue methods sit on the autoincrement
5041
+ * `outbound-queue` store and return mutations in HLC order for the sync engine.
5042
+ *
5043
+ * Construct only where `indexedDB` exists — {@link ../detect.isOfflineCapable}
5044
+ * gates this, and the {@link ../../core/Client} falls back to {@link ./NoopStore}
5045
+ * everywhere else.
5046
+ */
5047
+
5048
+ declare class IndexedDbStore implements OfflineStore {
5049
+ readonly enabled = true;
5050
+ private conn;
5051
+ private opening;
5052
+ private connect;
5053
+ ready(): Promise<void>;
5054
+ get<T>(store: StoreName, key: string): Promise<T | null>;
5055
+ put<T>(store: StoreName, key: string, value: T): Promise<void>;
5056
+ delete(store: StoreName, key: string): Promise<void>;
5057
+ prefix<T>(store: StoreName, prefix: string): Promise<Array<OfflineEntry<T>>>;
5058
+ range<T>(store: StoreName, lower: string, upper: string): Promise<Array<OfflineEntry<T>>>;
5059
+ all<T>(store: StoreName): Promise<Array<OfflineEntry<T>>>;
5060
+ clear(store: StoreName): Promise<void>;
5061
+ enqueue(entry: Omit<QueueEntry, "seq">): Promise<number>;
5062
+ dequeue(seq: number): Promise<void>;
5063
+ queued(): Promise<QueueEntry[]>;
5064
+ }
5065
+
5066
+ /**
5067
+ * `NoopStore` — the {@link OfflineStore} used wherever offline support is off
5068
+ * (Node, Workers, SSR, or when a consumer disables it). Every read returns
5069
+ * empty and every write is dropped, so the namespace hooks that call into it
5070
+ * behave exactly as they did before the offline layer existed. `enabled` is
5071
+ * `false` so hot paths can skip the call entirely.
5072
+ */
5073
+
5074
+ declare class NoopStore implements OfflineStore {
5075
+ readonly enabled = false;
5076
+ ready(): Promise<void>;
5077
+ get<T>(_store: StoreName, _key: string): Promise<T | null>;
5078
+ put<T>(_store: StoreName, _key: string, _value: T): Promise<void>;
5079
+ delete(_store: StoreName, _key: string): Promise<void>;
5080
+ prefix<T>(_store: StoreName, _prefix: string): Promise<Array<OfflineEntry<T>>>;
5081
+ range<T>(_store: StoreName, _lower: string, _upper: string): Promise<Array<OfflineEntry<T>>>;
5082
+ all<T>(_store: StoreName): Promise<Array<OfflineEntry<T>>>;
5083
+ clear(_store: StoreName): Promise<void>;
5084
+ enqueue(_entry: Omit<QueueEntry, "seq">): Promise<number>;
5085
+ dequeue(_seq: number): Promise<void>;
5086
+ queued(): Promise<QueueEntry[]>;
5087
+ }
5088
+
5089
+ /** The smallest possible stamp — used as the "nothing stored yet" sentinel so a
5090
+ * first real write always wins (`compareHlc(real, ZERO_HLC) > 0`). */
5091
+ declare const ZERO_HLC: string;
5092
+ interface HlcParts {
5093
+ /** Physical wall-clock time, milliseconds since the Unix epoch. */
5094
+ wall: number;
5095
+ /** Logical counter within `wall`. */
5096
+ counter: number;
5097
+ /** Stable per-install node id (tiebreak). */
5098
+ nodeId: string;
5099
+ }
5100
+ /** Pack the three components into the sortable string form. */
5101
+ declare function pack(wall: number, counter: number, nodeId: string): string;
5102
+ /** Parse a packed stamp back into its components. Tolerates the empty/zero
5103
+ * sentinel. Throws on a malformed string so corruption surfaces loudly. */
5104
+ declare function unpack(stamp: string): HlcParts;
5105
+ /**
5106
+ * Total order over packed HLC strings: negative if `a < b`, positive if
5107
+ * `a > b`, `0` only when they are byte-identical. Because the fields are
5108
+ * fixed-width and ordered (wall, counter, nodeId), a single string comparison
5109
+ * already yields causal order — this helper just normalizes the result to the
5110
+ * `-1 | 0 | 1` shape callers expect.
5111
+ */
5112
+ declare function compareHlc(a: string, b: string): number;
5113
+ /** Convenience: is `a` strictly newer than `b`? */
5114
+ declare function isNewer(a: string, b: string): boolean;
5115
+
5116
+ /**
5117
+ * `HLC` — a Hybrid Logical Clock. Hands out monotonically increasing
5118
+ * {@link HlcTimestamp} stamps that (a) track real time closely enough to be
5119
+ * human-meaningful, yet (b) never go backwards, even across a reload or when a
5120
+ * peer's clock is ahead of ours.
5121
+ *
5122
+ * Two operations, both the textbook HLC algorithm (Kulkarni et al.):
5123
+ * - `now()` — stamp a local event.
5124
+ * - `update(remote)` — observe a remote stamp (e.g. arriving on the realtime
5125
+ * change feed) and advance our clock past it, so any event we generate
5126
+ * afterwards is causally `> remote`.
5127
+ *
5128
+ * State is just `{ wall, counter }`; `nodeId` is fixed for the install. The
5129
+ * owner ({@link ../OfflineManager}) seeds the clock from persisted state on
5130
+ * boot and persists `getState()` after each tick so monotonicity survives page
5131
+ * reloads — without that, a reload could mint a stamp that loses to one written
5132
+ * seconds earlier.
5133
+ */
5134
+
5135
+ interface HlcState {
5136
+ wall: number;
5137
+ counter: number;
5138
+ }
5139
+ /** Injected physical-time source — defaults to `Date.now`. Overridable so
5140
+ * tests can drive the clock deterministically. */
5141
+ type PhysicalClock = () => number;
5142
+ declare class HLC {
5143
+ private wall;
5144
+ private counter;
5145
+ private readonly nodeId;
5146
+ private readonly physical;
5147
+ constructor(nodeId: string, opts?: {
5148
+ state?: HlcState | null;
5149
+ physical?: PhysicalClock;
5150
+ });
5151
+ /** Stamp a locally-generated event. Advances the clock. */
5152
+ now(): string;
5153
+ /**
5154
+ * Merge in an observed remote stamp and advance our clock strictly past it.
5155
+ * Call this for every inbound HLC (change-feed frames, history rows) so the
5156
+ * next `now()` is causally after anything we've seen.
5157
+ */
5158
+ update(remote: HlcParts): string;
5159
+ /** The current stamp without advancing the clock. */
5160
+ current(): string;
5161
+ /** Serializable `{ wall, counter }` for persistence. */
5162
+ getState(): HlcState;
5163
+ }
5164
+
5165
+ /**
5166
+ * `LWWMap` — a map of independent last-write-wins registers, one per column.
5167
+ * This is the conflict model for a `db` row: two devices that edit *different*
5168
+ * columns of the same row while offline both keep their edits (no lost update),
5169
+ * and two that edit the *same* column converge to the greater-HLC write. A row
5170
+ * delete is a causal tombstone (see {@link ./ORSet}); a later write to any
5171
+ * column resurrects the row with just the surviving columns.
5172
+ *
5173
+ * This mirrors, on the client, exactly what the accelerator's `_muhkoo_crdt`
5174
+ * sidecar does server-side — so a row materializes identically whether it was
5175
+ * merged locally (offline) or by the server on replay.
5176
+ */
5177
+ interface LwwMap {
5178
+ /** Per-column value of the winning write. */
5179
+ fields: Record<string, unknown>;
5180
+ /** Per-column HLC of the winning write. */
5181
+ columnHlc: Record<string, string>;
5182
+ /** Row-delete tombstone HLC, or `null` when the row was never deleted. */
5183
+ tombstone: string | null;
5184
+ }
5185
+ declare function emptyMap(): LwwMap;
5186
+ /** Apply a local column write (insert/update) at the given per-column HLCs. */
5187
+ declare function writeColumns(map: LwwMap, values: Record<string, unknown>, hlc: string): LwwMap;
5188
+ /** Apply a row delete at `hlc` (causal tombstone). */
5189
+ declare function tombstoneRow(map: LwwMap, hlc: string): LwwMap;
5190
+ /** Merge two row states column-by-column. Pure; neither input is mutated. */
5191
+ declare function mergeMap(a: LwwMap, b: LwwMap): LwwMap;
5192
+ /**
5193
+ * Materialize the visible row from merged state, or `null` when the row is
5194
+ * deleted. A column is visible only when its write is causally newer than the
5195
+ * tombstone — so a delete hides every prior column, but a post-delete write to
5196
+ * a column brings the row back with just that column populated.
5197
+ */
5198
+ declare function materializeRow(map: LwwMap): Record<string, unknown> | null;
5199
+
5200
+ /**
5201
+ * Tombstone helpers shared by the LWW CRDTs. We don't need a full
5202
+ * observed-remove set — every domain that deletes (kv keys, db rows) does so
5203
+ * by stamping a delete with an HLC and keeping that stamp around. The only
5204
+ * rule that matters is the causal one:
5205
+ *
5206
+ * a delete at HLC `d` hides the value, but a *later* write at HLC `w > d`
5207
+ * resurrects it; a *stale* write at `w <= d` is dropped.
5208
+ *
5209
+ * Keeping the delete HLC (rather than physically forgetting the key) is what
5210
+ * prevents a delayed offline write from "un-deleting" something a peer already
5211
+ * removed. Callers GC tombstones older than any possible offline window.
5212
+ */
5213
+ /**
5214
+ * Does a write at `writeHlc` win against a tombstone at `tombstoneHlc`?
5215
+ * `true` ⇒ the write applies (resurrecting a deleted item if needed); `false`
5216
+ * ⇒ the tombstone still holds and the write is dropped. A missing tombstone
5217
+ * (`null`) means there's nothing suppressing the write.
5218
+ */
5219
+ declare function writeBeatsTombstone(writeHlc: string, tombstoneHlc: string | null): boolean;
5220
+ /**
5221
+ * Does a delete at `deleteHlc` win against the latest write at `writeHlc`?
5222
+ * `true` ⇒ the item is now tombstoned. Ties go to the delete (remove-wins) so
5223
+ * that a delete and a write stamped in the same instant resolve deterministically
5224
+ * toward removal.
5225
+ */
5226
+ declare function deleteBeatsWrite(deleteHlc: string, writeHlc: string | null): boolean;
5227
+
5228
+ /**
5229
+ * `PeerExchange` — the thin façade the storage layer consults to try fetching a
5230
+ * block from peers before falling back to origin. It satisfies the structural
5231
+ * `PeerBlockSource` interface that {@link ../storage/transport/ShardClient}
5232
+ * declares (so storage never imports the p2p module — the dependency arrow stays
5233
+ * one-way, p2p → storage).
5234
+ *
5235
+ * It delegates to an {@link EngineHandle} — either an in-process {@link
5236
+ * ./worker/blockEngine.BlockEngine} or the Web Worker proxy — so the same façade
5237
+ * works whether the block engine runs on the main thread or off it.
5238
+ */
5239
+
5240
+ /** The subset of a block engine (in-process or worker-proxied) PeerExchange drives. */
5241
+ interface EngineHandle {
5242
+ want(hash: string, timeoutMs: number): Promise<Uint8Array | null>;
5243
+ announce(hash: string): void;
5244
+ }
5245
+ interface PeerExchangeOptions {
5246
+ /** How long to wait for a peer before giving up to origin. Default 2500ms. */
5247
+ timeoutMs?: number;
5248
+ }
5249
+ declare class PeerExchange implements PeerBlockSource {
5250
+ private readonly engine;
5251
+ private readonly timeoutMs;
5252
+ constructor(engine: EngineHandle, opts?: PeerExchangeOptions);
5253
+ getBlock(hash: string, opts?: {
5254
+ timeoutMs?: number;
5255
+ }): Promise<Uint8Array | null>;
5256
+ announce(hash: string): void;
5257
+ }
5258
+
5259
+ /**
5260
+ * `SpaceSignaler` — WebRTC signaling carried over the Space's existing ephemeral
5261
+ * relay. No new server channel: `Space.sendEphemeral`/`onEphemeral` already
5262
+ * blind-relay `pub` frames stamped with the authenticated sender, so SDP
5263
+ * offers/answers and ICE candidates ride a reserved subject (`__p2p__`) scoped
5264
+ * to Space membership. The sender (`from`) is server-stamped and can't be
5265
+ * spoofed; the payload is opaque to the server.
5266
+ *
5267
+ * Envelopes carry a `to` (a member id, or `"*"` for discovery) so the
5268
+ * accelerator can optionally unicast peer-pair signaling instead of broadcasting
5269
+ * (a small, backward-compatible `pub` enhancement); the client filters by `to`
5270
+ * regardless.
5271
+ */
5272
+ declare const P2P_SUBJECT = "__p2p__";
5273
+ type SignalKind = "hello" | "bye" | "offer" | "answer" | "ice";
5274
+ interface SignalEnvelope {
5275
+ /** Target member id, or `"*"` to broadcast (discovery / departure). */
5276
+ to: string;
5277
+ kind: SignalKind;
5278
+ data?: unknown;
5279
+ }
5280
+ /** The slice of {@link ../../spaces/Space.Space} the signaler needs. */
5281
+ interface SignalingSpace {
5282
+ sendEphemeral(subject: string, data: unknown, to?: string): void;
5283
+ onEphemeral(handler: (e: {
5284
+ from: string;
5285
+ subject: string;
5286
+ data: unknown;
5287
+ }) => void): () => void;
5288
+ }
5289
+ declare class SpaceSignaler {
5290
+ private readonly space;
5291
+ private readonly myId;
5292
+ constructor(space: SignalingSpace, myId: string);
5293
+ /** Send a signal to one peer (or `"*"`). */
5294
+ send(to: string, kind: SignalKind, data?: unknown): void;
5295
+ /** Announce/withdraw P2P availability to all Space members. */
5296
+ broadcast(kind: SignalKind, data?: unknown): void;
5297
+ /**
5298
+ * Subscribe to inbound signals addressed to us (or broadcast). Skips our own
5299
+ * echoes. Returns an unsubscribe function.
5300
+ */
5301
+ onSignal(cb: (from: string, kind: SignalKind, data: unknown) => void): () => void;
5302
+ }
5303
+
5304
+ /**
5305
+ * `PeerTransport` — the pluggable wire under the block engine. It moves opaque
5306
+ * frames between Space peers; it knows nothing about blocks, hashes, or the
5307
+ * protocol. {@link ./WebRtcTransport} implements it over RTCDataChannels today;
5308
+ * a future libp2p/Helia transport can implement the same interface without the
5309
+ * block engine or `ShardClient` changing.
5310
+ *
5311
+ * Implementations are responsible for fragmenting/ reassembling frames larger
5312
+ * than the underlying channel's max message size (a 4 MiB shard won't fit in one
5313
+ * `RTCDataChannel.send`); the engine always deals in whole frames.
5314
+ */
5315
+ type PeerId = string;
5316
+ interface PeerTransport {
5317
+ /** Currently connected peer ids. */
5318
+ peers(): PeerId[];
5319
+ /** Dial a discovered peer (optional — some transports connect implicitly). */
5320
+ connectTo?(peer: PeerId): void;
5321
+ /** Send a whole frame to one peer on `channel` (fragmented internally if needed). */
5322
+ send(peer: PeerId, data: Uint8Array, channel?: number): void;
5323
+ /** Send a whole frame to every connected peer on `channel`. */
5324
+ broadcast(data: Uint8Array, channel?: number): void;
5325
+ /** Subscribe to inbound (reassembled) frames. Returns an unsubscribe fn. */
5326
+ onMessage(cb: (peer: PeerId, data: Uint8Array, channel: number) => void): () => void;
5327
+ /** Subscribe to the connected-peer set changing. Returns an unsubscribe fn. */
5328
+ onPeerChange(cb: (peers: PeerId[]) => void): () => void;
5329
+ /** Tear down all connections. */
5330
+ close(): void;
5331
+ }
5332
+
5333
+ /**
5334
+ * `BlockEngine` — the bitswap-lite want/have ledger. Host-agnostic: it speaks
5335
+ * only to a {@link BlockStore} (get/put/has) and an `out` sink that ships
5336
+ * encoded frames to a peer (or `"*"` to broadcast). The host wires `out` and
5337
+ * `handleFrame` to a real transport — on the main thread or inside the Web
5338
+ * Worker (see {@link ./engineClient}) — so this class is pure and unit-testable.
5339
+ *
5340
+ * It never sees keys: blocks are AES-GCM ciphertext, addressed by SHA-256. The
5341
+ * one trust check is integrity — an inbound BLOCK is re-hashed and dropped on
5342
+ * mismatch, so a malicious peer can't poison the store (the requester just
5343
+ * falls back to origin).
5344
+ */
5345
+ interface BlockStore {
5346
+ get(hash: string): Promise<Uint8Array | null>;
5347
+ put(hash: string, bytes: Uint8Array): Promise<void>;
5348
+ has(hash: string): Promise<boolean>;
5349
+ }
5350
+ /** Ship an encoded frame to one peer, or `"*"` for every connected peer. */
5351
+ type OutSink = (target: string | "*", frame: Uint8Array) => void;
5352
+ /** Lowercase hex SHA-256 of `bytes` — injected so tests can stub it. */
5353
+ type Hasher = (bytes: Uint8Array) => Promise<string>;
5354
+ declare class BlockEngine {
5355
+ private readonly store;
5356
+ private readonly out;
5357
+ private readonly hash;
5358
+ private readonly wants;
5359
+ constructor(store: BlockStore, out: OutSink, hash: Hasher);
5360
+ /**
5361
+ * Ask the swarm for a block. Resolves with the verified bytes, or `null` on
5362
+ * timeout (caller then falls back to origin). Returns immediately if we
5363
+ * already hold it; concurrent wants for the same hash share one request.
5364
+ */
5365
+ want(hash: string, timeoutMs: number): Promise<Uint8Array | null>;
5366
+ /** Tell the swarm we now hold `hash` (so peers wanting it can pull from us). */
5367
+ announce(hash: string): void;
5368
+ /** Handle one inbound frame from `peer`. */
5369
+ handleFrame(peer: string, frame: Uint8Array): Promise<void>;
5370
+ /** Drop all pending waits (resolve null) — on teardown. */
5371
+ close(): void;
5372
+ }
5373
+
5374
+ /**
5375
+ * `PeerNetwork` — the per-Space orchestrator that ties the pieces together on
5376
+ * the main thread:
5377
+ *
5378
+ * transport (WebRTC) ⇄ engine host (Worker or in-process) → PeerExchange
5379
+ * ▲ │
5380
+ * └────────────── signaling over the Space relay ─────────────┘
5381
+ *
5382
+ * It owns one {@link ./transport/WebRtcTransport} + one {@link
5383
+ * ./worker/engineHost.EngineHost}, wires inbound frames to the engine and the
5384
+ * engine's outbound frames back to the transport, runs peer discovery over the
5385
+ * Space's ephemeral channel, and exposes a {@link ./PeerExchange.PeerExchange}
5386
+ * for `ShardClient` to consult. Scope is one Space (private swarm).
5387
+ *
5388
+ * `engineHost` defaults to in-process; pass `workerFactory` to run the block
5389
+ * engine off the main thread (the consumer's bundler resolves the worker URL).
5390
+ */
5391
+
5392
+ interface PeerNetworkOptions {
5393
+ /** The Space (used for signaling over its ephemeral relay). */
5394
+ space: SignalingSpace;
5395
+ /** Our authenticated member id (the Space's `myId`). */
5396
+ myId: string;
5397
+ /** Blockstore for the in-process engine. Defaults to a `ShardCache`. */
5398
+ store?: BlockStore;
5399
+ /** Run the block engine in a Worker — `() => new Worker(new URL(...))`. */
5400
+ workerFactory?: () => Worker;
5401
+ iceServers?: RTCIceServer[];
5402
+ maxPeers?: number;
5403
+ /** Peer fetch timeout passed to PeerExchange. */
5404
+ timeoutMs?: number;
5405
+ /** Inject a transport (tests / a future libp2p transport). Default WebRTC. */
5406
+ transport?: PeerTransport;
5407
+ }
5408
+ declare class PeerNetwork {
5409
+ /** Pass this to a `ShardClient` as `peers`. */
5410
+ readonly exchange: PeerExchange;
5411
+ private readonly transport;
5412
+ private readonly host;
5413
+ private readonly signaler;
5414
+ private readonly offs;
5415
+ private readonly gossipCbs;
5416
+ private closed;
5417
+ constructor(opts: PeerNetworkOptions);
5418
+ /**
5419
+ * Begin peer discovery — broadcast a hello so existing members dial us and
5420
+ * we learn them. Call once the signaling channel is live (the Space fires
5421
+ * this on CONNECTED); safe to call again on reconnect.
5422
+ */
5423
+ start(): void;
5424
+ /**
5425
+ * Broadcast an app payload to all connected peers over the gossip channel —
5426
+ * a direct, server-bypassing data path for CRDT ops / high-frequency state
5427
+ * (cursors, presence, collaborative edits). Single-hop (no re-forwarding).
5428
+ */
5429
+ gossip(data: Uint8Array): void;
5430
+ /** Subscribe to gossip from peers. Returns an unsubscribe function. */
5431
+ onGossip(cb: (from: string, data: Uint8Array) => void): () => void;
5432
+ /** Connected peer count (open data channels). */
5433
+ peerCount(): number;
5434
+ close(): void;
5435
+ }
5436
+
5437
+ /**
5438
+ * Whether this environment can do WebRTC peering. `RTCPeerConnection` is
5439
+ * Window-only (absent in Node/Workers/SSR), so this gates whether a
5440
+ * {@link ./PeerNetwork} can be built at all.
5441
+ */
5442
+ declare function isP2pCapable(): boolean;
5443
+
5444
+ /**
5445
+ * `WebRtcTransport` — a {@link ./PeerTransport.PeerTransport} over RTCDataChannels,
5446
+ * signaled by {@link ../signaling/SpaceSignaler}. Runs on the **main thread**
5447
+ * (`RTCPeerConnection` is Window-only — not available in Workers), so the block
5448
+ * engine lives in a Worker and this only shuttles bytes.
5449
+ *
5450
+ * Connection setup uses a deterministic initiator (the peer with the smaller id
5451
+ * dials) to avoid offer/answer glare. STUN handles NAT traversal; there is no
5452
+ * TURN — an unreachable peer simply never connects and the caller falls back to
5453
+ * origin (P2P is best-effort).
5454
+ *
5455
+ * Frames (up to a 4 MiB shard) exceed an RTCDataChannel's max message size, so
5456
+ * each frame is fragmented into `CHUNK`-sized pieces with a 12-byte header
5457
+ * `[msgId u32][index u32][total u32]` and reassembled per peer.
5458
+ */
5459
+
5460
+ interface WebRtcTransportOptions {
5461
+ iceServers?: RTCIceServer[];
5462
+ /** Cap on simultaneous peer connections (mesh size). Default 8. */
5463
+ maxPeers?: number;
5464
+ }
5465
+ declare class WebRtcTransport implements PeerTransport {
5466
+ private readonly signaler;
5467
+ private readonly myId;
5468
+ private readonly conns;
5469
+ private readonly msgCbs;
5470
+ private readonly peerCbs;
5471
+ private readonly offSignal;
5472
+ private readonly iceServers;
5473
+ private readonly maxPeers;
5474
+ private nextMsgId;
5475
+ constructor(signaler: SpaceSignaler, myId: PeerId, opts?: WebRtcTransportOptions);
5476
+ peers(): PeerId[];
5477
+ /** Dial a peer (no-op unless we're the deterministic initiator for the pair). */
5478
+ connectTo(peerId: PeerId): void;
5479
+ send(peer: PeerId, data: Uint8Array, channel?: number): void;
5480
+ broadcast(data: Uint8Array, channel?: number): void;
5481
+ onMessage(cb: (peer: PeerId, data: Uint8Array, channel: number) => void): () => void;
5482
+ onPeerChange(cb: (peers: PeerId[]) => void): () => void;
5483
+ close(): void;
5484
+ private newPc;
5485
+ private initiate;
5486
+ private bindChannel;
5487
+ private onSignal;
5488
+ private drop;
5489
+ private firePeerChange;
5490
+ private sendFragmented;
5491
+ private drain;
5492
+ private onChunk;
5493
+ }
5494
+
5495
+ /**
5496
+ * `EngineHost` — the boundary {@link ../PeerNetwork} wires a transport to. It
5497
+ * abstracts *where* the {@link ./blockEngine.BlockEngine} runs:
5498
+ * - {@link LocalEngineHost} runs it on the calling thread (tests, or a
5499
+ * fallback when the Web Worker can't be built by a consumer's bundler);
5500
+ * - the Worker proxy ({@link ./engineClient}) runs it off-thread.
5501
+ *
5502
+ * Either way the host exposes the same surface: drive `want`/`announce`, feed it
5503
+ * inbound frames via `handleFrame`, and receive its outbound frames via
5504
+ * `onOutbound` (which the network forwards to the transport).
5505
+ */
5506
+
5507
+ interface EngineHost {
5508
+ want(hash: string, timeoutMs: number): Promise<Uint8Array | null>;
5509
+ announce(hash: string): void;
5510
+ /** Feed an inbound frame received from `peer` on the transport. */
5511
+ handleFrame(peer: string, frame: Uint8Array): void;
5512
+ /** Receive frames the engine wants to send (`"*"` = broadcast). */
5513
+ onOutbound(cb: (target: string, frame: Uint8Array) => void): () => void;
5514
+ close(): void;
5515
+ }
5516
+ /** Runs the engine in-process (main thread or test). */
5517
+ declare class LocalEngineHost implements EngineHost {
5518
+ private readonly engine;
5519
+ private readonly cbs;
5520
+ constructor(store: BlockStore, hasher: Hasher);
5521
+ want(hash: string, timeoutMs: number): Promise<Uint8Array | null>;
5522
+ announce(hash: string): void;
5523
+ handleFrame(peer: string, frame: Uint8Array): void;
5524
+ onOutbound(cb: (target: string, frame: Uint8Array) => void): () => void;
5525
+ close(): void;
5526
+ }
5527
+
5528
+ /**
5529
+ * `WorkerEngineHost` — the main-thread proxy to a {@link ./blockEngine.BlockEngine}
5530
+ * running inside a dedicated Web Worker (so hashing, blockstore I/O, and the
5531
+ * protocol codec stay off the main thread). It speaks the small message protocol
5532
+ * in {@link ./blockEngine.worker} and satisfies {@link ./engineHost.EngineHost},
5533
+ * so {@link ../PeerNetwork} treats it identically to the in-process host.
5534
+ *
5535
+ * Block bytes cross the worker boundary as **transferable** ArrayBuffers
5536
+ * (zero-copy). Only encrypted blocks ever cross — keys never enter the worker.
5537
+ */
5538
+
5539
+ declare class WorkerEngineHost implements EngineHost {
5540
+ private readonly worker;
5541
+ private readonly wants;
5542
+ private readonly cbs;
5543
+ private nextId;
5544
+ constructor(worker: Worker);
5545
+ want(hash: string, timeoutMs: number): Promise<Uint8Array | null>;
5546
+ announce(hash: string): void;
5547
+ handleFrame(peer: string, frame: Uint8Array): void;
5548
+ onOutbound(cb: (target: string, frame: Uint8Array) => void): () => void;
5549
+ close(): void;
5550
+ }
5551
+
5552
+ /**
5553
+ * Bitswap-lite wire protocol for peer block exchange. Four frame types over a
5554
+ * {@link ../transport/PeerTransport} data channel:
5555
+ *
5556
+ * WANT(hash) — "does anyone have this block?" / "send it to me"
5557
+ * HAVE(hash) — "I hold this block" (announce / reply to a WANT)
5558
+ * BLOCK(hash, bytes) — the block itself (verified by hash on receipt)
5559
+ * CANCEL(hash) — "stop, I got it elsewhere"
5560
+ *
5561
+ * Frames are compact binary so a 4 MiB shard isn't base64-bloated through JSON:
5562
+ *
5563
+ * byte 0 : type (0 WANT | 1 HAVE | 2 BLOCK | 3 CANCEL)
5564
+ * bytes 1..33 : 32-byte raw SHA-256 of the block (our shard hash, hex-decoded)
5565
+ * bytes 33.. : payload (BLOCK only)
5566
+ *
5567
+ * Blocks are AES-GCM ciphertext, so a frame carries nothing readable; the hash
5568
+ * is the capability check (the receiver re-hashes the payload and drops a
5569
+ * mismatch). See {@link ./blockEngine}.
5570
+ */
5571
+ declare enum FrameType {
5572
+ WANT = 0,
5573
+ HAVE = 1,
5574
+ BLOCK = 2,
5575
+ CANCEL = 3
5576
+ }
5577
+ interface DecodedFrame {
5578
+ type: FrameType;
5579
+ /** Lowercase hex SHA-256 (matches `shardHash`). */
5580
+ hash: string;
5581
+ /** Present only for {@link FrameType.BLOCK}. */
5582
+ payload?: Uint8Array;
5583
+ }
5584
+ declare function encodeWant(hash: string): Uint8Array;
5585
+ declare function encodeHave(hash: string): Uint8Array;
5586
+ declare function encodeCancel(hash: string): Uint8Array;
5587
+ declare function encodeBlock(hash: string, payload: Uint8Array): Uint8Array;
5588
+ declare function decode(frame: Uint8Array): DecodedFrame;
5589
+
5590
+ type protocol_DecodedFrame = DecodedFrame;
5591
+ type protocol_FrameType = FrameType;
5592
+ declare const protocol_FrameType: typeof FrameType;
5593
+ declare const protocol_decode: typeof decode;
5594
+ declare const protocol_encodeBlock: typeof encodeBlock;
5595
+ declare const protocol_encodeCancel: typeof encodeCancel;
5596
+ declare const protocol_encodeHave: typeof encodeHave;
5597
+ declare const protocol_encodeWant: typeof encodeWant;
5598
+ declare namespace protocol {
5599
+ export { protocol_FrameType as FrameType, protocol_decode as decode, protocol_encodeBlock as encodeBlock, protocol_encodeCancel as encodeCancel, protocol_encodeHave as encodeHave, protocol_encodeWant as encodeWant };
5600
+ export type { protocol_DecodedFrame as DecodedFrame };
5601
+ }
5602
+
4271
5603
  /**
4272
5604
  * KeyringClient — HTTP implementation of {@link KeyringTransport} against the
4273
5605
  * accelerator's per-space keyring endpoints. All blobs are opaque to the
@@ -4328,5 +5660,5 @@ declare class KeyringClient implements KeyringTransport {
4328
5660
  fetchMetadata(): Promise<SpaceMetadata | null>;
4329
5661
  }
4330
5662
 
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 };
5663
+ export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, 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, LocalEngineHost, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, P2P_SUBJECT, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VaultUnavailableError, WSTransport, WebRtcTransport, WorkerEngineHost, 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, isP2pCapable, isProvisional, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, sealSeed, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
5664
+ export type { ACL, ACLEntry, AgentConfig, AgentCreateInput, AgentDbToolMode, AgentProvisioned, AgentScopeOpts, AgentSkill, AgentToolsConfig, AgentTrigger, AgentTriggerType, AgentUpdateInput, AgentsNamespaceDeps, AttachedPeerNetwork, AuthClientOptions, AuthKeyPair, AuthSuccess, AuthToken, AuthType, AuthUser, BlockStore, Bn128WasmInstance, BroadcastChannelEvent, BroadcastChannelOptions, CachedSpaceMessage, ChannelLike, ChunkKeyMaterial, ChunkManifest, CipherFrame, CipherMessage, CipherMessageHeader, CircuitBufferConfig, CircuitUrls, ClientOptions, ConnectivityOptions, ConnectivityState, DbQueryPage, DehydratedKeypair, DehydratedKeys, DoubleRatchetCipherDeps, ECDHCurve, EncodeResult, EngineHandle, EngineHost, EphemeralEvent, EpochKeyProvider, FileManifest, FileNamespaceDeps, FileStat, FileStorageOptions, FunctionCaps, FunctionConfig, FunctionDeployInput, FunctionInvokeOptions, FunctionScopeOpts, FunctionTrigger, FunctionTriggerType, FunctionTriggers, FunctionUpdateInput, FunctionsNamespaceDeps, Groth16Proof, Hasher, 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, OutSink, PacketCipher, PacketHeaders, PacketOptions, Participant, PeerBlockSource, PeerExchangeOptions, PeerId, PeerNetworkOptions, PeerTransport, 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, SignalEnvelope, SignalKind, SignalingSpace, SnapshotNamespaceDeps, SnarkProof, SpaceChannelLike, SpaceCursor, SpaceDeps, SpaceFileMetadata, SpaceKeyCache, SpaceKeyringDeps, SpaceMessageEvent, SpaceNamespaceDeps, SpaceOfflineAdapter, StorageChangeEvent, StoreName, StoredSession, Subscription, UserIdentity, VaultFactor, VerificationKey, WSMessage, WSMessageType, WSTransportOptions, WebRtcTransportOptions, WrappedPayload, WriteFileInput, WriteFileOptions, WriteToShardsInput, ZkAuthDeps, ZkAuthenticateRequest, ZkChallengeResponse, ZkCompiled, ZkIdentity, ZkRegisterRequest, ZkRegisterResponse };