@rebasepro/client 0.17.3-canary.gdd23447 → 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
package/src/index.ts DELETED
@@ -1,639 +0,0 @@
1
- import { createTransport, RebaseClientConfig } from "./transport";
2
- import { RebaseClientError } from "./errors";
3
- import { createAuth, CreateAuthOptions } from "./auth";
4
- import { createAdmin, CreateAdminOptions } from "./admin";
5
- import { createCron, CreateCronOptions } from "./cron";
6
- import { createBackups } from "./backups";
7
- import { createApiKeys, CreateApiKeysOptions } from "./api-keys";
8
- import { CollectionClient, createCollectionClient } from "./collection";
9
- import { createFunctionsClient } from "./functions";
10
- import { createStorage } from "./storage";
11
- import { ClientStorageSourceRegistry } from "./storage-registry";
12
- import { RebaseWebSocketClient } from "./websocket";
13
- import { RebaseRealtimeChannel, type ChannelOptions } from "./realtime-channel";
14
- import { OfflineManager, type OfflineApi, type OfflineConfig } from "./offline";
15
- import {
16
- DEFAULT_STORAGE_SOURCE_KEY,
17
- InsertOf,
18
- RebaseClient,
19
- RebaseSdkData,
20
- RowOf,
21
- StorageSource,
22
- StorageSourceDefinition,
23
- StorageSourceRegistry,
24
- UpdateOf
25
- } from "@rebasepro/types";
26
- import { toSnakeCase } from "@rebasepro/utils";
27
-
28
- // ─── Public API surface ──────────────────────────────────────────────────────
29
- //
30
- // This barrel is the public API of `@rebasepro/client`. It is an explicit,
31
- // curated list — NOT `export *` — so that adding an export to a module below
32
- // does not silently republish it to app developers. Internal factories
33
- // (`createTransport`, `createAuth`, `createCollectionClient`, …), the raw
34
- // `Transport`, the storage-source registry impl, the JSON reviver, and the
35
- // concrete `SDKQueryBuilder` class are intentionally NOT re-exported: they are
36
- // implementation details of `createRebaseClient()` and have no external
37
- // consumers. App developers reach them through the client instance, never by
38
- // importing the factory. To add something to the public surface, add it here
39
- // deliberately.
40
-
41
- // Errors — the single error type thrown by SDK HTTP calls, plus the
42
- // data-proxy's unknown-collection error.
43
- export { RebaseApiError } from "./transport";
44
- export { RebaseClientError } from "./errors";
45
- // The codes `RebaseApiError.code` carries. An open union — routes add their own
46
- // — so it gives completion on the common ones without pretending to be closed.
47
- export type { RebaseErrorCode } from "@rebasepro/types";
48
-
49
- // Query + collection types (annotate SDK results; construct via the fluent API).
50
- export type { RebaseClientConfig, FindParams, FindResponse } from "./transport";
51
- export type { CollectionClient } from "./collection";
52
- export type { FindResult, SDKCollectionClient, SDKQueryBuilderInterface, PaginationMeta } from "@rebasepro/types";
53
-
54
- // Pagination: `iterate()` / `findAll()` parameter types and the error a walk
55
- // throws instead of quietly returning a truncated answer.
56
- export type { IterateParams, FindAllParams, PageWalkOptions, CursorSpec } from "@rebasepro/types";
57
- export { RebasePaginationError } from "@rebasepro/common";
58
- export type { PaginationErrorCode } from "@rebasepro/common";
59
-
60
- // Logical-condition helpers for `.where(or(...), and(...))`.
61
- export { QueryBuilder, or, and, cond } from "@rebasepro/common";
62
-
63
- // Auth: session/token types, config, and the pluggable storage strategies.
64
- export { createCookieStorage, createMemoryStorage } from "./auth";
65
- export type { AuthConfig, AuthStorage, CookieStorageOptions, CreateAuthOptions } from "./auth";
66
- // `User` is re-exported alongside the session types because `client.auth` hands
67
- // one back and a browser app installs `@rebasepro/client` only — `@rebasepro/types`
68
- // is a transitive dependency there, not something a consumer can import from.
69
- export type { User, RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
70
-
71
- // Control-plane client option/DTO types (the client instance exposes the impls).
72
- export type { CreateAdminOptions } from "./admin";
73
- export type { AdminUser } from "./admin";
74
- export type { CreateCronOptions } from "./cron";
75
- export { createBackups } from "./backups";
76
- export type { CreateBackupsOptions } from "./backups";
77
- export type {
78
- ApiKeyMasked,
79
- ApiKeyPermission,
80
- ApiKeyWithSecret,
81
- CreateApiKeyRequest,
82
- CreateApiKeysOptions,
83
- UpdateApiKeyRequest
84
- } from "./api-keys";
85
- export type { FunctionInvokeOptions, FunctionsClient } from "./functions";
86
-
87
- // Realtime: the WebSocket client class is internal to `createRebaseClient()`,
88
- // but re-exported (see @internal on the class) so a data-source driver can
89
- // construct it directly. Not a stable app-facing API.
90
- export { RebaseWebSocketClient } from "./websocket";
91
- export { RebaseRealtimeChannel } from "./realtime-channel";
92
- export type {
93
- PresenceState,
94
- PresenceDiff,
95
- BroadcastEvent,
96
- ChannelTransport,
97
- ChannelOptions,
98
- ChannelHistoryEntry,
99
- ChannelHistoryResult
100
- } from "./realtime-channel";
101
-
102
- // Offline: config, the `client.offline` surface, and the metadata a UI needs
103
- // to reflect sync state. `isOfflineError` distinguishes "there was no network
104
- // and nothing local to answer with" from a request that genuinely failed.
105
- // The store contract is public so other environments (React Native/
106
- // AsyncStorage, Electron, …) can supply their own persistence;
107
- // `MemoryOfflineStore` is exported for tests and as the reference
108
- // implementation, while the IndexedDB store is wired automatically in the
109
- // browser and needs no direct construction.
110
- export type { OfflineApi, OfflineConfig, OfflineStatus } from "./offline";
111
- export { isOfflineError } from "./offline";
112
- export type { LiveResult, ObserveOptions, RowSnapshotMeta } from "./collection";
113
- export type { OfflineStore, OfflineCacheEntry, OfflineCacheRecord, PendingMutation, MutationRollback } from "./offline-store";
114
- export { MemoryOfflineStore } from "./offline-store";
115
-
116
- export interface CreateRebaseClientOptions extends RebaseClientConfig {
117
- auth?: CreateAuthOptions;
118
- admin?: CreateAdminOptions;
119
- cron?: CreateCronOptions;
120
- apiKeys?: CreateApiKeysOptions;
121
- /**
122
- * Declared storage sources for multi-backend support. Server-transport
123
- * entries are auto-wired into `client.storageRegistry`; `direct` sources
124
- * are registered app-side (e.g. via a Firebase Storage hook). The default
125
- * source (`storage`) is always registered under
126
- * {@link DEFAULT_STORAGE_SOURCE_KEY}.
127
- */
128
- storageSources?: StorageSourceDefinition[];
129
- /**
130
- * Maps camelCase property names / safe identifiers to the actual
131
- * collection slugs on the server (e.g. `{ companyMembers: "company-members" }`).
132
- * If provided, the data layer proxy will resolve property accessors to their
133
- * correct slugs via this map before falling back to automatic snake_casing.
134
- */
135
- collections?: Record<string, string>;
136
- /**
137
- * Local-first sync for the data layer.
138
- *
139
- * `true` enables it with defaults: reads populate a local row database and
140
- * fall back to it (evaluating filters and sorts locally) when the network
141
- * is gone, writes made offline apply immediately and replay in order when
142
- * it returns, and `observe()` becomes a live query that emits from the
143
- * local database first. A rejected write is rolled back. Pass an
144
- * {@link OfflineConfig} to control the store, cache sizes, retry backoff,
145
- * or rejection handling.
146
- *
147
- * Local rows and queued writes are partitioned per signed-in user, and
148
- * shared across tabs. Off by default.
149
- */
150
- offline?: boolean | OfflineConfig;
151
- }
152
-
153
- // ─── Typed Data Proxy ────────────────────────────────────────────────────────
154
- // Adds typed collection accessors when `DB` is provided via the SDK generator.
155
-
156
- type KebabToCamelCase<S extends string> =
157
- S extends `${infer T}-${infer U}`
158
- ? `${T}${Capitalize<KebabToCamelCase<U>>}`
159
- : S;
160
-
161
- // Resolve a generated `Database` entry from a (kebab-case) slug literal,
162
- // or `unknown` when the slug isn't in the schema — the extractors below
163
- // then fall back to the open row / partial shapes.
164
- type DBEntry<DB, S extends string> =
165
- KebabToCamelCase<S> extends keyof DB ? DB[KebabToCamelCase<S>] : unknown;
166
-
167
- type TypedDataLayer<DB> = {
168
- collection<S extends string>(slug: S): CollectionClient<
169
- RowOf<DBEntry<DB, S>>,
170
- InsertOf<DBEntry<DB, S>>,
171
- UpdateOf<DBEntry<DB, S>>
172
- >;
173
- } & {
174
- [K in keyof DB]: CollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>>;
175
- } & RebaseSdkData;
176
-
177
- /**
178
- * The return type of `createRebaseClient<DB>()`.
179
- *
180
- * This is `RebaseClient` (from `@rebasepro/types`) with all optional
181
- * capabilities populated and the `data` layer narrowed to provide
182
- * typed collection accessors when a `DB` schema generic is supplied.
183
- */
184
- export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<RebaseClient<DB>, "data" | "email"> & {
185
- setToken: (token: string | null) => void;
186
- setAuthTokenGetter: (getter: () => Promise<string | null>) => void;
187
- setOnUnauthorized: (handler: () => Promise<boolean>) => void;
188
- resolveToken: () => Promise<string | null>;
189
- auth: ReturnType<typeof createAuth>;
190
- admin: ReturnType<typeof createAdmin>;
191
- cron: ReturnType<typeof createCron>;
192
- backups: ReturnType<typeof createBackups>;
193
- apiKeys: ReturnType<typeof createApiKeys>;
194
- functions: ReturnType<typeof createFunctionsClient>;
195
- ws?: RebaseWebSocketClient;
196
- /**
197
- * Broadcast and presence channels.
198
- *
199
- * Was missing from this type while present on the returned object, which
200
- * made `client.realtime.channel(...)` a type error and forced every adopter
201
- * to cast around the feature before they could reach it.
202
- */
203
- realtime: {
204
- /**
205
- * Join a broadcast/presence channel. Repeated calls with the same name
206
- * return the same channel object. Throws only when the client was
207
- * created with `realtime: false`.
208
- *
209
- * Pass `{ history: true }` to have the channel replay what it missed on
210
- * join and on every reconnect, for channels the server retains.
211
- */
212
- channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
213
- };
214
- /**
215
- * Release everything this client holds that can keep a process alive: the
216
- * realtime socket and its reconnect timer, channel presence heartbeats, the
217
- * offline manager, and the scheduled token refresh.
218
- *
219
- * Each of those keeps the Node event loop alive on its own, so a script
220
- * that does not call this will not exit — and, until the refresh timer was
221
- * included, one that *did* call it still would not if it had signed in.
222
- *
223
- * Safe when realtime was never started (`realtime: false`), safe when
224
- * signed out, and safe to call twice. It does not sign the user out: a
225
- * persisted session survives for the next client to restore.
226
- */
227
- close: () => void;
228
- storage: StorageSource;
229
- storageRegistry: StorageSourceRegistry;
230
- createStorageSource: (storageId: string) => StorageSource;
231
- fetchStorageSources: () => Promise<StorageSourceDefinition[]>;
232
- call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
233
- collection: <M extends Record<string, unknown> = Record<string, unknown>>(slug: string) => CollectionClient<M>;
234
- data: TypedDataLayer<DB>;
235
- /** Present only when the client was created with `offline` enabled. */
236
- offline?: OfflineApi;
237
- };
238
-
239
- // ─── Factory ─────────────────────────────────────────────────────────────────
240
-
241
- /**
242
- * Derive a WebSocket URL from an HTTP base URL.
243
- * `http://` → `ws://`, `https://` → `wss://`.
244
- *
245
- * A backend mounted under a path is the reason `baseUrl` accepts one, so the
246
- * path is kept. It used to be kept for an absolute `baseUrl` and dropped for a
247
- * relative one — resolved through `.origin` — so one deployment dialled two
248
- * different sockets depending on whether its config said `"/backend"` or
249
- * `"https://app.example.com/backend"`.
250
- *
251
- * Returns `""` when there is nothing to resolve against: a relative `baseUrl`
252
- * outside a browser has no origin, and inventing one would dial somewhere
253
- * arbitrary. The caller warns rather than leaving that silent.
254
- */
255
- function deriveWebSocketUrl(baseUrl?: string): string {
256
- const toWsProtocol = (url: string): string => {
257
- const secure = /^(https|wss):/i.test(url);
258
- return url
259
- .replace(/^https?:\/\//i, secure ? "wss://" : "ws://")
260
- .replace(/^wss?:\/\//i, secure ? "wss://" : "ws://")
261
- .replace(/\/$/, "");
262
- };
263
-
264
- if (typeof window !== "undefined") {
265
- let absoluteUrl: string;
266
- if (!baseUrl) {
267
- absoluteUrl = window.location.origin;
268
- } else if (/^https?:\/\//i.test(baseUrl) || /^wss?:\/\//i.test(baseUrl)) {
269
- absoluteUrl = baseUrl;
270
- } else {
271
- try {
272
- const resolved = new URL(baseUrl, window.location.href);
273
- absoluteUrl = resolved.origin + resolved.pathname;
274
- } catch {
275
- absoluteUrl = window.location.origin;
276
- }
277
- }
278
- return toWsProtocol(absoluteUrl);
279
- }
280
-
281
- if (!baseUrl) return "";
282
- if (!/^https?:\/\//i.test(baseUrl) && !/^wss?:\/\//i.test(baseUrl)) {
283
- return "";
284
- }
285
- return toWsProtocol(baseUrl);
286
- }
287
-
288
- export function createRebaseClient<DB = Record<string, unknown>>(options: CreateRebaseClientOptions): CreateRebaseClientResult<DB> {
289
- // `credentialOutOfBand`: in cookie auth mode the credential is an httpOnly
290
- // cookie, so a tokenless transport is not an anonymous client and must not
291
- // trip the server-side anonymous guard (see `RebaseClientConfig.anonymous`).
292
- const transport = createTransport(options, { credentialOutOfBand: options.auth?.authFlowMode === "cookie" });
293
- const auth = createAuth(transport, options.auth);
294
- const admin = createAdmin(transport, options.admin);
295
- const cron = createCron(transport, options.cron);
296
- const backups = createBackups(transport);
297
- const apiKeys = createApiKeys(transport, options.apiKeys);
298
- const storage = createStorage(transport);
299
- const functions = createFunctionsClient(transport);
300
-
301
- // Build a server-backed StorageSource for a given storage-source key.
302
- const createStorageSource = (storageId: string): StorageSource =>
303
- storageId === DEFAULT_STORAGE_SOURCE_KEY ? storage : createStorage(transport, storageId);
304
-
305
- // Storage registry: always holds the default source, plus any declared
306
- // server-transport sources. `direct` sources are registered app-side.
307
- const storageRegistry = new ClientStorageSourceRegistry();
308
- storageRegistry.register(DEFAULT_STORAGE_SOURCE_KEY, storage);
309
- for (const def of options.storageSources ?? []) {
310
- if (def.transport === "server" && def.key !== DEFAULT_STORAGE_SOURCE_KEY) {
311
- storageRegistry.register(def.key, createStorageSource(def.key));
312
- }
313
- }
314
-
315
- // Discover storage sources from the backend, making the server the single
316
- // source of truth. Server-transport sources are auto-wired into the
317
- // registry; `direct` sources are returned for the app to register. The
318
- // promise is cached on success and reset on failure so it can be retried
319
- // (e.g. once the user authenticates).
320
- let storageSourcesPromise: Promise<StorageSourceDefinition[]> | undefined;
321
- const fetchStorageSources = (): Promise<StorageSourceDefinition[]> => {
322
- if (storageSourcesPromise) return storageSourcesPromise;
323
- storageSourcesPromise = transport
324
- .request<{ data: StorageSourceDefinition[] }>("/storage/sources")
325
- .then((res) => {
326
- const defs = res.data ?? [];
327
- for (const def of defs) {
328
- if (def.transport === "server"
329
- && def.key !== DEFAULT_STORAGE_SOURCE_KEY
330
- && !storageRegistry.has(def.key)) {
331
- storageRegistry.register(def.key, createStorageSource(def.key));
332
- }
333
- }
334
- return defs;
335
- })
336
- .catch((e) => {
337
- storageSourcesPromise = undefined; // allow retry
338
- throw e;
339
- });
340
- return storageSourcesPromise;
341
- };
342
-
343
- // Opting out has to happen before the URL is derived: `deriveWebSocketUrl`
344
- // always produces one, so a truthy check alone can never leave the socket
345
- // closed.
346
- const realtimeEnabled = options.realtime !== false;
347
- const resolvedWsUrl = realtimeEnabled
348
- ? (options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl))
349
- : undefined;
350
-
351
- // Realtime is on unless it was switched off, so "on, but no URL could be
352
- // derived" is a misconfiguration and not a choice. It used to be silent:
353
- // the client simply had no socket, `observe()` quietly degraded to a
354
- // one-shot fetch, and `realtime.channel()` blamed `realtime: false` — an
355
- // option the caller had not passed.
356
- const realtimeUnreachable = realtimeEnabled && !resolvedWsUrl;
357
- const unreachableReason =
358
- "no WebSocket URL could be derived from baseUrl " +
359
- `${JSON.stringify(options.baseUrl ?? null)} — outside a browser there is no page origin ` +
360
- "to resolve a relative URL against. Pass an absolute `baseUrl`, set `websocketUrl` " +
361
- "explicitly, or pass `realtime: false` to say this was intended.";
362
- if (realtimeUnreachable) {
363
- console.warn(
364
- `[Rebase] Realtime is enabled but ${unreachableReason} ` +
365
- "Live queries will fall back to a single fetch and channels will throw."
366
- );
367
- }
368
-
369
- let ws: RebaseWebSocketClient | undefined;
370
- /** One channel object per name — see `realtime.channel`. */
371
- const realtimeChannels = new Map<string, RebaseRealtimeChannel>();
372
- if (resolvedWsUrl) {
373
- const wsOnUnauthorized = options.onUnauthorized || (() => auth.handleUnauthorized());
374
-
375
- ws = new RebaseWebSocketClient({
376
- websocketUrl: resolvedWsUrl,
377
- getAuthToken: async () => {
378
- let session = auth.getSession();
379
- if (session && session.expiresAt <= Date.now() + 10000) {
380
- try {
381
- session = await auth.refreshSession();
382
- } catch (e) { /* ignore */ }
383
- }
384
- return session?.accessToken || options.token || "";
385
- },
386
- onUnauthorized: wsOnUnauthorized
387
- });
388
-
389
- auth.onAuthStateChange((event, session) => {
390
- if (!ws) return;
391
- if (event === "SIGNED_OUT") {
392
- // Not permanent: the client stays usable, and a later subscribe
393
- // should reconnect anonymously.
394
- ws.disconnect();
395
- } else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
396
- // Only re-authenticate a socket that already exists. Signing in
397
- // is not a request for realtime, and dialling here would undo
398
- // lazy connect for every app with a login. A socket opened
399
- // later authenticates itself from `getAuthToken` on open.
400
- if (session?.accessToken && ws.hasSocket) {
401
- ws.authenticate(session.accessToken).catch(console.warn);
402
- }
403
- }
404
- });
405
- }
406
-
407
- // Register transport callback for 401s after auth is instantiated.
408
- // IMPORTANT: We must use transport.setOnUnauthorized() here — NOT set
409
- // options.onUnauthorized — because the transport was already created above
410
- // and captured the (undefined) value from the config closure.
411
- if (!options.onUnauthorized) {
412
- // `handleUnauthorized` (not a bare `refreshSession`) so that a refresh
413
- // the server rejects outright drops the session and emits SIGNED_OUT —
414
- // otherwise the app keeps thinking it is signed in and every view just
415
- // renders "Invalid or expired token".
416
- transport.setOnUnauthorized(() => auth.handleUnauthorized());
417
- }
418
-
419
- /**
420
- * Suggest the closest known collection key for a mistyped accessor.
421
- * Uses edit-distance-1 and prefix matching — no external dependency.
422
- */
423
- function suggestCollection(prop: string, knownKeys: string[]): string | undefined {
424
- // Prefix match (e.g. "prod" → "products")
425
- const prefixMatch = knownKeys.find(k => k.startsWith(prop) || prop.startsWith(k));
426
- if (prefixMatch) return prefixMatch;
427
-
428
- // Edit-distance-1: deletions, insertions, substitutions, transpositions
429
- for (const key of knownKeys) {
430
- if (Math.abs(key.length - prop.length) > 1) continue;
431
- let diffs = 0;
432
- const longer = key.length >= prop.length ? key : prop;
433
- const shorter = key.length >= prop.length ? prop : key;
434
- if (longer.length === shorter.length) {
435
- // Same length: allow 1 substitution or 1 transposition
436
- for (let i = 0; i < longer.length; i++) {
437
- if (longer[i] !== shorter[i]) {
438
- // Check for transposition
439
- if (
440
- i + 1 < longer.length &&
441
- longer[i] === shorter[i + 1] &&
442
- longer[i + 1] === shorter[i]
443
- ) {
444
- diffs++;
445
- i++; // skip next char (already accounted for)
446
- if (diffs > 1) break;
447
- continue;
448
- }
449
- diffs++;
450
- }
451
- if (diffs > 1) break;
452
- }
453
- } else {
454
- // Length differs by 1: allow 1 insertion/deletion
455
- let li = 0;
456
- let si = 0;
457
- while (li < longer.length) {
458
- if (si < shorter.length && longer[li] === shorter[si]) {
459
- si++;
460
- } else {
461
- diffs++;
462
- }
463
- li++;
464
- if (diffs > 1) break;
465
- }
466
- }
467
- if (diffs <= 1) return key;
468
- }
469
-
470
- return undefined;
471
- }
472
-
473
- // Offline layer: wraps every collection client with a read cache and a
474
- // write queue. Replay goes through *unwrapped* clients (the factory below)
475
- // so a failing replay can never re-queue itself.
476
- const offlineManager = options.offline
477
- ? new OfflineManager(
478
- typeof options.offline === "object" ? options.offline : {},
479
- (slug) => createCollectionClient(transport, slug)
480
- )
481
- : undefined;
482
-
483
- if (offlineManager) {
484
- // Cache and queue are partitioned per user: cached rows are RLS-scoped
485
- // to whoever fetched them, and queued writes must replay as the user
486
- // who made them — a shared browser must never mix the two.
487
- offlineManager.setScope(auth.getSession()?.user?.uid);
488
- auth.onAuthStateChange((event, session) => {
489
- offlineManager.setScope(event === "SIGNED_OUT" ? undefined : session?.user?.uid);
490
- });
491
- }
492
-
493
- const collectionClients = new Map<string, CollectionClient<Record<string, unknown>>>();
494
- let untypedWarned = false;
495
-
496
- function collection(slug: string): CollectionClient<Record<string, unknown>> {
497
- if (!collectionClients.has(slug)) {
498
- const inner = createCollectionClient(transport, slug, ws);
499
- collectionClients.set(slug, offlineManager ? offlineManager.wrap(slug, inner) : inner);
500
- }
501
- return collectionClients.get(slug)!;
502
- }
503
-
504
- const dataTarget = { collection } as Record<string, unknown>;
505
-
506
- const dataProxy = new Proxy(dataTarget, {
507
- get(_target, prop: string | symbol) {
508
- if (prop === "collection") {
509
- return collection;
510
- }
511
- if (typeof prop === "symbol") return undefined;
512
- if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") {
513
- if (options.collections) {
514
- if (prop in options.collections) {
515
- return collection(options.collections[prop]);
516
- }
517
- // Strict mode: the developer supplied a typed dictionary,
518
- // so we know the full set of valid accessors.
519
- const knownKeys = Object.keys(options.collections);
520
- const suggestion = suggestCollection(prop, knownKeys);
521
- const knownList = knownKeys.join(", ");
522
- let msg = `Unknown collection accessor "${prop}". Known collections: ${knownList}.`;
523
- if (suggestion) msg += ` Did you mean "${suggestion}"?`;
524
- msg += ` Use data.collection("<slug>") for dynamic slugs.`;
525
- throw new RebaseClientError(msg);
526
- }
527
- // Untyped fallback: convert camelCase property names to snake_case slugs.
528
- // e.g. `companyMembers` → `company_members`
529
- if (!untypedWarned) {
530
- untypedWarned = true;
531
- console.warn(
532
- `[Rebase] Untyped data access detected (client.data.${prop}). ` +
533
- `Collection names are resolved via snake_case conversion, which may cause silent 404s at request time. ` +
534
- `Pass a \`collections\` dictionary to createRebaseClient() or use the generated SDK for type-safe access.`
535
- );
536
- }
537
- const slug = toSnakeCase(prop);
538
- return collection(slug);
539
- }
540
- return undefined;
541
- }
542
- });
543
-
544
- const target = {
545
- auth,
546
- admin,
547
- cron,
548
- backups,
549
- apiKeys,
550
- functions,
551
- storage,
552
- storageRegistry,
553
- createStorageSource,
554
- fetchStorageSources,
555
- ws,
556
- realtime: {
557
- /**
558
- * Join a broadcast/presence channel.
559
- *
560
- * Repeated calls with the same name return the same channel, so
561
- * separate components can attach handlers without each opening its
562
- * own membership — and `leave()` from one would otherwise silently
563
- * cut off the others.
564
- */
565
- channel: (name: string, options?: ChannelOptions): RebaseRealtimeChannel => {
566
- // Being merely *unconnected* is not an error: the socket opens
567
- // on the first channel operation, which is the whole point of
568
- // asking for a channel before you use one. Having no socket at
569
- // all is, and there are two reasons for it — say which.
570
- if (!ws) {
571
- throw new RebaseClientError(
572
- realtimeUnreachable
573
- ? `Realtime is enabled but ${unreachableReason}`
574
- : "Realtime is disabled on this client (realtime: false), so channels are unavailable."
575
- );
576
- }
577
- let existing = realtimeChannels.get(name);
578
- if (!existing) {
579
- existing = new RebaseRealtimeChannel(name, ws, options);
580
- realtimeChannels.set(name, existing);
581
- } else if (options?.history) {
582
- // Same object by name, so options on a later call have no
583
- // new channel to apply to. Asking for history upgrades the
584
- // one that exists rather than being quietly ignored — but
585
- // never the reverse, so a caller that omits the option
586
- // cannot switch it off under one that asked for it.
587
- existing.enableHistory();
588
- }
589
- return existing;
590
- }
591
- },
592
- /**
593
- * Release every handle that can keep a process alive — see the
594
- * `close` docblock on the client interface.
595
- *
596
- * Safe to call when realtime was never started, safe when signed out,
597
- * and safe to call twice.
598
- */
599
- close: () => {
600
- // Channels hold presence heartbeat timers, which would otherwise
601
- // keep firing (and keep a Node process alive) after the socket
602
- // they publish over is gone.
603
- for (const channel of realtimeChannels.values()) void channel.leave();
604
- realtimeChannels.clear();
605
- // Permanent: nothing queued afterwards may redial and keep the
606
- // event loop alive, which is the reason this method exists.
607
- ws?.disconnect(true);
608
- // The offline retry timer is unref'd but the `online` listener is
609
- // not, and neither should outlive the client.
610
- offlineManager?.dispose();
611
- // The scheduled token refresh is a plain setTimeout up to a token
612
- // lifetime away, and not unref'd — so on Node it holds the event
613
- // loop open all by itself. Without this, closing a SIGNED-IN client
614
- // released the socket and the process still never exited, which is
615
- // the opposite of what this method exists to guarantee.
616
- auth.stopAutoRefresh();
617
- },
618
- setToken: transport.setToken,
619
- setAuthTokenGetter: transport.setAuthTokenGetter,
620
- setOnUnauthorized: transport.setOnUnauthorized,
621
- resolveToken: transport.resolveToken,
622
- baseUrl: transport.baseUrl,
623
- apiPath: transport.apiPath,
624
- collection,
625
- call: async <T = unknown>(endpoint: string, payload?: unknown): Promise<T> => {
626
- const prefix = endpoint.startsWith("/") ? "" : "/";
627
- const res = await transport.request<{ data: T }>(`${prefix}${endpoint}`, {
628
- method: "POST",
629
- body: payload ? JSON.stringify(payload) : undefined
630
- });
631
- return res.data ?? (res as T);
632
- },
633
- data: dataProxy,
634
- ...(offlineManager ? { offline: offlineManager.api } : {}),
635
- } as unknown as CreateRebaseClientResult<DB>;
636
-
637
- return target;
638
- }
639
-