@mitway/sdk 0.2.3 → 0.2.4

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.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Socket } from 'socket.io-client';
1
2
  import * as _supabase_postgrest_js from '@supabase/postgrest-js';
2
3
 
3
4
  /**
@@ -51,111 +52,277 @@ declare class TokenManager {
51
52
  }
52
53
 
53
54
  /**
54
- * Realtime module — Socket.IO client wrapper for MITWAY-BaaS.
55
+ * Realtime module — Socket.IO client exposing the channel API.
55
56
  *
56
- * Provides a thin, typed layer over `socket.io-client` so app code can
57
- * subscribe / publish / listen without dealing with the underlying
58
- * transport details. The MITWAY-BaaS backend handles auth (JWT / API
59
- * key), RLS, rate limiting, and fan-out across replicas — the SDK just
60
- * opens one socket per `MitwayBaasClient` instance and routes events.
57
+ * Design goals:
58
+ * 1. `client.realtime.channel(topic).on(type, filter, cb).subscribe()` is
59
+ * the single entry point for every primitive.
60
+ * 2. Three binding types coexist on a channel:
61
+ * * `postgres_changes` WAL-based auto-broadcast of DB events,
62
+ * gated by per-subscriber RLS replay in the backend.
63
+ * * `broadcast` — fire-and-forget custom events published
64
+ * via `channel.send({...})` (in-memory fan-out) or via the
65
+ * server-side `realtime.send()` SQL helper.
66
+ * * `presence` — per-channel live "who's here" state.
67
+ * 3. One socket per `Realtime` instance. Multiple channels share it.
68
+ *
69
+ * The wire protocol is defined in the backend's socket.manager.ts.
61
70
  */
62
71
 
63
- /** Standardized meta envelope emitted by the server on every push. */
72
+ type PostgresChangesEventSelector = 'INSERT' | 'UPDATE' | 'DELETE' | '*';
73
+ interface PostgresChangesFilter {
74
+ event: PostgresChangesEventSelector;
75
+ schema?: string;
76
+ table: string;
77
+ /** PostgREST-style filter: `column=op.value` or `column=in.(a,b,c)`. */
78
+ filter?: string;
79
+ }
80
+ interface PostgresChangesPayload<T = Record<string, unknown>> {
81
+ type: 'INSERT' | 'UPDATE' | 'DELETE';
82
+ schema: string;
83
+ table: string;
84
+ commit_timestamp: string;
85
+ columns: Array<{
86
+ name: string;
87
+ type: string;
88
+ }>;
89
+ record?: T;
90
+ old_record?: T;
91
+ errors?: string[];
92
+ }
93
+ interface BroadcastFilter {
94
+ event: string;
95
+ }
96
+ interface BroadcastPayload<T = Record<string, unknown>> {
97
+ type: 'broadcast';
98
+ event: string;
99
+ payload: T;
100
+ }
101
+ /** Presence event selector used with `.on('presence', { event }, cb)`. */
102
+ type PresenceEventSelector = 'sync' | 'join' | 'leave';
103
+ interface PresenceFilter {
104
+ event: PresenceEventSelector;
105
+ }
106
+ /** State map keyed by opaque client key (the socket id on our server side).
107
+ * The client treats these keys as blackboxes — just compare across sync /
108
+ * join / leave deltas to track which participants appeared or departed. */
109
+ type PresenceState<T = Record<string, unknown>> = Record<string, T>;
110
+ interface PresenceSyncPayload<T = Record<string, unknown>> {
111
+ event: 'sync';
112
+ state: PresenceState<T>;
113
+ }
114
+ interface PresenceJoinPayload<T = Record<string, unknown>> {
115
+ event: 'join';
116
+ /** One-entry map `{ [key]: state }` with the new or updated state. */
117
+ joins: PresenceState<T>;
118
+ }
119
+ interface PresenceLeavePayload<T = Record<string, unknown>> {
120
+ event: 'leave';
121
+ /** One-entry map `{ [key]: state }` with the state just before leaving. */
122
+ leaves: PresenceState<T>;
123
+ }
124
+ type PresencePayload<T = Record<string, unknown>> = PresenceSyncPayload<T> | PresenceJoinPayload<T> | PresenceLeavePayload<T>;
64
125
  interface RealtimeMessageMeta {
65
126
  channel?: string;
66
127
  message_id: string;
67
- sender_type: 'system' | 'user';
68
128
  sender_id?: string;
69
129
  timestamp: string;
70
130
  }
71
- /** Subscribe ack returned by the server. */
72
- type SubscribeResult = {
73
- ok: true;
74
- channel: string;
75
- } | {
76
- ok: false;
77
- channel: string;
78
- error: {
79
- code: string;
80
- message: string;
131
+ /**
132
+ * Channel configuration — passed to `client.realtime.channel(topic, opts)`.
133
+ * Every option has a safe default so apps can omit `opts` entirely for the
134
+ * common case.
135
+ */
136
+ interface ChannelOptions {
137
+ config?: {
138
+ /** When true, subscribe is gated on `realtime.authorize_subscribe(role,
139
+ * claims, topic)` at the server side. Replay is also gated if
140
+ * requested on a private channel. Default: `false` (open topic). */
141
+ private?: boolean;
142
+ 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
+ /** When `false`, the sending socket is excluded from the fan-out.
149
+ * Default: `true` (sender also receives its own broadcasts). */
150
+ self?: boolean;
151
+ };
152
+ presence?: {
153
+ /** Optional stable key that replaces the socket id in the presence
154
+ * hash. Useful to group multiple tabs of the same user under one
155
+ * entry. Default: socket id (each tab is a separate entry). */
156
+ key?: string;
157
+ };
81
158
  };
82
- };
83
- /** Server-pushed unsolicited error. */
84
- interface RealtimeErrorPayload {
85
- channel?: string;
159
+ }
160
+ type ChannelStatus = 'SUBSCRIBED' | 'CHANNEL_ERROR' | 'TIMED_OUT' | 'CLOSED';
161
+ type ChannelStatusCallback = (status: ChannelStatus, error?: {
86
162
  code: string;
87
163
  message: string;
88
- }
89
- interface RealtimeListener<T = unknown> {
90
- (payload: T, meta: RealtimeMessageMeta): void;
91
- }
92
- type ConnectionListener = () => void;
93
- type DisconnectListener = (reason: string) => void;
94
- type ConnectErrorListener = (error: Error) => void;
164
+ }) => void;
95
165
  interface RealtimeOptions {
96
- /** Override the path on the server. Defaults to the Socket.IO default. */
97
166
  path?: string;
98
- /** Transport strategy. Defaults to `['websocket']` — modern browsers
99
- * and Node always support it, no need for the long-polling fallback. */
100
167
  transports?: Array<'websocket' | 'polling'>;
101
- /** Handshake timeout in ms. */
102
168
  timeoutMs?: number;
103
- /** Extra fields merged into socket.handshake.auth. Advanced usage. */
104
169
  extraAuth?: Record<string, string>;
105
170
  }
106
- /**
107
- * MITWAY-BaaS realtime client.
108
- *
109
- * Public API mirrors the InsForge realtime SDK for familiarity, but the
110
- * wire protocol follows our backend (see
111
- * `MITWAY-BaaS/backend/src/infra/socket/socket.manager.ts`):
112
- *
113
- * - Handshake auth uses `auth.token` containing the JWT (preferred)
114
- * or an opaque API key string.
115
- * - `realtime:subscribe` / `realtime:unsubscribe` / `realtime:publish`
116
- * are the client-to-server events.
117
- * - Server pushes the user-defined `event` name with `{ ...payload,
118
- * meta }` shape.
119
- * - `realtime:error` is the unsolicited error channel for publish
120
- * failures and similar.
121
- */
171
+ type PostgresChangesCallback<T = Record<string, unknown>> = (payload: PostgresChangesPayload<T>) => void;
172
+ type BroadcastCallback<T = Record<string, unknown>> = (payload: BroadcastPayload<T>) => void;
173
+ type PresenceCallback<T = Record<string, unknown>> = (payload: PresencePayload<T>) => void;
174
+ declare class RealtimeChannel {
175
+ readonly topic: string;
176
+ private readonly realtime;
177
+ private bindings;
178
+ private state;
179
+ private statusCallback;
180
+ /** Local presence state mirror — populated from `presence_state` /
181
+ * `presence_join` / `presence_leave` events. Read via `presenceState()`. */
182
+ private presence;
183
+ /** Latest state this client has tracked. Non-null means the heartbeat
184
+ * timer is active and we'll re-emit this state every TTL/2. */
185
+ private trackedState;
186
+ private presenceHeartbeat;
187
+ /** Timestamp of the most recently received broadcast on this channel —
188
+ * used as the `since` anchor on replay after reconnect. ISO-8601. */
189
+ private lastBroadcastTimestamp;
190
+ /** Configuration from `channel(topic, opts)`. Frozen at construction. */
191
+ private readonly options;
192
+ constructor(topic: string, realtime: Realtime, options?: ChannelOptions);
193
+ /** Whether this channel was opened as `private: true`. */
194
+ private get isPrivate();
195
+ /** The user-supplied presence key, if any. */
196
+ private get presenceKey();
197
+ /** Internal — exposed for Realtime to drive resubscription after a
198
+ * network hiccup. Returns the current lifecycle state. */
199
+ _state(): 'closed' | 'joining' | 'joined' | 'errored';
200
+ /** Internal — called by `Realtime` when Socket.IO reconnects after a
201
+ * drop. Re-runs the registration flow; the backend assigns fresh
202
+ * subscription_ids and Socket.IO rejoins the per-subscription rooms,
203
+ * so events resume without developer intervention. The user-provided
204
+ * statusCallback (from the original subscribe()) fires again with
205
+ * 'SUBSCRIBED' or 'CHANNEL_ERROR' so the app can reflect state. */
206
+ _rejoinAfterReconnect(): Promise<void>;
207
+ on<T = Record<string, unknown>>(type: 'postgres_changes', filter: PostgresChangesFilter, callback: PostgresChangesCallback<T>): this;
208
+ 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;
210
+ /**
211
+ * Register or refresh this client's presence entry on the channel. Safe
212
+ * to call many times — state replaces (not merges). Starts a heartbeat
213
+ * timer at TTL/2 so the entry stays alive while the socket is open.
214
+ * The channel must be `subscribe()`d first (the server enforces this).
215
+ */
216
+ track(state: Record<string, unknown>): Promise<void>;
217
+ /**
218
+ * Remove this client's presence entry immediately, stopping the
219
+ * heartbeat. Safe if the client never called `track`.
220
+ */
221
+ untrack(): void;
222
+ /** Snapshot of the current presence state on this channel. Keys are
223
+ * opaque client identifiers (server-assigned). Re-read inside your
224
+ * `.on('presence', { event: 'sync' }, ...)` handler. */
225
+ presenceState<T = Record<string, unknown>>(): PresenceState<T>;
226
+ /**
227
+ * Register all bindings with the server:
228
+ * * For each `broadcast` binding, ensure we're subscribed to the topic
229
+ * (one `realtime:subscribe` for the channel as a whole).
230
+ * * For each `postgres_changes` binding, emit
231
+ * `realtime:postgres_changes:subscribe` and record the assigned
232
+ * subscription_id.
233
+ *
234
+ * `statusCallback` is invoked with `'SUBSCRIBED'` when every binding
235
+ * has ack'd, or with `'CHANNEL_ERROR' | 'TIMED_OUT'` on failure.
236
+ */
237
+ subscribe(statusCallback?: ChannelStatusCallback): this;
238
+ /**
239
+ * Tear down every binding: unsubscribe from the broadcast topic (if
240
+ * any broadcast bindings exist) and remove every postgres_changes
241
+ * subscription by id. Safe to call when state is already closed.
242
+ */
243
+ unsubscribe(): Promise<void>;
244
+ /**
245
+ * Publish a broadcast event to the topic. Broadcasts are always
246
+ * ephemeral — the server fans out to every subscribed socket in memory,
247
+ * with no DB persistence or webhook fan-out. For audited / durable
248
+ * events, write to your own application table and enable
249
+ * `postgres_changes` on it; the SDK will surface the INSERT as a
250
+ * `postgres_changes` event without a separate channel.send call.
251
+ */
252
+ send<T extends Record<string, unknown>>(args: {
253
+ type: 'broadcast';
254
+ event: string;
255
+ payload: T;
256
+ }): Promise<{
257
+ ok: true;
258
+ message_id: string;
259
+ } | {
260
+ ok: false;
261
+ error: {
262
+ code: string;
263
+ message: string;
264
+ };
265
+ } | void>;
266
+ /**
267
+ * Replay SQL-originated broadcasts on this topic since the given
268
+ * timestamp. Delivered only to this socket (same envelope format as
269
+ * live broadcasts; the SDK routes them through `.on('broadcast', ...)`
270
+ * bindings just like the real-time path). Backend caps the window at
271
+ * 24 h and the limit at 1000.
272
+ */
273
+ replay(args: {
274
+ since: string;
275
+ limit?: number;
276
+ }): Promise<void>;
277
+ /** Internal — called by Realtime's event router on every incoming event. */
278
+ _dispatch(event: string, envelope: Record<string, unknown>): void;
279
+ private firePresence;
280
+ private registerAllBindings;
281
+ }
122
282
  declare class Realtime {
123
283
  private socket;
124
- private baseUrl;
125
- private options;
126
- private anonKey;
127
- private tokenManager;
128
- private listeners;
129
- private reserved;
284
+ private readonly baseUrl;
285
+ private readonly options;
286
+ private readonly anonKey;
287
+ private readonly tokenManager;
288
+ private channels;
130
289
  private connecting;
131
- private subscribedChannels;
290
+ /** Flips to `true` once the initial handshake resolves. Differentiates
291
+ * the first `connect` event (part of `openSocket`) from subsequent
292
+ * reconnect events (which should trigger auto-resubscribe). */
293
+ private firstConnected;
132
294
  constructor(baseUrl: string, tokenManager: TokenManager, anonKey: string | undefined, options?: RealtimeOptions);
133
295
  get isConnected(): boolean;
134
296
  get socketId(): string | undefined;
135
- /** Explicitly open the connection. Safe to call multiple times; only
136
- * opens one socket per instance. Subsequent calls during connection
137
- * return the same in-flight promise. */
297
+ /**
298
+ * Get (or create) a channel for `topic`. Channels are cached so multiple
299
+ * `.channel('same')` calls return the same instance.
300
+ *
301
+ * The optional `opts` argument lets the caller configure the channel:
302
+ * * `config.private` — enable subscribe-side authorization against
303
+ * `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
+ * * `config.broadcast.self` — `false` excludes the sender from the
307
+ * fan-out (defaults to `true`).
308
+ * * `config.presence.key` — stable presence key to group multiple
309
+ * tabs of the same user under one entry.
310
+ *
311
+ * Options are locked in when the channel is first created; subsequent
312
+ * `.channel('same')` calls with different opts are ignored. Pass a
313
+ * different topic to get a different-configured channel.
314
+ */
315
+ channel(topic: string, opts?: ChannelOptions): RealtimeChannel;
138
316
  connect(): Promise<void>;
139
- private openSocket;
140
- /** Close the socket and clear in-memory subscription state. Reserved
141
- * listeners survive so callers can reconnect later via `connect()`. */
317
+ /**
318
+ * Close the socket. Channels are left as-is so they can re-subscribe
319
+ * on the next `connect()` — useful for auth token refresh flows.
320
+ */
142
321
  disconnect(): void;
143
- subscribe(channel: string): Promise<SubscribeResult>;
144
- /** Fire-and-forget. No ack from the server. */
145
- unsubscribe(channel: string): void;
146
- /** Publish via the Socket.IO transport. Subject to RLS INSERT policy
147
- * on `realtime.messages` (disabled by default — the developer must
148
- * add a policy before clients can publish). Returns immediately; any
149
- * server rejection comes through the `error` reserved event. */
150
- publish(channel: string, event: string, payload: Record<string, unknown>): void;
151
- on(event: 'connect', cb: ConnectionListener): void;
152
- on(event: 'disconnect', cb: DisconnectListener): void;
153
- on(event: 'connect_error', cb: ConnectErrorListener): void;
154
- on(event: 'error', cb: RealtimeListener<RealtimeErrorPayload>): void;
155
- on<T = unknown>(event: string, cb: RealtimeListener<T>): void;
156
- off(event: string, cb: (...args: any[]) => void): void;
322
+ _getSocket(): Socket | null;
323
+ _detachChannel(channel: RealtimeChannel): void;
324
+ private openSocket;
157
325
  private dispatch;
158
- private emitReserved;
159
326
  }
160
327
 
161
328
  /**
@@ -548,7 +715,7 @@ declare class MitwayBaasClient {
548
715
  */
549
716
 
550
717
  /**
551
- * Factory function for creating SDK clients (Supabase-style).
718
+ * Factory function for creating SDK clients.
552
719
  *
553
720
  * @example
554
721
  * ```typescript
@@ -562,4 +729,4 @@ declare class MitwayBaasClient {
562
729
  */
563
730
  declare function createClient(config: MitwayBaasConfig): MitwayBaasClient;
564
731
 
565
- export { type ApiError, Auth, type AuthRefreshResponse, type AuthResponse, type AuthResult, type AuthSession, type ConnectErrorListener, type ConnectionListener, Database, type DisconnectListener, HttpClient, Logger, MitwayBaasClient, type MitwayBaasConfig, MitwayBaasError, Realtime, type RealtimeErrorPayload, type RealtimeListener, type RealtimeMessageMeta, type RealtimeOptions, type SignInRequest, type SignUpRequest, type SubscribeResult, TokenManager, type User, createClient, MitwayBaasClient as default };
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 };