@rebasepro/server 0.10.0 → 0.10.1-canary.a54c057

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/index.es.js CHANGED
@@ -13,7 +13,7 @@ import path, { join } from "path";
13
13
  import { pathToFileURL } from "url";
14
14
  import { Hono } from "hono";
15
15
  import { promisify } from "util";
16
- import * as crypto from "crypto";
16
+ import * as crypto$1 from "crypto";
17
17
  import { createCipheriv, createDecipheriv, createHash as createHash$1, createHmac, randomBytes as randomBytes$1, randomUUID as randomUUID$1, scrypt, timingSafeEqual } from "crypto";
18
18
  import { bodyLimit } from "hono/body-limit";
19
19
  import { csrf } from "hono/csrf";
@@ -17633,6 +17633,578 @@ var RebaseRealtimeChannel = class {
17633
17633
  }
17634
17634
  };
17635
17635
  /**
17636
+ * In-memory store: the default outside the browser and the workhorse of the
17637
+ * test suite. Values are deep-copied on the way in and out so a caller
17638
+ * mutating a returned row cannot silently edit the "persisted" copy — the
17639
+ * IndexedDB implementation gets the same guarantee for free from structured
17640
+ * cloning, and the two must not differ in aliasing behaviour.
17641
+ */
17642
+ var MemoryOfflineStore = class {
17643
+ cache = /* @__PURE__ */ new Map();
17644
+ queue = /* @__PURE__ */ new Map();
17645
+ async getCache(key) {
17646
+ const entry = this.cache.get(key);
17647
+ return entry ? structuredClone(entry) : void 0;
17648
+ }
17649
+ async setCache(key, entry) {
17650
+ this.cache.set(key, structuredClone(entry));
17651
+ }
17652
+ async deleteCache(keys) {
17653
+ for (const key of keys) this.cache.delete(key);
17654
+ }
17655
+ async listCache(prefix) {
17656
+ const out = [];
17657
+ for (const [key, entry] of this.cache) if (key.startsWith(prefix)) out.push({
17658
+ key,
17659
+ cachedAt: entry.cachedAt
17660
+ });
17661
+ return out;
17662
+ }
17663
+ async enqueue(key, mutation) {
17664
+ this.queue.set(key, structuredClone(mutation));
17665
+ }
17666
+ async dequeue(key) {
17667
+ this.queue.delete(key);
17668
+ }
17669
+ async listQueue(prefix) {
17670
+ return [...this.queue.entries()].filter(([key]) => key.startsWith(prefix)).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, mutation]) => structuredClone(mutation));
17671
+ }
17672
+ async clear(prefix) {
17673
+ for (const key of [...this.cache.keys()]) if (key.startsWith(prefix)) this.cache.delete(key);
17674
+ for (const key of [...this.queue.keys()]) if (key.startsWith(prefix)) this.queue.delete(key);
17675
+ }
17676
+ };
17677
+ var IDB_NAME = "rebase-offline";
17678
+ var IDB_VERSION = 1;
17679
+ var CACHE_STORE = "cache";
17680
+ var QUEUE_STORE = "queue";
17681
+ /** The exclusive upper bound of an IDBKeyRange covering every key under `prefix`. */
17682
+ function prefixRange(prefix) {
17683
+ return IDBKeyRange.bound(prefix, prefix + "￿", false, false);
17684
+ }
17685
+ function requestToPromise(request) {
17686
+ return new Promise((resolve, reject) => {
17687
+ request.onsuccess = () => resolve(request.result);
17688
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB request failed"));
17689
+ });
17690
+ }
17691
+ /**
17692
+ * IndexedDB-backed store — the browser default, so cached reads and queued
17693
+ * writes survive a reload or a browser restart. Everything lives in one
17694
+ * database with two object stores; keys are the manager's full prefixed
17695
+ * strings, so multiple users (scopes) share the database without ever
17696
+ * sharing entries.
17697
+ */
17698
+ var IndexedDBOfflineStore = class {
17699
+ dbPromise;
17700
+ open() {
17701
+ if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
17702
+ const request = indexedDB.open(IDB_NAME, IDB_VERSION);
17703
+ request.onupgradeneeded = () => {
17704
+ const db = request.result;
17705
+ if (!db.objectStoreNames.contains(CACHE_STORE)) db.createObjectStore(CACHE_STORE);
17706
+ if (!db.objectStoreNames.contains(QUEUE_STORE)) db.createObjectStore(QUEUE_STORE);
17707
+ };
17708
+ request.onsuccess = () => resolve(request.result);
17709
+ request.onerror = () => {
17710
+ this.dbPromise = void 0;
17711
+ reject(request.error ?? /* @__PURE__ */ new Error("Failed to open IndexedDB"));
17712
+ };
17713
+ });
17714
+ return this.dbPromise;
17715
+ }
17716
+ async store(name, mode) {
17717
+ return (await this.open()).transaction(name, mode).objectStore(name);
17718
+ }
17719
+ async getCache(key) {
17720
+ return await requestToPromise((await this.store(CACHE_STORE, "readonly")).get(key));
17721
+ }
17722
+ async setCache(key, entry) {
17723
+ await requestToPromise((await this.store(CACHE_STORE, "readwrite")).put(entry, key));
17724
+ }
17725
+ async deleteCache(keys) {
17726
+ if (keys.length === 0) return;
17727
+ const store = await this.store(CACHE_STORE, "readwrite");
17728
+ await Promise.all(keys.map((key) => requestToPromise(store.delete(key))));
17729
+ }
17730
+ async listCache(prefix) {
17731
+ const store = await this.store(CACHE_STORE, "readonly");
17732
+ const [keys, entries] = await Promise.all([requestToPromise(store.getAllKeys(prefixRange(prefix))), requestToPromise(store.getAll(prefixRange(prefix)))]);
17733
+ return keys.map((key, i) => ({
17734
+ key: String(key),
17735
+ cachedAt: entries[i]?.cachedAt ?? 0
17736
+ }));
17737
+ }
17738
+ async enqueue(key, mutation) {
17739
+ await requestToPromise((await this.store(QUEUE_STORE, "readwrite")).put(mutation, key));
17740
+ }
17741
+ async dequeue(key) {
17742
+ await requestToPromise((await this.store(QUEUE_STORE, "readwrite")).delete(key));
17743
+ }
17744
+ async listQueue(prefix) {
17745
+ return await requestToPromise((await this.store(QUEUE_STORE, "readonly")).getAll(prefixRange(prefix)));
17746
+ }
17747
+ async clear(prefix) {
17748
+ await requestToPromise((await this.store(CACHE_STORE, "readwrite")).delete(prefixRange(prefix)));
17749
+ await requestToPromise((await this.store(QUEUE_STORE, "readwrite")).delete(prefixRange(prefix)));
17750
+ }
17751
+ };
17752
+ /** True for "the request never reached the server", false for a server reply. */
17753
+ function isNetworkError(error) {
17754
+ if (error instanceof RebaseApiError) return false;
17755
+ return error instanceof TypeError;
17756
+ }
17757
+ function generateOfflineId() {
17758
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
17759
+ return `off-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
17760
+ }
17761
+ /** A find is "appendable" when queued creates provably belong in its result set. */
17762
+ function isAppendable(params) {
17763
+ return !params?.where && !params?.logical && !params?.searchString;
17764
+ }
17765
+ var OfflineManager = class {
17766
+ store;
17767
+ maxCachedQueries;
17768
+ onSyncError;
17769
+ syncIntervalMs;
17770
+ createInner;
17771
+ inners = /* @__PURE__ */ new Map();
17772
+ scope = "anon";
17773
+ /** In-memory mirror of the current scope's queue, kept in seq order. */
17774
+ queue = [];
17775
+ loadPromise;
17776
+ nextSeq = 1;
17777
+ /** Serializes enqueues so concurrent writes get distinct, ordered seqs. */
17778
+ enqueueChain = Promise.resolve();
17779
+ flushPromise;
17780
+ queueListeners = /* @__PURE__ */ new Set();
17781
+ syncTimer;
17782
+ onOnline = () => {
17783
+ this.sync().catch(() => void 0);
17784
+ };
17785
+ api;
17786
+ constructor(config, createInner) {
17787
+ this.store = config.store ?? (typeof indexedDB !== "undefined" ? new IndexedDBOfflineStore() : new MemoryOfflineStore());
17788
+ this.maxCachedQueries = config.maxCachedQueriesPerCollection ?? 50;
17789
+ this.syncIntervalMs = config.syncIntervalMs ?? 3e4;
17790
+ this.onSyncError = config.onSyncError;
17791
+ this.createInner = createInner;
17792
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") window.addEventListener("online", this.onOnline);
17793
+ this.api = {
17794
+ sync: () => this.sync(),
17795
+ pending: async () => {
17796
+ await this.ensureLoaded();
17797
+ return this.queue.map((m) => structuredClone(m));
17798
+ },
17799
+ clear: async () => {
17800
+ await this.store.clear(`${this.scope}|`);
17801
+ this.queue = [];
17802
+ this.notifyQueue();
17803
+ },
17804
+ onQueueChange: (listener) => {
17805
+ this.queueListeners.add(listener);
17806
+ return () => this.queueListeners.delete(listener);
17807
+ }
17808
+ };
17809
+ }
17810
+ /**
17811
+ * Cache and queue are partitioned per signed-in user: cached rows are
17812
+ * RLS-filtered for the user who fetched them, and queued writes must
17813
+ * replay under the credentials that made them — so neither may ever leak
17814
+ * across a sign-out/sign-in on a shared browser.
17815
+ */
17816
+ setScope(uid) {
17817
+ const next = uid || "anon";
17818
+ if (next === this.scope) return;
17819
+ this.scope = next;
17820
+ this.loadPromise = void 0;
17821
+ this.queue = [];
17822
+ this.sync().catch(() => void 0);
17823
+ }
17824
+ /** Release the online listener and retry timer (client.close()). */
17825
+ dispose() {
17826
+ if (typeof window !== "undefined" && typeof window.removeEventListener === "function") window.removeEventListener("online", this.onOnline);
17827
+ this.stopTimer();
17828
+ }
17829
+ wrap(slug, inner) {
17830
+ this.inners.set(slug, inner);
17831
+ const wrapped = {
17832
+ find: async (params) => {
17833
+ try {
17834
+ const res = await inner.find(params);
17835
+ await this.cacheSet(this.findKey(slug, params), res);
17836
+ return await this.overlayFind(slug, params, res);
17837
+ } catch (error) {
17838
+ if (!isNetworkError(error)) throw error;
17839
+ const cached = await this.cacheGet(this.findKey(slug, params));
17840
+ if (cached === void 0) throw error;
17841
+ return this.overlayFind(slug, params, cached);
17842
+ }
17843
+ },
17844
+ findById: async (id) => {
17845
+ try {
17846
+ const row = await inner.findById(id);
17847
+ if (row !== void 0) await this.cacheSet(this.rowKey(slug, id), row);
17848
+ return await this.overlayRow(slug, id, row);
17849
+ } catch (error) {
17850
+ if (!isNetworkError(error)) throw error;
17851
+ const cached = await this.cacheGet(this.rowKey(slug, id));
17852
+ const { touched, row } = await this.composeRowFromQueue(slug, id, cached);
17853
+ if (!touched && cached === void 0) throw error;
17854
+ return touched ? row : cached;
17855
+ }
17856
+ },
17857
+ create: async (data, id) => {
17858
+ try {
17859
+ return await inner.create(data, id);
17860
+ } catch (error) {
17861
+ if (!isNetworkError(error)) throw error;
17862
+ const providedId = id ?? data.id;
17863
+ const rowId = providedId ?? generateOfflineId();
17864
+ const row = {
17865
+ ...data,
17866
+ id: rowId
17867
+ };
17868
+ await this.enqueue({
17869
+ collection: slug,
17870
+ type: "create",
17871
+ id: rowId,
17872
+ data: row,
17873
+ generatedId: providedId === void 0
17874
+ });
17875
+ await this.cacheSet(this.rowKey(slug, rowId), row);
17876
+ return row;
17877
+ }
17878
+ },
17879
+ createMany: async (data, options) => {
17880
+ try {
17881
+ return await inner.createMany(data, options);
17882
+ } catch (error) {
17883
+ if (!isNetworkError(error)) throw error;
17884
+ if (!Array.isArray(data) || data.length === 0) return [];
17885
+ const rows = data.map((r) => ({
17886
+ ...r,
17887
+ id: r.id ?? generateOfflineId()
17888
+ }));
17889
+ await this.enqueue({
17890
+ collection: slug,
17891
+ type: "createMany",
17892
+ data: rows,
17893
+ upsert: options?.upsert
17894
+ });
17895
+ for (const row of rows) await this.cacheSet(this.rowKey(slug, row.id), row);
17896
+ return rows;
17897
+ }
17898
+ },
17899
+ update: async (id, data) => {
17900
+ try {
17901
+ const row = await inner.update(id, data);
17902
+ await this.cacheSet(this.rowKey(slug, id), row);
17903
+ return row;
17904
+ } catch (error) {
17905
+ if (!isNetworkError(error)) throw error;
17906
+ await this.enqueue({
17907
+ collection: slug,
17908
+ type: "update",
17909
+ id,
17910
+ data
17911
+ });
17912
+ const optimistic = {
17913
+ ...await this.cacheGet(this.rowKey(slug, id)) ?? {},
17914
+ ...data,
17915
+ id
17916
+ };
17917
+ await this.cacheSet(this.rowKey(slug, id), optimistic);
17918
+ return optimistic;
17919
+ }
17920
+ },
17921
+ delete: async (id) => {
17922
+ try {
17923
+ await inner.delete(id);
17924
+ await this.cacheDelete(this.rowKey(slug, id));
17925
+ } catch (error) {
17926
+ if (!isNetworkError(error)) throw error;
17927
+ await this.enqueue({
17928
+ collection: slug,
17929
+ type: "delete",
17930
+ id
17931
+ });
17932
+ await this.cacheDelete(this.rowKey(slug, id));
17933
+ }
17934
+ },
17935
+ count: async (params) => {
17936
+ try {
17937
+ const n = await inner.count(params);
17938
+ await this.cacheSet(this.countKey(slug, params), n);
17939
+ return this.overlayCount(slug, params, n);
17940
+ } catch (error) {
17941
+ if (!isNetworkError(error)) throw error;
17942
+ const cached = await this.cacheGet(this.countKey(slug, params));
17943
+ if (cached === void 0) throw error;
17944
+ return this.overlayCount(slug, params, cached);
17945
+ }
17946
+ },
17947
+ where(columnOrCondition, operator, value) {
17948
+ const builder = new SDKQueryBuilder(wrapped);
17949
+ if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
17950
+ return builder.where(columnOrCondition, operator, value);
17951
+ },
17952
+ orderBy: (column, direction) => new SDKQueryBuilder(wrapped).orderBy(column, direction),
17953
+ limit: (count) => new SDKQueryBuilder(wrapped).limit(count),
17954
+ offset: (count) => new SDKQueryBuilder(wrapped).offset(count),
17955
+ search: (searchString) => new SDKQueryBuilder(wrapped).search(searchString),
17956
+ include: (...relations) => new SDKQueryBuilder(wrapped).include(...relations)
17957
+ };
17958
+ if (inner.listen) wrapped.listen = inner.listen.bind(inner);
17959
+ if (inner.listenById) wrapped.listenById = inner.listenById.bind(inner);
17960
+ return wrapped;
17961
+ }
17962
+ async pendingFor(slug) {
17963
+ await this.ensureLoaded();
17964
+ return this.queue.filter((m) => m.collection === slug);
17965
+ }
17966
+ async overlayFind(slug, params, result) {
17967
+ const ops = await this.pendingFor(slug);
17968
+ if (ops.length === 0) return result;
17969
+ const appendable = isAppendable(params);
17970
+ let rows = result.data.map((r) => ({ ...r }));
17971
+ let total = result.meta.total;
17972
+ const applyCreate = (row) => {
17973
+ if (!appendable) return;
17974
+ if (rows.some((r) => r.id === row.id)) return;
17975
+ rows.push({ ...row });
17976
+ total++;
17977
+ };
17978
+ for (const op of ops) if (op.type === "create") applyCreate(op.data);
17979
+ else if (op.type === "createMany") for (const row of op.data) applyCreate(row);
17980
+ else if (op.type === "update") {
17981
+ const idx = rows.findIndex((r) => r.id === op.id);
17982
+ if (idx >= 0) rows[idx] = {
17983
+ ...rows[idx],
17984
+ ...op.data
17985
+ };
17986
+ } else if (op.type === "delete") {
17987
+ const before = rows.length;
17988
+ rows = rows.filter((r) => r.id !== op.id);
17989
+ if (rows.length < before) total = Math.max(0, total - 1);
17990
+ }
17991
+ return {
17992
+ data: rows,
17993
+ meta: {
17994
+ ...result.meta,
17995
+ total
17996
+ }
17997
+ };
17998
+ }
17999
+ async overlayCount(slug, params, count) {
18000
+ const ops = await this.pendingFor(slug);
18001
+ if (ops.length === 0 || !isAppendable(params)) return count;
18002
+ let n = count;
18003
+ for (const op of ops) if (op.type === "create") n++;
18004
+ else if (op.type === "createMany") n += op.data.length;
18005
+ else if (op.type === "delete") n = Math.max(0, n - 1);
18006
+ return n;
18007
+ }
18008
+ /**
18009
+ * Fold the queued ops for one row into a base value. `touched` separates
18010
+ * "the queue says this row does not exist" (a pending delete → undefined)
18011
+ * from "the queue has nothing to say" — the caller falls back differently.
18012
+ */
18013
+ async composeRowFromQueue(slug, id, base) {
18014
+ const ops = await this.pendingFor(slug);
18015
+ let touched = false;
18016
+ let row = base;
18017
+ for (const op of ops) if (op.type === "create" && op.id === id) {
18018
+ row = { ...op.data };
18019
+ touched = true;
18020
+ } else if (op.type === "createMany") {
18021
+ const match = op.data.find((r) => r.id === id);
18022
+ if (match) {
18023
+ row = { ...match };
18024
+ touched = true;
18025
+ }
18026
+ } else if (op.type === "update" && op.id === id) {
18027
+ row = {
18028
+ ...row ?? {},
18029
+ ...op.data,
18030
+ id
18031
+ };
18032
+ touched = true;
18033
+ } else if (op.type === "delete" && op.id === id) {
18034
+ row = void 0;
18035
+ touched = true;
18036
+ }
18037
+ return {
18038
+ touched,
18039
+ row
18040
+ };
18041
+ }
18042
+ async overlayRow(slug, id, base) {
18043
+ const { touched, row } = await this.composeRowFromQueue(slug, id, base);
18044
+ return touched ? row : base;
18045
+ }
18046
+ ensureLoaded() {
18047
+ if (!this.loadPromise) {
18048
+ const scope = this.scope;
18049
+ this.loadPromise = this.store.listQueue(`${scope}|`).then((queue) => {
18050
+ if (this.scope !== scope) return;
18051
+ this.queue = queue;
18052
+ this.nextSeq = queue.reduce((max, m) => Math.max(max, m.seq), 0) + 1;
18053
+ if (queue.length > 0) this.startTimer();
18054
+ this.notifyQueue();
18055
+ });
18056
+ }
18057
+ return this.loadPromise;
18058
+ }
18059
+ enqueue(mutation) {
18060
+ const result = this.enqueueChain.then(async () => {
18061
+ await this.ensureLoaded();
18062
+ if (mutation.type === "update") {
18063
+ const tail = this.queue[this.queue.length - 1];
18064
+ if (tail && tail.collection === mutation.collection && (tail.type === "create" || tail.type === "update") && tail.id === mutation.id) {
18065
+ tail.data = {
18066
+ ...tail.data,
18067
+ ...mutation.data,
18068
+ id: tail.id
18069
+ };
18070
+ await this.store.enqueue(this.queueKey(tail.seq), tail);
18071
+ return;
18072
+ }
18073
+ }
18074
+ if (mutation.type === "delete") {
18075
+ if (this.queue.some((m) => m.collection === mutation.collection && m.type === "create" && m.id === mutation.id && m.generatedId === true)) {
18076
+ const doomed = this.queue.filter((m) => m.collection === mutation.collection && m.id === mutation.id && (m.type === "create" || m.type === "update"));
18077
+ for (const op of doomed) await this.store.dequeue(this.queueKey(op.seq));
18078
+ this.queue = this.queue.filter((m) => !doomed.includes(m));
18079
+ this.notifyQueue();
18080
+ return;
18081
+ }
18082
+ }
18083
+ const full = {
18084
+ ...mutation,
18085
+ seq: this.nextSeq++,
18086
+ queuedAt: Date.now()
18087
+ };
18088
+ await this.store.enqueue(this.queueKey(full.seq), full);
18089
+ this.queue.push(full);
18090
+ this.startTimer();
18091
+ this.notifyQueue();
18092
+ });
18093
+ this.enqueueChain = result.catch(() => void 0);
18094
+ return result;
18095
+ }
18096
+ sync() {
18097
+ if (this.flushPromise) return this.flushPromise;
18098
+ this.flushPromise = (async () => {
18099
+ await this.ensureLoaded();
18100
+ let flushed = 0;
18101
+ while (this.queue.length > 0) {
18102
+ const op = this.queue[0];
18103
+ try {
18104
+ await this.replay(op);
18105
+ } catch (error) {
18106
+ if (isNetworkError(error)) break;
18107
+ await this.drop(op);
18108
+ this.onSyncError?.(error, op);
18109
+ continue;
18110
+ }
18111
+ await this.drop(op);
18112
+ flushed++;
18113
+ }
18114
+ if (this.queue.length === 0) this.stopTimer();
18115
+ return {
18116
+ flushed,
18117
+ remaining: this.queue.length
18118
+ };
18119
+ })().finally(() => {
18120
+ this.flushPromise = void 0;
18121
+ });
18122
+ return this.flushPromise;
18123
+ }
18124
+ async replay(op) {
18125
+ const inner = this.innerFor(op.collection);
18126
+ if (op.type === "create") {
18127
+ const row = await inner.create(op.data);
18128
+ await this.cacheSet(this.rowKey(op.collection, row.id ?? op.id), row);
18129
+ } else if (op.type === "createMany") await inner.createMany(op.data, op.upsert ? { upsert: true } : void 0);
18130
+ else if (op.type === "update") {
18131
+ const row = await inner.update(op.id, op.data);
18132
+ await this.cacheSet(this.rowKey(op.collection, op.id), row);
18133
+ } else if (op.type === "delete") await inner.delete(op.id);
18134
+ }
18135
+ async drop(op) {
18136
+ await this.store.dequeue(this.queueKey(op.seq));
18137
+ this.queue = this.queue.filter((m) => m.seq !== op.seq);
18138
+ this.notifyQueue();
18139
+ }
18140
+ /** Replay uses unwrapped clients: a failure must never re-enqueue itself. */
18141
+ innerFor(slug) {
18142
+ let inner = this.inners.get(slug);
18143
+ if (!inner) {
18144
+ inner = this.createInner(slug);
18145
+ this.inners.set(slug, inner);
18146
+ }
18147
+ return inner;
18148
+ }
18149
+ notifyQueue() {
18150
+ for (const listener of this.queueListeners) listener(this.queue.length);
18151
+ }
18152
+ startTimer() {
18153
+ if (this.syncTimer || this.syncIntervalMs <= 0) return;
18154
+ this.syncTimer = setInterval(() => {
18155
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return;
18156
+ this.sync().catch(() => void 0);
18157
+ }, this.syncIntervalMs);
18158
+ this.syncTimer.unref?.();
18159
+ }
18160
+ stopTimer() {
18161
+ if (this.syncTimer) {
18162
+ clearInterval(this.syncTimer);
18163
+ this.syncTimer = void 0;
18164
+ }
18165
+ }
18166
+ findKey(slug, params) {
18167
+ return `${this.scope}|find|${slug}|${buildQueryString(params)}`;
18168
+ }
18169
+ countKey(slug, params) {
18170
+ return `${this.scope}|count|${slug}|${buildQueryString(params)}`;
18171
+ }
18172
+ rowKey(slug, id) {
18173
+ return `${this.scope}|row|${slug}|${String(id)}`;
18174
+ }
18175
+ queueKey(seq) {
18176
+ return `${this.scope}|${String(seq).padStart(16, "0")}`;
18177
+ }
18178
+ async cacheGet(key) {
18179
+ try {
18180
+ return (await this.store.getCache(key))?.value;
18181
+ } catch {
18182
+ return;
18183
+ }
18184
+ }
18185
+ async cacheSet(key, value) {
18186
+ try {
18187
+ await this.store.setCache(key, {
18188
+ value,
18189
+ cachedAt: Date.now()
18190
+ });
18191
+ if (key.startsWith(`${this.scope}|find|`)) {
18192
+ const bucket = key.slice(0, key.lastIndexOf("|") + 1);
18193
+ const entries = await this.store.listCache(bucket);
18194
+ if (entries.length > this.maxCachedQueries) {
18195
+ entries.sort((a, b) => a.cachedAt - b.cachedAt);
18196
+ await this.store.deleteCache(entries.slice(0, entries.length - this.maxCachedQueries).map((e) => e.key));
18197
+ }
18198
+ }
18199
+ } catch {}
18200
+ }
18201
+ async cacheDelete(key) {
18202
+ try {
18203
+ await this.store.deleteCache([key]);
18204
+ } catch {}
18205
+ }
18206
+ };
18207
+ /**
17636
18208
  * Derive a WebSocket URL from an HTTP base URL.
17637
18209
  * `http://` → `ws://`, `https://` → `wss://`.
17638
18210
  */
@@ -17755,10 +18327,20 @@ function createRebaseClient(options) {
17755
18327
  if (diffs <= 1) return key;
17756
18328
  }
17757
18329
  }
18330
+ const offlineManager = options.offline ? new OfflineManager(typeof options.offline === "object" ? options.offline : {}, (slug) => createCollectionClient(transport, slug)) : void 0;
18331
+ if (offlineManager) {
18332
+ offlineManager.setScope(auth.getSession()?.user?.uid);
18333
+ auth.onAuthStateChange((event, session) => {
18334
+ offlineManager.setScope(event === "SIGNED_OUT" ? void 0 : session?.user?.uid);
18335
+ });
18336
+ }
17758
18337
  const collectionClients = /* @__PURE__ */ new Map();
17759
18338
  let untypedWarned = false;
17760
18339
  function collection(slug) {
17761
- if (!collectionClients.has(slug)) collectionClients.set(slug, createCollectionClient(transport, slug, ws));
18340
+ if (!collectionClients.has(slug)) {
18341
+ const inner = createCollectionClient(transport, slug, ws);
18342
+ collectionClients.set(slug, offlineManager ? offlineManager.wrap(slug, inner) : inner);
18343
+ }
17762
18344
  return collectionClients.get(slug);
17763
18345
  }
17764
18346
  const dataProxy = new Proxy({ collection }, { get(_target, prop) {
@@ -17822,6 +18404,7 @@ channel: (name, options) => {
17822
18404
  for (const channel of realtimeChannels.values()) channel.leave();
17823
18405
  realtimeChannels.clear();
17824
18406
  ws?.disconnect(true);
18407
+ offlineManager?.dispose();
17825
18408
  },
17826
18409
  setToken: transport.setToken,
17827
18410
  setAuthTokenGetter: transport.setAuthTokenGetter,
@@ -17837,7 +18420,8 @@ channel: (name, options) => {
17837
18420
  });
17838
18421
  return res.data ?? res;
17839
18422
  },
17840
- data: dataProxy
18423
+ data: dataProxy,
18424
+ ...offlineManager ? { offline: offlineManager.api } : {}
17841
18425
  };
17842
18426
  }
17843
18427
  //#endregion
@@ -19779,7 +20363,7 @@ var authJwt = () => {
19779
20363
  * avoids the need for hardcoded dev secrets.
19780
20364
  */
19781
20365
  function generateSecret(bytes = 48) {
19782
- return crypto.randomBytes(bytes).toString("hex");
20366
+ return crypto$1.randomBytes(bytes).toString("hex");
19783
20367
  }
19784
20368
  /**
19785
20369
  * Zod coercion helper: transforms `"true"` → `true`, everything else → `false`.