@rebasepro/client 0.17.3 → 0.18.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.
Files changed (63) hide show
  1. package/README.md +4 -0
  2. package/dist/auth.d.ts +80 -0
  3. package/dist/functions.d.ts +6 -1
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.es.js +471 -94
  6. package/dist/index.es.js.map +1 -1
  7. package/dist/offline-connectivity.d.ts +12 -1
  8. package/dist/offline.d.ts +23 -1
  9. package/dist/query-contract.types.d.ts +30 -0
  10. package/dist/realtime-channel.d.ts +29 -1
  11. package/dist/sdk_query_builder.d.ts +21 -2
  12. package/dist/transport.d.ts +24 -0
  13. package/package.json +28 -15
  14. package/src/admin.ts +0 -90
  15. package/src/anonymous-client-guard.test.ts +0 -190
  16. package/src/api-keys.ts +0 -87
  17. package/src/auth-listener-errors.test.ts +0 -57
  18. package/src/auth-refresh-overflow.test.ts +0 -89
  19. package/src/auth.ts +0 -982
  20. package/src/backups.ts +0 -40
  21. package/src/client-close.test.ts +0 -80
  22. package/src/collection-listen-meta.test.ts +0 -105
  23. package/src/collection-observe.test.ts +0 -138
  24. package/src/collection.test.ts +0 -293
  25. package/src/collection.ts +0 -525
  26. package/src/cron.test.ts +0 -164
  27. package/src/cron.ts +0 -62
  28. package/src/data-proxy.test.ts +0 -183
  29. package/src/errors.ts +0 -9
  30. package/src/functions.ts +0 -82
  31. package/src/index.ts +0 -639
  32. package/src/like-pattern-redos.test.ts +0 -61
  33. package/src/offline-codec.ts +0 -79
  34. package/src/offline-connectivity.test.ts +0 -191
  35. package/src/offline-connectivity.ts +0 -255
  36. package/src/offline-idb-store.test.ts +0 -340
  37. package/src/offline-integration.test.ts +0 -180
  38. package/src/offline-query.test.ts +0 -431
  39. package/src/offline-query.ts +0 -529
  40. package/src/offline-store.ts +0 -357
  41. package/src/offline-sync-engine.test.ts +0 -857
  42. package/src/offline.test.ts +0 -897
  43. package/src/offline.ts +0 -1928
  44. package/src/query-contract.types.ts +0 -206
  45. package/src/query_builder.ts +0 -1
  46. package/src/realtime-channel.test.ts +0 -542
  47. package/src/realtime-channel.ts +0 -539
  48. package/src/realtime-concurrent-subscribe.test.ts +0 -102
  49. package/src/realtime-error-surfacing.test.ts +0 -105
  50. package/src/realtime-optout.test.ts +0 -279
  51. package/src/realtime-row-identity.test.ts +0 -254
  52. package/src/realtime-subscription-key.test.ts +0 -92
  53. package/src/reviver.ts +0 -39
  54. package/src/sdk_query_builder.ts +0 -206
  55. package/src/storage-key-encoding.test.ts +0 -65
  56. package/src/storage-registry.ts +0 -102
  57. package/src/storage.ts +0 -253
  58. package/src/transport-baseurl.test.ts +0 -101
  59. package/src/transport.ts +0 -505
  60. package/src/vector-search-listen.test.ts +0 -42
  61. package/src/vector-search-query.test.ts +0 -55
  62. package/src/websocket-url.test.ts +0 -97
  63. package/src/websocket.ts +0 -1837
@@ -1,357 +0,0 @@
1
- /**
2
- * Persistence backends for the SDK's offline support.
3
- *
4
- * The store is a dumb, namespaced key/value surface with two areas: a read
5
- * cache (normalized rows, query snapshots and sync bookkeeping) and a mutation
6
- * queue (local writes waiting to reach the server). All structure — per-user
7
- * prefixes, the `row|`/`q|`/`meta|` namespaces, mutation ordering — is owned by
8
- * the {@link OfflineManager}; the store only promises that a prefix listing
9
- * comes back in lexicographic key order, which is what makes the queue a FIFO.
10
- *
11
- * Two implementations ship with the SDK:
12
- * - {@link IndexedDBOfflineStore} — the browser default; survives reloads.
13
- * - {@link MemoryOfflineStore} — the fallback everywhere IndexedDB does not
14
- * exist (Node, React Native, tests); survives only the process.
15
- *
16
- * Environments with neither (React Native + AsyncStorage, Electron main, …)
17
- * implement this interface and pass it via `offline.store`.
18
- */
19
-
20
- /** A cached value plus the moment it was written, for LRU eviction. */
21
- export interface OfflineCacheEntry {
22
- value: unknown;
23
- cachedAt: number;
24
- }
25
-
26
- /** A cache entry with its key, as returned by prefix listings. */
27
- export interface OfflineCacheRecord extends OfflineCacheEntry {
28
- key: string;
29
- }
30
-
31
- /** What a mutation has to put back if the server rejects it. */
32
- export interface MutationRollback {
33
- /**
34
- * The rows as they were locally *before* this mutation was applied, keyed
35
- * by id. A `null` value means "the row did not exist" — restoring it is a
36
- * delete, not a write.
37
- */
38
- rows: Record<string, Record<string, unknown> | null>;
39
- }
40
-
41
- /**
42
- * A local write waiting to be replayed against the server.
43
- *
44
- * `mutationId` orders the queue globally (not per collection): a create in one
45
- * collection may be the parent a later insert in another references, so replay
46
- * must preserve the order the app issued the writes in. It is lexicographically
47
- * time-ordered and carries a random suffix, so two browser tabs writing in the
48
- * same millisecond produce distinct, still-roughly-ordered ids instead of
49
- * silently overwriting each other's queue entry.
50
- */
51
- export interface PendingMutation {
52
- /** Unique, lexicographically sortable identity — also the queue key suffix. */
53
- mutationId: string;
54
- collection: string;
55
- type: "create" | "createMany" | "update" | "updateMany" | "delete" | "deleteMany";
56
- /** Target row id for update/delete, and the (client-generated) id of an offline create. */
57
- id?: string | number;
58
- /** Target row ids for `deleteMany`. */
59
- ids?: (string | number)[];
60
- /** `{ id, data }` entries for `updateMany`. */
61
- updates?: { id: string | number; data: Record<string, unknown> }[];
62
- /**
63
- * True when the SDK minted this create's id itself. Only such creates may
64
- * cancel out against a later offline delete: a freshly generated UUID
65
- * cannot name a row the server already has, while a caller-supplied id
66
- * can — and there the delete must still replay to remove the server row.
67
- */
68
- generatedId?: boolean;
69
- /** The payload: a row for create/update, an array of rows for createMany. */
70
- data?: Record<string, unknown> | Record<string, unknown>[];
71
- upsert?: boolean;
72
- queuedAt: number;
73
- /** How many times replay has been attempted (diagnostics for a stuck queue). */
74
- attempts?: number;
75
- /** The last replay failure's message, when there was one. */
76
- lastError?: string;
77
- /** Local state to restore if the server rejects this mutation. */
78
- rollback?: MutationRollback;
79
- }
80
-
81
- export interface OfflineStore {
82
- getCache(key: string): Promise<OfflineCacheEntry | undefined>;
83
- setCache(key: string, entry: OfflineCacheEntry): Promise<void>;
84
- /** Write many entries at once — one transaction where the backend has them. */
85
- setCacheMany(entries: { key: string; entry: OfflineCacheEntry }[]): Promise<void>;
86
- deleteCache(keys: string[]): Promise<void>;
87
- /** Every cache key starting with `prefix`, with its write time (for eviction). */
88
- listCache(prefix: string): Promise<{ key: string; cachedAt: number }[]>;
89
- /** As {@link listCache}, but with the values — the local query engine's input. */
90
- listCacheEntries(prefix: string): Promise<OfflineCacheRecord[]>;
91
-
92
- enqueue(key: string, mutation: PendingMutation): Promise<void>;
93
- dequeue(key: string): Promise<void>;
94
- /** Queued mutations whose key starts with `prefix`, in lexicographic key order. */
95
- listQueue(prefix: string): Promise<PendingMutation[]>;
96
-
97
- /** Remove every cache entry and queued mutation whose key starts with `prefix`. */
98
- clear(prefix: string): Promise<void>;
99
- }
100
-
101
- // ─── Mutation ids ────────────────────────────────────────────────────────────
102
-
103
- /**
104
- * Monotonic within a tab, unique across tabs, and sortable as a plain string:
105
- * `<ms base36, padded>-<counter>-<random>`. The padding is what keeps
106
- * lexicographic order equal to chronological order, and the random suffix is
107
- * what stops two tabs from writing the same queue key in the same millisecond
108
- * — which would silently drop one of the two writes.
109
- */
110
- let mutationCounter = 0;
111
- export function createMutationId(now: number = Date.now()): string {
112
- const time = now.toString(36).padStart(10, "0");
113
- const counter = (mutationCounter = (mutationCounter + 1) % 1_679_616).toString(36).padStart(4, "0");
114
- const random = Math.random().toString(36).slice(2, 10).padStart(8, "0");
115
- return `${time}-${counter}-${random}`;
116
- }
117
-
118
- // ─── Memory ──────────────────────────────────────────────────────────────────
119
-
120
- /**
121
- * In-memory store: the default outside the browser and the workhorse of the
122
- * test suite. Values are deep-copied on the way in and out so a caller
123
- * mutating a returned row cannot silently edit the "persisted" copy — the
124
- * IndexedDB implementation gets the same guarantee for free from structured
125
- * cloning, and the two must not differ in aliasing behaviour.
126
- */
127
- export class MemoryOfflineStore implements OfflineStore {
128
- private cache = new Map<string, OfflineCacheEntry>();
129
- private queue = new Map<string, PendingMutation>();
130
-
131
- async getCache(key: string): Promise<OfflineCacheEntry | undefined> {
132
- const entry = this.cache.get(key);
133
- return entry ? structuredClone(entry) : undefined;
134
- }
135
-
136
- async setCache(key: string, entry: OfflineCacheEntry): Promise<void> {
137
- this.cache.set(key, structuredClone(entry));
138
- }
139
-
140
- async setCacheMany(entries: { key: string; entry: OfflineCacheEntry }[]): Promise<void> {
141
- for (const { key, entry } of entries) this.cache.set(key, structuredClone(entry));
142
- }
143
-
144
- async deleteCache(keys: string[]): Promise<void> {
145
- for (const key of keys) this.cache.delete(key);
146
- }
147
-
148
- async listCache(prefix: string): Promise<{ key: string; cachedAt: number }[]> {
149
- const out: { key: string; cachedAt: number }[] = [];
150
- for (const [key, entry] of this.cache) {
151
- if (key.startsWith(prefix)) out.push({ key, cachedAt: entry.cachedAt });
152
- }
153
- return out;
154
- }
155
-
156
- async listCacheEntries(prefix: string): Promise<OfflineCacheRecord[]> {
157
- const out: OfflineCacheRecord[] = [];
158
- for (const [key, entry] of this.cache) {
159
- if (key.startsWith(prefix)) out.push({ key, ...structuredClone(entry) });
160
- }
161
- out.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
162
- return out;
163
- }
164
-
165
- async enqueue(key: string, mutation: PendingMutation): Promise<void> {
166
- this.queue.set(key, structuredClone(mutation));
167
- }
168
-
169
- async dequeue(key: string): Promise<void> {
170
- this.queue.delete(key);
171
- }
172
-
173
- async listQueue(prefix: string): Promise<PendingMutation[]> {
174
- return [...this.queue.entries()]
175
- .filter(([key]) => key.startsWith(prefix))
176
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
177
- .map(([, mutation]) => structuredClone(mutation));
178
- }
179
-
180
- async clear(prefix: string): Promise<void> {
181
- for (const key of [...this.cache.keys()]) {
182
- if (key.startsWith(prefix)) this.cache.delete(key);
183
- }
184
- for (const key of [...this.queue.keys()]) {
185
- if (key.startsWith(prefix)) this.queue.delete(key);
186
- }
187
- }
188
- }
189
-
190
- // ─── IndexedDB ───────────────────────────────────────────────────────────────
191
-
192
- const IDB_NAME = "rebase-offline";
193
- /**
194
- * v2 introduced the normalized row cache and string mutation ids. A v1
195
- * database holds whole-response blobs under keys this version cannot read and
196
- * queue entries ordered by a numeric `seq` this version no longer writes, so
197
- * the upgrade drops both stores rather than trying to translate them. Offline
198
- * support had not shipped in a release when v2 landed, so nothing in the wild
199
- * loses a queued write to this.
200
- */
201
- const IDB_VERSION = 2;
202
- const CACHE_STORE = "cache";
203
- const QUEUE_STORE = "queue";
204
-
205
- /** The exclusive upper bound of an IDBKeyRange covering every key under `prefix`. */
206
- function prefixRange(prefix: string): IDBKeyRange {
207
- return IDBKeyRange.bound(prefix, prefix + "￿", false, false);
208
- }
209
-
210
- function requestToPromise<T>(request: IDBRequest<T>): Promise<T> {
211
- return new Promise((resolve, reject) => {
212
- request.onsuccess = () => resolve(request.result);
213
- request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
214
- });
215
- }
216
-
217
- /** Resolve when the whole transaction commits, not just when the last request returns. */
218
- function transactionDone(tx: IDBTransaction): Promise<void> {
219
- return new Promise((resolve, reject) => {
220
- tx.oncomplete = () => resolve();
221
- tx.onabort = tx.onerror = () => reject(tx.error ?? new Error("IndexedDB transaction failed"));
222
- });
223
- }
224
-
225
- /**
226
- * IndexedDB-backed store — the browser default, so cached rows and queued
227
- * writes survive a reload or a browser restart. Everything lives in one
228
- * database with two object stores; keys are the manager's full prefixed
229
- * strings, so multiple users (scopes) share the database without ever
230
- * sharing entries.
231
- */
232
- export class IndexedDBOfflineStore implements OfflineStore {
233
- private dbPromise?: Promise<IDBDatabase>;
234
-
235
- private open(): Promise<IDBDatabase> {
236
- if (!this.dbPromise) {
237
- this.dbPromise = new Promise((resolve, reject) => {
238
- const request = indexedDB.open(IDB_NAME, IDB_VERSION);
239
- request.onupgradeneeded = (event) => {
240
- const db = request.result;
241
- // A v1 database speaks a key layout this version cannot
242
- // read; keeping it would surface as corrupt cache entries
243
- // and un-replayable mutations. Start clean instead.
244
- if (event.oldVersion > 0 && event.oldVersion < 2) {
245
- if (db.objectStoreNames.contains(CACHE_STORE)) db.deleteObjectStore(CACHE_STORE);
246
- if (db.objectStoreNames.contains(QUEUE_STORE)) db.deleteObjectStore(QUEUE_STORE);
247
- }
248
- if (!db.objectStoreNames.contains(CACHE_STORE)) db.createObjectStore(CACHE_STORE);
249
- if (!db.objectStoreNames.contains(QUEUE_STORE)) db.createObjectStore(QUEUE_STORE);
250
- };
251
- request.onsuccess = () => {
252
- const db = request.result;
253
- // Another tab asking for a newer version needs this
254
- // connection out of the way, or its upgrade blocks forever.
255
- db.onversionchange = () => {
256
- db.close();
257
- this.dbPromise = undefined;
258
- };
259
- resolve(db);
260
- };
261
- // Reset so a transient failure (private browsing quota, a
262
- // version race with another tab) can be retried instead of
263
- // poisoning every later call with the same rejection.
264
- request.onerror = () => {
265
- this.dbPromise = undefined;
266
- reject(request.error ?? new Error("Failed to open IndexedDB"));
267
- };
268
- request.onblocked = () => {
269
- this.dbPromise = undefined;
270
- reject(new Error("IndexedDB upgrade blocked by another tab"));
271
- };
272
- });
273
- }
274
- return this.dbPromise;
275
- }
276
-
277
- private async store(name: string, mode: IDBTransactionMode): Promise<IDBObjectStore> {
278
- const db = await this.open();
279
- return db.transaction(name, mode).objectStore(name);
280
- }
281
-
282
- async getCache(key: string): Promise<OfflineCacheEntry | undefined> {
283
- const store = await this.store(CACHE_STORE, "readonly");
284
- const entry = await requestToPromise(store.get(key));
285
- return entry as OfflineCacheEntry | undefined;
286
- }
287
-
288
- async setCache(key: string, entry: OfflineCacheEntry): Promise<void> {
289
- const store = await this.store(CACHE_STORE, "readwrite");
290
- await requestToPromise(store.put(entry, key));
291
- }
292
-
293
- async setCacheMany(entries: { key: string; entry: OfflineCacheEntry }[]): Promise<void> {
294
- if (entries.length === 0) return;
295
- const store = await this.store(CACHE_STORE, "readwrite");
296
- for (const { key, entry } of entries) store.put(entry, key);
297
- // One commit for the whole batch: a `find` writing 200 rows must not
298
- // be 200 round-trips through the transaction queue.
299
- await transactionDone(store.transaction);
300
- }
301
-
302
- async deleteCache(keys: string[]): Promise<void> {
303
- if (keys.length === 0) return;
304
- const store = await this.store(CACHE_STORE, "readwrite");
305
- for (const key of keys) store.delete(key);
306
- await transactionDone(store.transaction);
307
- }
308
-
309
- async listCache(prefix: string): Promise<{ key: string; cachedAt: number }[]> {
310
- const store = await this.store(CACHE_STORE, "readonly");
311
- const [keys, entries] = await Promise.all([
312
- requestToPromise(store.getAllKeys(prefixRange(prefix))),
313
- requestToPromise(store.getAll(prefixRange(prefix)))
314
- ]);
315
- return keys.map((key, i) => ({
316
- key: String(key),
317
- cachedAt: (entries[i] as OfflineCacheEntry)?.cachedAt ?? 0
318
- }));
319
- }
320
-
321
- async listCacheEntries(prefix: string): Promise<OfflineCacheRecord[]> {
322
- const store = await this.store(CACHE_STORE, "readonly");
323
- const [keys, entries] = await Promise.all([
324
- requestToPromise(store.getAllKeys(prefixRange(prefix))),
325
- requestToPromise(store.getAll(prefixRange(prefix)))
326
- ]);
327
- return keys.map((key, i) => {
328
- const entry = entries[i] as OfflineCacheEntry | undefined;
329
- return { key: String(key), value: entry?.value, cachedAt: entry?.cachedAt ?? 0 };
330
- });
331
- }
332
-
333
- async enqueue(key: string, mutation: PendingMutation): Promise<void> {
334
- const store = await this.store(QUEUE_STORE, "readwrite");
335
- await requestToPromise(store.put(mutation, key));
336
- }
337
-
338
- async dequeue(key: string): Promise<void> {
339
- const store = await this.store(QUEUE_STORE, "readwrite");
340
- await requestToPromise(store.delete(key));
341
- }
342
-
343
- async listQueue(prefix: string): Promise<PendingMutation[]> {
344
- const store = await this.store(QUEUE_STORE, "readonly");
345
- // getAll on a key range returns values in key order, which is the
346
- // FIFO guarantee this interface promises.
347
- const entries = await requestToPromise(store.getAll(prefixRange(prefix)));
348
- return entries as PendingMutation[];
349
- }
350
-
351
- async clear(prefix: string): Promise<void> {
352
- const cache = await this.store(CACHE_STORE, "readwrite");
353
- await requestToPromise(cache.delete(prefixRange(prefix)));
354
- const queue = await this.store(QUEUE_STORE, "readwrite");
355
- await requestToPromise(queue.delete(prefixRange(prefix)));
356
- }
357
- }