@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.
- package/README.md +4 -0
- package/dist/auth.d.ts +80 -0
- package/dist/functions.d.ts +6 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.es.js +471 -94
- package/dist/index.es.js.map +1 -1
- package/dist/offline-connectivity.d.ts +12 -1
- package/dist/offline.d.ts +23 -1
- package/dist/query-contract.types.d.ts +30 -0
- package/dist/realtime-channel.d.ts +29 -1
- package/dist/sdk_query_builder.d.ts +21 -2
- package/dist/transport.d.ts +24 -0
- package/package.json +28 -15
- package/src/admin.ts +0 -90
- package/src/anonymous-client-guard.test.ts +0 -190
- package/src/api-keys.ts +0 -87
- package/src/auth-listener-errors.test.ts +0 -57
- package/src/auth-refresh-overflow.test.ts +0 -89
- package/src/auth.ts +0 -982
- package/src/backups.ts +0 -40
- package/src/client-close.test.ts +0 -80
- package/src/collection-listen-meta.test.ts +0 -105
- package/src/collection-observe.test.ts +0 -138
- package/src/collection.test.ts +0 -293
- package/src/collection.ts +0 -525
- package/src/cron.test.ts +0 -164
- package/src/cron.ts +0 -62
- package/src/data-proxy.test.ts +0 -183
- package/src/errors.ts +0 -9
- package/src/functions.ts +0 -82
- package/src/index.ts +0 -639
- package/src/like-pattern-redos.test.ts +0 -61
- package/src/offline-codec.ts +0 -79
- package/src/offline-connectivity.test.ts +0 -191
- package/src/offline-connectivity.ts +0 -255
- package/src/offline-idb-store.test.ts +0 -340
- package/src/offline-integration.test.ts +0 -180
- package/src/offline-query.test.ts +0 -431
- package/src/offline-query.ts +0 -529
- package/src/offline-store.ts +0 -357
- package/src/offline-sync-engine.test.ts +0 -857
- package/src/offline.test.ts +0 -897
- package/src/offline.ts +0 -1928
- package/src/query-contract.types.ts +0 -206
- package/src/query_builder.ts +0 -1
- package/src/realtime-channel.test.ts +0 -542
- package/src/realtime-channel.ts +0 -539
- package/src/realtime-concurrent-subscribe.test.ts +0 -102
- package/src/realtime-error-surfacing.test.ts +0 -105
- package/src/realtime-optout.test.ts +0 -279
- package/src/realtime-row-identity.test.ts +0 -254
- package/src/realtime-subscription-key.test.ts +0 -92
- package/src/reviver.ts +0 -39
- package/src/sdk_query_builder.ts +0 -206
- package/src/storage-key-encoding.test.ts +0 -65
- package/src/storage-registry.ts +0 -102
- package/src/storage.ts +0 -253
- package/src/transport-baseurl.test.ts +0 -101
- package/src/transport.ts +0 -505
- package/src/vector-search-listen.test.ts +0 -42
- package/src/vector-search-query.test.ts +0 -55
- package/src/websocket-url.test.ts +0 -97
- 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
|
-
|