@mitway/sdk 0.2.4 → 0.3.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/dist/index.d.cts CHANGED
@@ -70,6 +70,7 @@ declare class TokenManager {
70
70
  */
71
71
 
72
72
  type PostgresChangesEventSelector = 'INSERT' | 'UPDATE' | 'DELETE' | '*';
73
+ /** Filter for any event (including the `*` catch-all). */
73
74
  interface PostgresChangesFilter {
74
75
  event: PostgresChangesEventSelector;
75
76
  schema?: string;
@@ -77,8 +78,8 @@ interface PostgresChangesFilter {
77
78
  /** PostgREST-style filter: `column=op.value` or `column=in.(a,b,c)`. */
78
79
  filter?: string;
79
80
  }
80
- interface PostgresChangesPayload<T = Record<string, unknown>> {
81
- type: 'INSERT' | 'UPDATE' | 'DELETE';
81
+ /** Common shape shared by every postgres_changes payload variant. */
82
+ interface PostgresChangesPayloadBase {
82
83
  schema: string;
83
84
  table: string;
84
85
  commit_timestamp: string;
@@ -86,10 +87,33 @@ interface PostgresChangesPayload<T = Record<string, unknown>> {
86
87
  name: string;
87
88
  type: string;
88
89
  }>;
89
- record?: T;
90
- old_record?: T;
91
90
  errors?: string[];
92
91
  }
92
+ /** INSERT: the new row is in `new`. `old` is kept as an empty object so
93
+ * code that accesses both fields never needs optional chaining. */
94
+ interface PostgresChangesInsertPayload<T = Record<string, unknown>> extends PostgresChangesPayloadBase {
95
+ eventType: 'INSERT';
96
+ new: T;
97
+ old: Record<string, never>;
98
+ }
99
+ /** UPDATE: both `new` and `old` are populated. `old` is `Partial<T>`
100
+ * because RLS / REPLICA IDENTITY may strip columns the subscriber can't
101
+ * see. */
102
+ interface PostgresChangesUpdatePayload<T = Record<string, unknown>> extends PostgresChangesPayloadBase {
103
+ eventType: 'UPDATE';
104
+ new: T;
105
+ old: Partial<T>;
106
+ }
107
+ /** DELETE: the deleted row is in `old`. On RLS-enabled tables only
108
+ * primary-key columns are populated. `new` is always an empty object. */
109
+ interface PostgresChangesDeletePayload<T = Record<string, unknown>> extends PostgresChangesPayloadBase {
110
+ eventType: 'DELETE';
111
+ new: Record<string, never>;
112
+ old: Partial<T>;
113
+ }
114
+ /** Discriminated union — what the `'*'` overload of `on('postgres_changes',
115
+ * ...)` passes to the callback. */
116
+ type PostgresChangesPayload<T = Record<string, unknown>> = PostgresChangesInsertPayload<T> | PostgresChangesUpdatePayload<T> | PostgresChangesDeletePayload<T>;
93
117
  interface BroadcastFilter {
94
118
  event: string;
95
119
  }
@@ -140,11 +164,6 @@ interface ChannelOptions {
140
164
  * requested on a private channel. Default: `false` (open topic). */
141
165
  private?: boolean;
142
166
  broadcast?: {
143
- /** When true, `channel.send(...)` resolves only after the server
144
- * ack'd the publish — useful if the app wants to know the
145
- * message_id assigned by the server. Default: `false`
146
- * (fire-and-forget). */
147
- ack?: boolean;
148
167
  /** When `false`, the sending socket is excluded from the fan-out.
149
168
  * Default: `true` (sender also receives its own broadcasts). */
150
169
  self?: boolean;
@@ -158,9 +177,13 @@ interface ChannelOptions {
158
177
  };
159
178
  }
160
179
  type ChannelStatus = 'SUBSCRIBED' | 'CHANNEL_ERROR' | 'TIMED_OUT' | 'CLOSED';
161
- type ChannelStatusCallback = (status: ChannelStatus, error?: {
162
- code: string;
163
- message: string;
180
+ /** Callback invoked for every channel status transition. The second
181
+ * argument is a standard `Error` (so consumers can annotate it as
182
+ * `err: Error` out of the box) with a non-standard `.code` string
183
+ * attached for programmatic discrimination (`SUBSCRIBE_FAILED`,
184
+ * `CONNECT_FAILED`, `REJOIN_FAILED`, …). */
185
+ type ChannelStatusCallback = (status: ChannelStatus, error?: Error & {
186
+ code?: string;
164
187
  }) => void;
165
188
  interface RealtimeOptions {
166
189
  path?: string;
@@ -168,9 +191,10 @@ interface RealtimeOptions {
168
191
  timeoutMs?: number;
169
192
  extraAuth?: Record<string, string>;
170
193
  }
171
- type PostgresChangesCallback<T = Record<string, unknown>> = (payload: PostgresChangesPayload<T>) => void;
172
194
  type BroadcastCallback<T = Record<string, unknown>> = (payload: BroadcastPayload<T>) => void;
173
- type PresenceCallback<T = Record<string, unknown>> = (payload: PresencePayload<T>) => void;
195
+ type PresenceSyncCallback = () => void;
196
+ type PresenceJoinCallback<T = Record<string, unknown>> = (payload: PresenceJoinPayload<T>) => void;
197
+ type PresenceLeaveCallback<T = Record<string, unknown>> = (payload: PresenceLeavePayload<T>) => void;
174
198
  declare class RealtimeChannel {
175
199
  readonly topic: string;
176
200
  private readonly realtime;
@@ -204,9 +228,40 @@ declare class RealtimeChannel {
204
228
  * statusCallback (from the original subscribe()) fires again with
205
229
  * 'SUBSCRIBED' or 'CHANNEL_ERROR' so the app can reflect state. */
206
230
  _rejoinAfterReconnect(): Promise<void>;
207
- on<T = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter, callback: PostgresChangesCallback<T>): this;
231
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
232
+ event: 'INSERT';
233
+ schema?: string;
234
+ table: string;
235
+ filter?: string;
236
+ }, callback: (payload: PostgresChangesInsertPayload<T>) => void): this;
237
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
238
+ event: 'UPDATE';
239
+ schema?: string;
240
+ table: string;
241
+ filter?: string;
242
+ }, callback: (payload: PostgresChangesUpdatePayload<T>) => void): this;
243
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
244
+ event: 'DELETE';
245
+ schema?: string;
246
+ table: string;
247
+ filter?: string;
248
+ }, callback: (payload: PostgresChangesDeletePayload<T>) => void): this;
249
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
250
+ event: '*';
251
+ schema?: string;
252
+ table: string;
253
+ filter?: string;
254
+ }, callback: (payload: PostgresChangesPayload<T>) => void): this;
208
255
  on<T = Record<string, unknown>>(type: 'broadcast', filter: BroadcastFilter, callback: BroadcastCallback<T>): this;
209
- on<T = Record<string, unknown>>(type: 'presence', filter: PresenceFilter, callback: PresenceCallback<T>): this;
256
+ on(type: 'presence', filter: {
257
+ event: 'sync';
258
+ }, callback: PresenceSyncCallback): this;
259
+ on<T = Record<string, unknown>>(type: 'presence', filter: {
260
+ event: 'join';
261
+ }, callback: PresenceJoinCallback<T>): this;
262
+ on<T = Record<string, unknown>>(type: 'presence', filter: {
263
+ event: 'leave';
264
+ }, callback: PresenceLeaveCallback<T>): this;
210
265
  /**
211
266
  * Register or refresh this client's presence entry on the channel. Safe
212
267
  * to call many times — state replaces (not merges). Starts a heartbeat
@@ -248,21 +303,27 @@ declare class RealtimeChannel {
248
303
  * events, write to your own application table and enable
249
304
  * `postgres_changes` on it; the SDK will surface the INSERT as a
250
305
  * `postgres_changes` event without a separate channel.send call.
306
+ *
307
+ * The returned promise always resolves with the server ack, so callers
308
+ * can `await channel.send(...)` to confirm delivery + get the server-
309
+ * assigned `message_id`. There's no performance cost — Socket.IO piggy-
310
+ * backs the ack on the same frame. Callers that don't need it just
311
+ * don't await.
251
312
  */
252
313
  send<T extends Record<string, unknown>>(args: {
253
314
  type: 'broadcast';
254
315
  event: string;
255
316
  payload: T;
256
317
  }): Promise<{
257
- ok: true;
318
+ status: 'ok';
258
319
  message_id: string;
259
320
  } | {
260
- ok: false;
321
+ status: 'error';
261
322
  error: {
262
323
  code: string;
263
324
  message: string;
264
325
  };
265
- } | void>;
326
+ }>;
266
327
  /**
267
328
  * Replay SQL-originated broadcasts on this topic since the given
268
329
  * timestamp. Delivered only to this socket (same envelope format as
@@ -301,13 +362,14 @@ declare class Realtime {
301
362
  * The optional `opts` argument lets the caller configure the channel:
302
363
  * * `config.private` — enable subscribe-side authorization against
303
364
  * `realtime.authorize_subscribe(...)` on the tenant DB.
304
- * * `config.broadcast.ack` — `channel.send()` resolves with the
305
- * server's ack (message_id) instead of fire-and-forget.
306
365
  * * `config.broadcast.self` — `false` excludes the sender from the
307
366
  * fan-out (defaults to `true`).
308
367
  * * `config.presence.key` — stable presence key to group multiple
309
368
  * tabs of the same user under one entry.
310
369
  *
370
+ * `channel.send()` always resolves with the server ack (see its own
371
+ * docstring); there is no separate opt-in needed.
372
+ *
311
373
  * Options are locked in when the channel is first created; subsequent
312
374
  * `.channel('same')` calls with different opts are ignored. Pass a
313
375
  * different topic to get a different-configured channel.
@@ -729,4 +791,4 @@ declare class MitwayBaasClient {
729
791
  */
730
792
  declare function createClient(config: MitwayBaasConfig): MitwayBaasClient;
731
793
 
732
- export { type ApiError, Auth, type AuthRefreshResponse, type AuthResponse, type AuthResult, type AuthSession, type BroadcastFilter, type BroadcastPayload, type ChannelOptions, type ChannelStatus, type ChannelStatusCallback, Database, HttpClient, Logger, MitwayBaasClient, type MitwayBaasConfig, MitwayBaasError, type PostgresChangesEventSelector, type PostgresChangesFilter, type PostgresChangesPayload, type PresenceEventSelector, type PresenceFilter, type PresenceJoinPayload, type PresenceLeavePayload, type PresencePayload, type PresenceState, type PresenceSyncPayload, Realtime, RealtimeChannel, type RealtimeMessageMeta, type RealtimeOptions, type SignInRequest, type SignUpRequest, TokenManager, type User, createClient, MitwayBaasClient as default };
794
+ export { type ApiError, Auth, type AuthRefreshResponse, type AuthResponse, type AuthResult, type AuthSession, type BroadcastFilter, type BroadcastPayload, type ChannelOptions, type ChannelStatus, type ChannelStatusCallback, Database, HttpClient, Logger, MitwayBaasClient, type MitwayBaasConfig, MitwayBaasError, type PostgresChangesDeletePayload, type PostgresChangesEventSelector, type PostgresChangesFilter, type PostgresChangesInsertPayload, type PostgresChangesPayload, type PostgresChangesUpdatePayload, type PresenceEventSelector, type PresenceFilter, type PresenceJoinPayload, type PresenceLeavePayload, type PresencePayload, type PresenceState, type PresenceSyncPayload, Realtime, RealtimeChannel, type RealtimeMessageMeta, type RealtimeOptions, type SignInRequest, type SignUpRequest, TokenManager, type User, createClient, MitwayBaasClient as default };
package/dist/index.d.ts CHANGED
@@ -70,6 +70,7 @@ declare class TokenManager {
70
70
  */
71
71
 
72
72
  type PostgresChangesEventSelector = 'INSERT' | 'UPDATE' | 'DELETE' | '*';
73
+ /** Filter for any event (including the `*` catch-all). */
73
74
  interface PostgresChangesFilter {
74
75
  event: PostgresChangesEventSelector;
75
76
  schema?: string;
@@ -77,8 +78,8 @@ interface PostgresChangesFilter {
77
78
  /** PostgREST-style filter: `column=op.value` or `column=in.(a,b,c)`. */
78
79
  filter?: string;
79
80
  }
80
- interface PostgresChangesPayload<T = Record<string, unknown>> {
81
- type: 'INSERT' | 'UPDATE' | 'DELETE';
81
+ /** Common shape shared by every postgres_changes payload variant. */
82
+ interface PostgresChangesPayloadBase {
82
83
  schema: string;
83
84
  table: string;
84
85
  commit_timestamp: string;
@@ -86,10 +87,33 @@ interface PostgresChangesPayload<T = Record<string, unknown>> {
86
87
  name: string;
87
88
  type: string;
88
89
  }>;
89
- record?: T;
90
- old_record?: T;
91
90
  errors?: string[];
92
91
  }
92
+ /** INSERT: the new row is in `new`. `old` is kept as an empty object so
93
+ * code that accesses both fields never needs optional chaining. */
94
+ interface PostgresChangesInsertPayload<T = Record<string, unknown>> extends PostgresChangesPayloadBase {
95
+ eventType: 'INSERT';
96
+ new: T;
97
+ old: Record<string, never>;
98
+ }
99
+ /** UPDATE: both `new` and `old` are populated. `old` is `Partial<T>`
100
+ * because RLS / REPLICA IDENTITY may strip columns the subscriber can't
101
+ * see. */
102
+ interface PostgresChangesUpdatePayload<T = Record<string, unknown>> extends PostgresChangesPayloadBase {
103
+ eventType: 'UPDATE';
104
+ new: T;
105
+ old: Partial<T>;
106
+ }
107
+ /** DELETE: the deleted row is in `old`. On RLS-enabled tables only
108
+ * primary-key columns are populated. `new` is always an empty object. */
109
+ interface PostgresChangesDeletePayload<T = Record<string, unknown>> extends PostgresChangesPayloadBase {
110
+ eventType: 'DELETE';
111
+ new: Record<string, never>;
112
+ old: Partial<T>;
113
+ }
114
+ /** Discriminated union — what the `'*'` overload of `on('postgres_changes',
115
+ * ...)` passes to the callback. */
116
+ type PostgresChangesPayload<T = Record<string, unknown>> = PostgresChangesInsertPayload<T> | PostgresChangesUpdatePayload<T> | PostgresChangesDeletePayload<T>;
93
117
  interface BroadcastFilter {
94
118
  event: string;
95
119
  }
@@ -140,11 +164,6 @@ interface ChannelOptions {
140
164
  * requested on a private channel. Default: `false` (open topic). */
141
165
  private?: boolean;
142
166
  broadcast?: {
143
- /** When true, `channel.send(...)` resolves only after the server
144
- * ack'd the publish — useful if the app wants to know the
145
- * message_id assigned by the server. Default: `false`
146
- * (fire-and-forget). */
147
- ack?: boolean;
148
167
  /** When `false`, the sending socket is excluded from the fan-out.
149
168
  * Default: `true` (sender also receives its own broadcasts). */
150
169
  self?: boolean;
@@ -158,9 +177,13 @@ interface ChannelOptions {
158
177
  };
159
178
  }
160
179
  type ChannelStatus = 'SUBSCRIBED' | 'CHANNEL_ERROR' | 'TIMED_OUT' | 'CLOSED';
161
- type ChannelStatusCallback = (status: ChannelStatus, error?: {
162
- code: string;
163
- message: string;
180
+ /** Callback invoked for every channel status transition. The second
181
+ * argument is a standard `Error` (so consumers can annotate it as
182
+ * `err: Error` out of the box) with a non-standard `.code` string
183
+ * attached for programmatic discrimination (`SUBSCRIBE_FAILED`,
184
+ * `CONNECT_FAILED`, `REJOIN_FAILED`, …). */
185
+ type ChannelStatusCallback = (status: ChannelStatus, error?: Error & {
186
+ code?: string;
164
187
  }) => void;
165
188
  interface RealtimeOptions {
166
189
  path?: string;
@@ -168,9 +191,10 @@ interface RealtimeOptions {
168
191
  timeoutMs?: number;
169
192
  extraAuth?: Record<string, string>;
170
193
  }
171
- type PostgresChangesCallback<T = Record<string, unknown>> = (payload: PostgresChangesPayload<T>) => void;
172
194
  type BroadcastCallback<T = Record<string, unknown>> = (payload: BroadcastPayload<T>) => void;
173
- type PresenceCallback<T = Record<string, unknown>> = (payload: PresencePayload<T>) => void;
195
+ type PresenceSyncCallback = () => void;
196
+ type PresenceJoinCallback<T = Record<string, unknown>> = (payload: PresenceJoinPayload<T>) => void;
197
+ type PresenceLeaveCallback<T = Record<string, unknown>> = (payload: PresenceLeavePayload<T>) => void;
174
198
  declare class RealtimeChannel {
175
199
  readonly topic: string;
176
200
  private readonly realtime;
@@ -204,9 +228,40 @@ declare class RealtimeChannel {
204
228
  * statusCallback (from the original subscribe()) fires again with
205
229
  * 'SUBSCRIBED' or 'CHANNEL_ERROR' so the app can reflect state. */
206
230
  _rejoinAfterReconnect(): Promise<void>;
207
- on<T = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter, callback: PostgresChangesCallback<T>): this;
231
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
232
+ event: 'INSERT';
233
+ schema?: string;
234
+ table: string;
235
+ filter?: string;
236
+ }, callback: (payload: PostgresChangesInsertPayload<T>) => void): this;
237
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
238
+ event: 'UPDATE';
239
+ schema?: string;
240
+ table: string;
241
+ filter?: string;
242
+ }, callback: (payload: PostgresChangesUpdatePayload<T>) => void): this;
243
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
244
+ event: 'DELETE';
245
+ schema?: string;
246
+ table: string;
247
+ filter?: string;
248
+ }, callback: (payload: PostgresChangesDeletePayload<T>) => void): this;
249
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: {
250
+ event: '*';
251
+ schema?: string;
252
+ table: string;
253
+ filter?: string;
254
+ }, callback: (payload: PostgresChangesPayload<T>) => void): this;
208
255
  on<T = Record<string, unknown>>(type: 'broadcast', filter: BroadcastFilter, callback: BroadcastCallback<T>): this;
209
- on<T = Record<string, unknown>>(type: 'presence', filter: PresenceFilter, callback: PresenceCallback<T>): this;
256
+ on(type: 'presence', filter: {
257
+ event: 'sync';
258
+ }, callback: PresenceSyncCallback): this;
259
+ on<T = Record<string, unknown>>(type: 'presence', filter: {
260
+ event: 'join';
261
+ }, callback: PresenceJoinCallback<T>): this;
262
+ on<T = Record<string, unknown>>(type: 'presence', filter: {
263
+ event: 'leave';
264
+ }, callback: PresenceLeaveCallback<T>): this;
210
265
  /**
211
266
  * Register or refresh this client's presence entry on the channel. Safe
212
267
  * to call many times — state replaces (not merges). Starts a heartbeat
@@ -248,21 +303,27 @@ declare class RealtimeChannel {
248
303
  * events, write to your own application table and enable
249
304
  * `postgres_changes` on it; the SDK will surface the INSERT as a
250
305
  * `postgres_changes` event without a separate channel.send call.
306
+ *
307
+ * The returned promise always resolves with the server ack, so callers
308
+ * can `await channel.send(...)` to confirm delivery + get the server-
309
+ * assigned `message_id`. There's no performance cost — Socket.IO piggy-
310
+ * backs the ack on the same frame. Callers that don't need it just
311
+ * don't await.
251
312
  */
252
313
  send<T extends Record<string, unknown>>(args: {
253
314
  type: 'broadcast';
254
315
  event: string;
255
316
  payload: T;
256
317
  }): Promise<{
257
- ok: true;
318
+ status: 'ok';
258
319
  message_id: string;
259
320
  } | {
260
- ok: false;
321
+ status: 'error';
261
322
  error: {
262
323
  code: string;
263
324
  message: string;
264
325
  };
265
- } | void>;
326
+ }>;
266
327
  /**
267
328
  * Replay SQL-originated broadcasts on this topic since the given
268
329
  * timestamp. Delivered only to this socket (same envelope format as
@@ -301,13 +362,14 @@ declare class Realtime {
301
362
  * The optional `opts` argument lets the caller configure the channel:
302
363
  * * `config.private` — enable subscribe-side authorization against
303
364
  * `realtime.authorize_subscribe(...)` on the tenant DB.
304
- * * `config.broadcast.ack` — `channel.send()` resolves with the
305
- * server's ack (message_id) instead of fire-and-forget.
306
365
  * * `config.broadcast.self` — `false` excludes the sender from the
307
366
  * fan-out (defaults to `true`).
308
367
  * * `config.presence.key` — stable presence key to group multiple
309
368
  * tabs of the same user under one entry.
310
369
  *
370
+ * `channel.send()` always resolves with the server ack (see its own
371
+ * docstring); there is no separate opt-in needed.
372
+ *
311
373
  * Options are locked in when the channel is first created; subsequent
312
374
  * `.channel('same')` calls with different opts are ignored. Pass a
313
375
  * different topic to get a different-configured channel.
@@ -729,4 +791,4 @@ declare class MitwayBaasClient {
729
791
  */
730
792
  declare function createClient(config: MitwayBaasConfig): MitwayBaasClient;
731
793
 
732
- export { type ApiError, Auth, type AuthRefreshResponse, type AuthResponse, type AuthResult, type AuthSession, type BroadcastFilter, type BroadcastPayload, type ChannelOptions, type ChannelStatus, type ChannelStatusCallback, Database, HttpClient, Logger, MitwayBaasClient, type MitwayBaasConfig, MitwayBaasError, type PostgresChangesEventSelector, type PostgresChangesFilter, type PostgresChangesPayload, type PresenceEventSelector, type PresenceFilter, type PresenceJoinPayload, type PresenceLeavePayload, type PresencePayload, type PresenceState, type PresenceSyncPayload, Realtime, RealtimeChannel, type RealtimeMessageMeta, type RealtimeOptions, type SignInRequest, type SignUpRequest, TokenManager, type User, createClient, MitwayBaasClient as default };
794
+ export { type ApiError, Auth, type AuthRefreshResponse, type AuthResponse, type AuthResult, type AuthSession, type BroadcastFilter, type BroadcastPayload, type ChannelOptions, type ChannelStatus, type ChannelStatusCallback, Database, HttpClient, Logger, MitwayBaasClient, type MitwayBaasConfig, MitwayBaasError, type PostgresChangesDeletePayload, type PostgresChangesEventSelector, type PostgresChangesFilter, type PostgresChangesInsertPayload, type PostgresChangesPayload, type PostgresChangesUpdatePayload, type PresenceEventSelector, type PresenceFilter, type PresenceJoinPayload, type PresenceLeavePayload, type PresencePayload, type PresenceState, type PresenceSyncPayload, Realtime, RealtimeChannel, type RealtimeMessageMeta, type RealtimeOptions, type SignInRequest, type SignUpRequest, TokenManager, type User, createClient, MitwayBaasClient as default };
package/dist/index.js CHANGED
@@ -857,6 +857,11 @@ var Database = class {
857
857
  // src/modules/realtime.ts
858
858
  import { io } from "socket.io-client";
859
859
  var PRESENCE_HEARTBEAT_MS = 2e4;
860
+ function makeChannelError(code, message) {
861
+ const err = new Error(message);
862
+ err.code = code;
863
+ return err;
864
+ }
860
865
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
861
866
  var RealtimeChannel = class {
862
867
  constructor(topic, realtime, options = {}) {
@@ -927,12 +932,19 @@ var RealtimeChannel = class {
927
932
  this.statusCallback?.("SUBSCRIBED");
928
933
  } catch (err) {
929
934
  this.state = "errored";
930
- this.statusCallback?.("CHANNEL_ERROR", {
931
- code: "REJOIN_FAILED",
932
- message: err instanceof Error ? err.message : String(err)
933
- });
935
+ this.statusCallback?.(
936
+ "CHANNEL_ERROR",
937
+ makeChannelError("REJOIN_FAILED", err instanceof Error ? err.message : String(err))
938
+ );
934
939
  }
935
940
  }
941
+ // ── implementation signature (not in public type surface).
942
+ // The callback type is intentionally broad: each overload above pins a
943
+ // specific payload shape, but TypeScript overload resolution needs the
944
+ // implementation to accept the union of every narrow callback without
945
+ // the contravariance conflict (TS2394). `any` is the standard escape
946
+ // hatch for this exact pattern and is confined to this one line — the
947
+ // public surface users see is strictly typed via the overloads.
936
948
  on(type, filter, callback) {
937
949
  if (type === "postgres_changes") {
938
950
  this.bindings.push({
@@ -1034,18 +1046,18 @@ var RealtimeChannel = class {
1034
1046
  } catch (err) {
1035
1047
  this.state = "errored";
1036
1048
  const message = err instanceof Error ? err.message : String(err);
1037
- this.statusCallback?.("CHANNEL_ERROR", {
1038
- code: "SUBSCRIBE_FAILED",
1039
- message
1040
- });
1049
+ this.statusCallback?.(
1050
+ "CHANNEL_ERROR",
1051
+ makeChannelError("SUBSCRIBE_FAILED", message)
1052
+ );
1041
1053
  }
1042
1054
  },
1043
1055
  (err) => {
1044
1056
  this.state = "errored";
1045
- this.statusCallback?.("CHANNEL_ERROR", {
1046
- code: "CONNECT_FAILED",
1047
- message: err.message
1048
- });
1057
+ this.statusCallback?.(
1058
+ "CHANNEL_ERROR",
1059
+ makeChannelError("CONNECT_FAILED", err.message)
1060
+ );
1049
1061
  }
1050
1062
  );
1051
1063
  return this;
@@ -1098,6 +1110,12 @@ var RealtimeChannel = class {
1098
1110
  * events, write to your own application table and enable
1099
1111
  * `postgres_changes` on it; the SDK will surface the INSERT as a
1100
1112
  * `postgres_changes` event without a separate channel.send call.
1113
+ *
1114
+ * The returned promise always resolves with the server ack, so callers
1115
+ * can `await channel.send(...)` to confirm delivery + get the server-
1116
+ * assigned `message_id`. There's no performance cost — Socket.IO piggy-
1117
+ * backs the ack on the same frame. Callers that don't need it just
1118
+ * don't await.
1101
1119
  */
1102
1120
  async send(args) {
1103
1121
  if (args.type !== "broadcast") {
@@ -1124,9 +1142,7 @@ var RealtimeChannel = class {
1124
1142
  if (!socket) {
1125
1143
  throw new MitwayBaasError("Socket not connected", 503, "NOT_CONNECTED");
1126
1144
  }
1127
- const broadcastCfg = this.options.config?.broadcast;
1128
- const wantAck = broadcastCfg?.ack === true;
1129
- const self = broadcastCfg?.self;
1145
+ const self = this.options.config?.broadcast?.self;
1130
1146
  const wirePayload = {
1131
1147
  channel: this.topic,
1132
1148
  event: args.event,
@@ -1135,10 +1151,6 @@ var RealtimeChannel = class {
1135
1151
  if (self === false) {
1136
1152
  wirePayload.self = false;
1137
1153
  }
1138
- if (!wantAck) {
1139
- socket.emit("realtime:publish", wirePayload);
1140
- return;
1141
- }
1142
1154
  return await new Promise((resolve) => {
1143
1155
  socket.emit(
1144
1156
  "realtime:publish",
@@ -1179,7 +1191,7 @@ var RealtimeChannel = class {
1179
1191
  if (!b.subscriptionId || !pcEvent.ids.includes(b.subscriptionId)) {
1180
1192
  continue;
1181
1193
  }
1182
- const matchesEvent = b.filter.event === "*" || b.filter.event === pcEvent.data.type;
1194
+ const matchesEvent = b.filter.event === "*" || b.filter.event === pcEvent.data.eventType;
1183
1195
  if (!matchesEvent) {
1184
1196
  continue;
1185
1197
  }
@@ -1238,7 +1250,13 @@ var RealtimeChannel = class {
1238
1250
  continue;
1239
1251
  }
1240
1252
  try {
1241
- b.callback(payload);
1253
+ if (payload.event === "sync") {
1254
+ b.callback();
1255
+ } else if (payload.event === "join") {
1256
+ b.callback(payload);
1257
+ } else {
1258
+ b.callback(payload);
1259
+ }
1242
1260
  } catch {
1243
1261
  }
1244
1262
  }
@@ -1257,7 +1275,7 @@ var RealtimeChannel = class {
1257
1275
  "realtime:subscribe",
1258
1276
  { channel: this.topic, private: this.isPrivate },
1259
1277
  (ack) => {
1260
- if (ack.ok) {
1278
+ if (ack.status === "ok") {
1261
1279
  resolve();
1262
1280
  } else {
1263
1281
  reject(new Error(ack.error?.message ?? "subscribe failed"));
@@ -1281,7 +1299,7 @@ var RealtimeChannel = class {
1281
1299
  filter: b.filter.filter
1282
1300
  },
1283
1301
  (ack) => {
1284
- if (ack.ok && ack.subscription_id) {
1302
+ if (ack.status === "ok" && ack.subscription_id) {
1285
1303
  b.subscriptionId = ack.subscription_id;
1286
1304
  resolve();
1287
1305
  } else {
@@ -1327,13 +1345,14 @@ var Realtime = class {
1327
1345
  * The optional `opts` argument lets the caller configure the channel:
1328
1346
  * * `config.private` — enable subscribe-side authorization against
1329
1347
  * `realtime.authorize_subscribe(...)` on the tenant DB.
1330
- * * `config.broadcast.ack` — `channel.send()` resolves with the
1331
- * server's ack (message_id) instead of fire-and-forget.
1332
1348
  * * `config.broadcast.self` — `false` excludes the sender from the
1333
1349
  * fan-out (defaults to `true`).
1334
1350
  * * `config.presence.key` — stable presence key to group multiple
1335
1351
  * tabs of the same user under one entry.
1336
1352
  *
1353
+ * `channel.send()` always resolves with the server ack (see its own
1354
+ * docstring); there is no separate opt-in needed.
1355
+ *
1337
1356
  * Options are locked in when the channel is first created; subsequent
1338
1357
  * `.channel('same')` calls with different opts are ignored. Pass a
1339
1358
  * different topic to get a different-configured channel.