@rebasepro/client 0.9.0 → 0.9.1-canary.0de22e0

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/src/index.ts CHANGED
@@ -3,12 +3,14 @@ import { RebaseClientError } from "./errors";
3
3
  import { createAuth, CreateAuthOptions } from "./auth";
4
4
  import { createAdmin, CreateAdminOptions } from "./admin";
5
5
  import { createCron, CreateCronOptions } from "./cron";
6
+ import { createBackups } from "./backups";
6
7
  import { createApiKeys, CreateApiKeysOptions } from "./api-keys";
7
8
  import { CollectionClient, createCollectionClient } from "./collection";
8
9
  import { createFunctionsClient } from "./functions";
9
10
  import { createStorage } from "./storage";
10
11
  import { ClientStorageSourceRegistry } from "./storage-registry";
11
12
  import { RebaseWebSocketClient } from "./websocket";
13
+ import { RebaseRealtimeChannel, type ChannelOptions } from "./realtime-channel";
12
14
  import {
13
15
  DEFAULT_STORAGE_SOURCE_KEY,
14
16
  InsertOf,
@@ -59,6 +61,8 @@ export type { RebaseUser, RebaseTokens } from "./auth";
59
61
  export type { CreateAdminOptions } from "./admin";
60
62
  export type { AdminUser } from "./admin";
61
63
  export type { CreateCronOptions } from "./cron";
64
+ export { createBackups } from "./backups";
65
+ export type { CreateBackupsOptions } from "./backups";
62
66
  export type {
63
67
  ApiKeyMasked,
64
68
  ApiKeyPermission,
@@ -70,9 +74,19 @@ export type {
70
74
  export type { FunctionInvokeOptions, FunctionsClient } from "./functions";
71
75
 
72
76
  // Realtime: the WebSocket client class is internal to `createRebaseClient()`,
73
- // but re-exported (see @internal on the class) because the `client-postgresql`
77
+ // but re-exported (see @internal on the class) because the `client-postgres`
74
78
  // driver constructs it directly. Not a stable app-facing API.
75
79
  export { RebaseWebSocketClient } from "./websocket";
80
+ export { RebaseRealtimeChannel } from "./realtime-channel";
81
+ export type {
82
+ PresenceState,
83
+ PresenceDiff,
84
+ BroadcastEvent,
85
+ ChannelTransport,
86
+ ChannelOptions,
87
+ ChannelHistoryEntry,
88
+ ChannelHistoryResult
89
+ } from "./realtime-channel";
76
90
 
77
91
  export interface CreateRebaseClientOptions extends RebaseClientConfig {
78
92
  auth?: CreateAuthOptions;
@@ -135,14 +149,42 @@ export type CreateRebaseClientResult<DB = Record<string, unknown>> = Omit<Rebase
135
149
  auth: ReturnType<typeof createAuth>;
136
150
  admin: ReturnType<typeof createAdmin>;
137
151
  cron: ReturnType<typeof createCron>;
152
+ backups: ReturnType<typeof createBackups>;
138
153
  apiKeys: ReturnType<typeof createApiKeys>;
139
154
  functions: ReturnType<typeof createFunctionsClient>;
140
155
  ws?: RebaseWebSocketClient;
156
+ /**
157
+ * Broadcast and presence channels.
158
+ *
159
+ * Was missing from this type while present on the returned object, which
160
+ * made `client.realtime.channel(...)` a type error and forced every adopter
161
+ * to cast around the feature before they could reach it.
162
+ */
163
+ realtime: {
164
+ /**
165
+ * Join a broadcast/presence channel. Repeated calls with the same name
166
+ * return the same channel object. Throws only when the client was
167
+ * created with `realtime: false`.
168
+ *
169
+ * Pass `{ history: true }` to have the channel replay what it missed on
170
+ * join and on every reconnect, for channels the server retains.
171
+ */
172
+ channel: (name: string, options?: ChannelOptions) => RebaseRealtimeChannel;
173
+ };
174
+ /**
175
+ * Release the realtime socket and its reconnect timer.
176
+ *
177
+ * An open socket keeps the Node event loop alive, so a script that does not
178
+ * call this will not exit on its own. Safe when realtime was never started
179
+ * (`realtime: false`), and safe to call twice.
180
+ */
181
+ close: () => void;
141
182
  storage: StorageSource;
142
183
  storageRegistry: StorageSourceRegistry;
143
184
  createStorageSource: (storageId: string) => StorageSource;
144
185
  fetchStorageSources: () => Promise<StorageSourceDefinition[]>;
145
186
  call: <T = unknown>(endpoint: string, payload?: unknown) => Promise<T>;
187
+ collection: <M extends Record<string, unknown> = Record<string, unknown>>(slug: string) => CollectionClient<M>;
146
188
  data: TypedDataLayer<DB>;
147
189
  };
148
190
 
@@ -187,6 +229,7 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
187
229
  const auth = createAuth(transport, options.auth);
188
230
  const admin = createAdmin(transport, options.admin);
189
231
  const cron = createCron(transport, options.cron);
232
+ const backups = createBackups(transport);
190
233
  const apiKeys = createApiKeys(transport, options.apiKeys);
191
234
  const storage = createStorage(transport);
192
235
  const functions = createFunctionsClient(transport);
@@ -233,9 +276,17 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
233
276
  return storageSourcesPromise;
234
277
  };
235
278
 
236
- const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
279
+ // Opting out has to happen before the URL is derived: `deriveWebSocketUrl`
280
+ // always produces one, so a truthy check alone can never leave the socket
281
+ // closed.
282
+ const realtimeEnabled = options.realtime !== false;
283
+ const resolvedWsUrl = realtimeEnabled
284
+ ? (options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl))
285
+ : undefined;
237
286
 
238
287
  let ws: RebaseWebSocketClient | undefined;
288
+ /** One channel object per name — see `realtime.channel`. */
289
+ const realtimeChannels = new Map<string, RebaseRealtimeChannel>();
239
290
  if (resolvedWsUrl) {
240
291
  const wsOnUnauthorized = options.onUnauthorized || (async () => {
241
292
  try {
@@ -263,9 +314,15 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
263
314
  auth.onAuthStateChange((event, session) => {
264
315
  if (!ws) return;
265
316
  if (event === "SIGNED_OUT") {
317
+ // Not permanent: the client stays usable, and a later subscribe
318
+ // should reconnect anonymously.
266
319
  ws.disconnect();
267
320
  } else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
268
- if (session?.accessToken) {
321
+ // Only re-authenticate a socket that already exists. Signing in
322
+ // is not a request for realtime, and dialling here would undo
323
+ // lazy connect for every app with a login. A socket opened
324
+ // later authenticates itself from `getAuthToken` on open.
325
+ if (session?.accessToken && ws.hasSocket) {
269
326
  ws.authenticate(session.accessToken).catch(console.warn);
270
327
  }
271
328
  }
@@ -395,6 +452,7 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
395
452
  auth,
396
453
  admin,
397
454
  cron,
455
+ backups,
398
456
  apiKeys,
399
457
  functions,
400
458
  storage,
@@ -402,6 +460,57 @@ export function createRebaseClient<DB = Record<string, unknown>>(options: Create
402
460
  createStorageSource,
403
461
  fetchStorageSources,
404
462
  ws,
463
+ realtime: {
464
+ /**
465
+ * Join a broadcast/presence channel.
466
+ *
467
+ * Repeated calls with the same name return the same channel, so
468
+ * separate components can attach handlers without each opening its
469
+ * own membership — and `leave()` from one would otherwise silently
470
+ * cut off the others.
471
+ */
472
+ channel: (name: string, options?: ChannelOptions): RebaseRealtimeChannel => {
473
+ // Only `realtime: false` gets here — a hard opt-out, so this
474
+ // stays an error. Being merely *unconnected* does not: the
475
+ // socket opens on the first channel operation, which is the
476
+ // whole point of asking for a channel before you use one.
477
+ if (!ws) {
478
+ throw new RebaseClientError(
479
+ "Realtime is disabled on this client (realtime: false), so channels are unavailable."
480
+ );
481
+ }
482
+ let existing = realtimeChannels.get(name);
483
+ if (!existing) {
484
+ existing = new RebaseRealtimeChannel(name, ws, options);
485
+ realtimeChannels.set(name, existing);
486
+ } else if (options?.history) {
487
+ // Same object by name, so options on a later call have no
488
+ // new channel to apply to. Asking for history upgrades the
489
+ // one that exists rather than being quietly ignored — but
490
+ // never the reverse, so a caller that omits the option
491
+ // cannot switch it off under one that asked for it.
492
+ existing.enableHistory();
493
+ }
494
+ return existing;
495
+ }
496
+ },
497
+ /**
498
+ * Release the realtime socket and its reconnect timer.
499
+ *
500
+ * Until this returns, the open socket keeps the Node event loop alive
501
+ * and the process will not exit on its own. Safe to call when realtime
502
+ * was never started, and safe to call twice.
503
+ */
504
+ close: () => {
505
+ // Channels hold presence heartbeat timers, which would otherwise
506
+ // keep firing (and keep a Node process alive) after the socket
507
+ // they publish over is gone.
508
+ for (const channel of realtimeChannels.values()) void channel.leave();
509
+ realtimeChannels.clear();
510
+ // Permanent: nothing queued afterwards may redial and keep the
511
+ // event loop alive, which is the reason this method exists.
512
+ ws?.disconnect(true);
513
+ },
405
514
  setToken: transport.setToken,
406
515
  setAuthTokenGetter: transport.setAuthTokenGetter,
407
516
  setOnUnauthorized: transport.setOnUnauthorized,