@revenexx/sdk 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/cjs/package.json +3 -0
  4. package/dist/cjs/sdk.js +16340 -0
  5. package/dist/cjs/sdk.js.map +1 -0
  6. package/dist/esm/package.json +3 -0
  7. package/dist/esm/sdk.js +16250 -0
  8. package/dist/esm/sdk.js.map +1 -0
  9. package/dist/iife/sdk.js +20101 -0
  10. package/package.json +56 -0
  11. package/src/channel.ts +158 -0
  12. package/src/client.ts +950 -0
  13. package/src/enums/adapter.ts +4 -0
  14. package/src/enums/attribute-boolean-status.ts +7 -0
  15. package/src/enums/attribute-datetime-status.ts +7 -0
  16. package/src/enums/attribute-email-status.ts +7 -0
  17. package/src/enums/attribute-enum-status.ts +7 -0
  18. package/src/enums/attribute-float-status.ts +7 -0
  19. package/src/enums/attribute-integer-status.ts +7 -0
  20. package/src/enums/attribute-ip-status.ts +7 -0
  21. package/src/enums/attribute-line-status.ts +7 -0
  22. package/src/enums/attribute-longtext-status.ts +7 -0
  23. package/src/enums/attribute-mediumtext-status.ts +7 -0
  24. package/src/enums/attribute-point-status.ts +7 -0
  25. package/src/enums/attribute-polygon-status.ts +7 -0
  26. package/src/enums/attribute-relationship-status.ts +7 -0
  27. package/src/enums/attribute-string-status.ts +7 -0
  28. package/src/enums/attribute-text-status.ts +7 -0
  29. package/src/enums/attribute-url-status.ts +7 -0
  30. package/src/enums/attribute-varchar-status.ts +7 -0
  31. package/src/enums/build-runtime.ts +73 -0
  32. package/src/enums/code.ts +16 -0
  33. package/src/enums/collection.ts +4 -0
  34. package/src/enums/column-boolean-status.ts +7 -0
  35. package/src/enums/column-datetime-status.ts +7 -0
  36. package/src/enums/column-email-status.ts +7 -0
  37. package/src/enums/column-enum-status.ts +7 -0
  38. package/src/enums/column-float-status.ts +7 -0
  39. package/src/enums/column-integer-status.ts +7 -0
  40. package/src/enums/column-ip-status.ts +7 -0
  41. package/src/enums/column-line-status.ts +7 -0
  42. package/src/enums/column-longtext-status.ts +7 -0
  43. package/src/enums/column-mediumtext-status.ts +7 -0
  44. package/src/enums/column-point-status.ts +7 -0
  45. package/src/enums/column-polygon-status.ts +7 -0
  46. package/src/enums/column-relationship-status.ts +7 -0
  47. package/src/enums/column-string-status.ts +7 -0
  48. package/src/enums/column-text-status.ts +7 -0
  49. package/src/enums/column-url-status.ts +7 -0
  50. package/src/enums/column-varchar-status.ts +7 -0
  51. package/src/enums/compression.ts +5 -0
  52. package/src/enums/database-type.ts +4 -0
  53. package/src/enums/deployment-status.ts +8 -0
  54. package/src/enums/execution-status.ts +7 -0
  55. package/src/enums/execution-trigger.ts +5 -0
  56. package/src/enums/framework.ts +17 -0
  57. package/src/enums/gravity.ts +11 -0
  58. package/src/enums/health-antivirus-status.ts +5 -0
  59. package/src/enums/health-status-status.ts +4 -0
  60. package/src/enums/index-status.ts +7 -0
  61. package/src/enums/message-status.ts +7 -0
  62. package/src/enums/method.ts +9 -0
  63. package/src/enums/output.ts +9 -0
  64. package/src/enums/permissions.ts +22 -0
  65. package/src/enums/priority.ts +4 -0
  66. package/src/enums/range.ts +5 -0
  67. package/src/enums/runtime.ts +73 -0
  68. package/src/enums/runtimes.ts +73 -0
  69. package/src/enums/scopes.ts +57 -0
  70. package/src/enums/theme.ts +4 -0
  71. package/src/enums/timezone.ts +421 -0
  72. package/src/enums/type.ts +5 -0
  73. package/src/enums/use-cases.ts +9 -0
  74. package/src/id.ts +47 -0
  75. package/src/index.ts +92 -0
  76. package/src/models.ts +6013 -0
  77. package/src/operator.ts +308 -0
  78. package/src/permission.ts +57 -0
  79. package/src/query.ts +576 -0
  80. package/src/role.ts +100 -0
  81. package/src/service.ts +30 -0
  82. package/src/services/apps.ts +2473 -0
  83. package/src/services/avatars.ts +744 -0
  84. package/src/services/carts.ts +1057 -0
  85. package/src/services/channels.ts +227 -0
  86. package/src/services/customers.ts +729 -0
  87. package/src/services/greetings.ts +294 -0
  88. package/src/services/locale.ts +198 -0
  89. package/src/services/markets.ts +796 -0
  90. package/src/services/messaging.ts +3463 -0
  91. package/src/services/products.ts +3100 -0
  92. package/src/services/realtime.ts +537 -0
  93. package/src/services/search.ts +346 -0
  94. package/src/services/sites.ts +1847 -0
  95. package/src/services/storage.ts +1076 -0
  96. package/src/services/tokens.ts +314 -0
  97. package/types/channel.d.ts +74 -0
  98. package/types/client.d.ts +211 -0
  99. package/types/enums/adapter.d.ts +4 -0
  100. package/types/enums/attribute-boolean-status.d.ts +7 -0
  101. package/types/enums/attribute-datetime-status.d.ts +7 -0
  102. package/types/enums/attribute-email-status.d.ts +7 -0
  103. package/types/enums/attribute-enum-status.d.ts +7 -0
  104. package/types/enums/attribute-float-status.d.ts +7 -0
  105. package/types/enums/attribute-integer-status.d.ts +7 -0
  106. package/types/enums/attribute-ip-status.d.ts +7 -0
  107. package/types/enums/attribute-line-status.d.ts +7 -0
  108. package/types/enums/attribute-longtext-status.d.ts +7 -0
  109. package/types/enums/attribute-mediumtext-status.d.ts +7 -0
  110. package/types/enums/attribute-point-status.d.ts +7 -0
  111. package/types/enums/attribute-polygon-status.d.ts +7 -0
  112. package/types/enums/attribute-relationship-status.d.ts +7 -0
  113. package/types/enums/attribute-string-status.d.ts +7 -0
  114. package/types/enums/attribute-text-status.d.ts +7 -0
  115. package/types/enums/attribute-url-status.d.ts +7 -0
  116. package/types/enums/attribute-varchar-status.d.ts +7 -0
  117. package/types/enums/build-runtime.d.ts +73 -0
  118. package/types/enums/code.d.ts +16 -0
  119. package/types/enums/collection.d.ts +4 -0
  120. package/types/enums/column-boolean-status.d.ts +7 -0
  121. package/types/enums/column-datetime-status.d.ts +7 -0
  122. package/types/enums/column-email-status.d.ts +7 -0
  123. package/types/enums/column-enum-status.d.ts +7 -0
  124. package/types/enums/column-float-status.d.ts +7 -0
  125. package/types/enums/column-integer-status.d.ts +7 -0
  126. package/types/enums/column-ip-status.d.ts +7 -0
  127. package/types/enums/column-line-status.d.ts +7 -0
  128. package/types/enums/column-longtext-status.d.ts +7 -0
  129. package/types/enums/column-mediumtext-status.d.ts +7 -0
  130. package/types/enums/column-point-status.d.ts +7 -0
  131. package/types/enums/column-polygon-status.d.ts +7 -0
  132. package/types/enums/column-relationship-status.d.ts +7 -0
  133. package/types/enums/column-string-status.d.ts +7 -0
  134. package/types/enums/column-text-status.d.ts +7 -0
  135. package/types/enums/column-url-status.d.ts +7 -0
  136. package/types/enums/column-varchar-status.d.ts +7 -0
  137. package/types/enums/compression.d.ts +5 -0
  138. package/types/enums/database-type.d.ts +4 -0
  139. package/types/enums/deployment-status.d.ts +8 -0
  140. package/types/enums/execution-status.d.ts +7 -0
  141. package/types/enums/execution-trigger.d.ts +5 -0
  142. package/types/enums/framework.d.ts +17 -0
  143. package/types/enums/gravity.d.ts +11 -0
  144. package/types/enums/health-antivirus-status.d.ts +5 -0
  145. package/types/enums/health-status-status.d.ts +4 -0
  146. package/types/enums/index-status.d.ts +7 -0
  147. package/types/enums/message-status.d.ts +7 -0
  148. package/types/enums/method.d.ts +9 -0
  149. package/types/enums/output.d.ts +9 -0
  150. package/types/enums/permissions.d.ts +22 -0
  151. package/types/enums/priority.d.ts +4 -0
  152. package/types/enums/range.d.ts +5 -0
  153. package/types/enums/runtime.d.ts +73 -0
  154. package/types/enums/runtimes.d.ts +73 -0
  155. package/types/enums/scopes.d.ts +57 -0
  156. package/types/enums/theme.d.ts +4 -0
  157. package/types/enums/timezone.d.ts +421 -0
  158. package/types/enums/type.d.ts +5 -0
  159. package/types/enums/use-cases.d.ts +9 -0
  160. package/types/id.d.ts +20 -0
  161. package/types/index.d.ts +92 -0
  162. package/types/models.d.ts +5830 -0
  163. package/types/operator.d.ts +180 -0
  164. package/types/permission.d.ts +43 -0
  165. package/types/query.d.ts +442 -0
  166. package/types/role.d.ts +70 -0
  167. package/types/service.d.ts +11 -0
  168. package/types/services/apps.d.ts +932 -0
  169. package/types/services/avatars.d.ts +318 -0
  170. package/types/services/carts.d.ts +352 -0
  171. package/types/services/channels.d.ts +75 -0
  172. package/types/services/customers.d.ts +231 -0
  173. package/types/services/greetings.d.ts +101 -0
  174. package/types/services/locale.d.ts +64 -0
  175. package/types/services/markets.d.ts +274 -0
  176. package/types/services/messaging.d.ts +1324 -0
  177. package/types/services/products.d.ts +1014 -0
  178. package/types/services/realtime.d.ts +134 -0
  179. package/types/services/search.d.ts +131 -0
  180. package/types/services/sites.d.ts +689 -0
  181. package/types/services/storage.d.ts +421 -0
  182. package/types/services/tokens.d.ts +119 -0
@@ -0,0 +1,537 @@
1
+ import { RevenexxException, Client } from '../client';
2
+ import { Channel, ActionableChannel, ResolvedChannel } from '../channel';
3
+ import { Query } from '../query';
4
+
5
+ export type RealtimeSubscription = {
6
+ close: () => Promise<void>;
7
+ }
8
+
9
+ export type RealtimeCallback<T = any> = {
10
+ channels: Set<string>;
11
+ queries: string[]; // Array of query strings
12
+ callback: (event: RealtimeResponseEvent<T>) => void;
13
+ }
14
+
15
+ export type RealtimeResponse = {
16
+ type: string;
17
+ data?: any;
18
+ }
19
+
20
+ export type RealtimeResponseEvent<T = any> = {
21
+ events: string[];
22
+ channels: string[];
23
+ timestamp: string;
24
+ payload: T;
25
+ subscriptions: string[]; // Backend-provided subscription IDs
26
+ }
27
+
28
+ export type RealtimeResponseConnected = {
29
+ channels: string[];
30
+ user?: object;
31
+ subscriptions?: { [slot: string]: string }; // Map slot index -> subscriptionId
32
+ }
33
+
34
+ export type RealtimeRequest = {
35
+ type: 'authentication';
36
+ data: {
37
+ session: string;
38
+ };
39
+ }
40
+
41
+ export enum RealtimeCode {
42
+ NORMAL_CLOSURE = 1000,
43
+ POLICY_VIOLATION = 1008,
44
+ UNKNOWN_ERROR = -1
45
+ }
46
+
47
+ export class Realtime {
48
+ private readonly TYPE_ERROR = 'error';
49
+ private readonly TYPE_EVENT = 'event';
50
+ private readonly TYPE_PONG = 'pong';
51
+ private readonly TYPE_CONNECTED = 'connected';
52
+ private readonly DEBOUNCE_MS = 1;
53
+ private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds
54
+
55
+ private client: Client;
56
+ private socket?: WebSocket;
57
+ // Slot-centric state: Map<slot, { channels: Set<string>, queries: string[], callback: Function }>
58
+ private activeSubscriptions = new Map<number, RealtimeCallback<any>>();
59
+ // Map slot index -> subscriptionId (from backend)
60
+ private slotToSubscriptionId = new Map<number, string>();
61
+ // Inverse map: subscriptionId -> slot index (for O(1) lookup)
62
+ private subscriptionIdToSlot = new Map<string, number>();
63
+ private heartbeatTimer?: number;
64
+
65
+ private subCallDepth = 0;
66
+ private reconnectAttempts = 0;
67
+ private subscriptionsCounter = 0;
68
+ private connectionId = 0;
69
+ private reconnect = true;
70
+
71
+ private onErrorCallbacks: Array<(error?: Error, statusCode?: number) => void> = [];
72
+ private onCloseCallbacks: Array<() => void> = [];
73
+ private onOpenCallbacks: Array<() => void> = [];
74
+
75
+ constructor(client: Client) {
76
+ this.client = client;
77
+ }
78
+
79
+ /**
80
+ * Register a callback function to be called when an error occurs
81
+ *
82
+ * @param {Function} callback - Callback function to handle errors
83
+ * @returns {void}
84
+ */
85
+ public onError(callback: (error?: Error, statusCode?: number) => void): void {
86
+ this.onErrorCallbacks.push(callback);
87
+ }
88
+
89
+ /**
90
+ * Register a callback function to be called when the connection closes
91
+ *
92
+ * @param {Function} callback - Callback function to handle connection close
93
+ * @returns {void}
94
+ */
95
+ public onClose(callback: () => void): void {
96
+ this.onCloseCallbacks.push(callback);
97
+ }
98
+
99
+ /**
100
+ * Register a callback function to be called when the connection opens
101
+ *
102
+ * @param {Function} callback - Callback function to handle connection open
103
+ * @returns {void}
104
+ */
105
+ public onOpen(callback: () => void): void {
106
+ this.onOpenCallbacks.push(callback);
107
+ }
108
+
109
+ private startHeartbeat(): void {
110
+ this.stopHeartbeat();
111
+ this.heartbeatTimer = window.setInterval(() => {
112
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
113
+ this.socket.send(JSON.stringify({ type: 'ping' }));
114
+ }
115
+ }, this.HEARTBEAT_INTERVAL);
116
+ }
117
+
118
+ private stopHeartbeat(): void {
119
+ if (this.heartbeatTimer) {
120
+ window.clearInterval(this.heartbeatTimer);
121
+ this.heartbeatTimer = undefined;
122
+ }
123
+ }
124
+
125
+ private async createSocket(): Promise<void> {
126
+ if (this.activeSubscriptions.size === 0) {
127
+ this.reconnect = false;
128
+ await this.closeSocket();
129
+ return;
130
+ }
131
+
132
+ const projectId = this.client.config.tenant;
133
+ if (!projectId) {
134
+ throw new RevenexxException('Missing tenant — call client.setTenant() before subscribing');
135
+ }
136
+
137
+ // Collect all unique channels from all slots
138
+ const allChannels = new Set<string>();
139
+ for (const subscription of this.activeSubscriptions.values()) {
140
+ for (const channel of subscription.channels) {
141
+ allChannels.add(channel);
142
+ }
143
+ }
144
+
145
+ let queryParams = `project=${projectId}`;
146
+ for (const channel of allChannels) {
147
+ queryParams += `&channels[]=${encodeURIComponent(channel)}`;
148
+ }
149
+
150
+ // Build query string from slots → channels → queries
151
+ // Format: channel[slot][]=query
152
+ // For each slot, repeat its queries under each channel it subscribes to
153
+ // Example: slot 1 → channels [tests, prod], queries [q1, q2]
154
+ // Produces: tests[1][]=q1&tests[1][]=q2&prod[1][]=q1&prod[1][]=q2
155
+ const selectAllQuery = Query.select(['*']).toString();
156
+ for (const [slot, subscription] of this.activeSubscriptions) {
157
+ // queries is string[] - iterate over each query string
158
+ const queries = subscription.queries.length === 0
159
+ ? [selectAllQuery]
160
+ : subscription.queries;
161
+
162
+ // Repeat this slot's queries under each channel it subscribes to
163
+ // Each query is sent as a separate parameter: channel[slot][]=q1&channel[slot][]=q2
164
+ for (const channel of subscription.channels) {
165
+ for (const query of queries) {
166
+ queryParams += `&${encodeURIComponent(channel)}[${slot}][]=${encodeURIComponent(query)}`;
167
+ }
168
+ }
169
+ }
170
+
171
+ const endpoint =
172
+ this.client.config.endpointRealtime !== ''
173
+ ? this.client.config.endpointRealtime
174
+ : this.client.config.endpoint || '';
175
+ const realtimeEndpoint = endpoint
176
+ .replace('https://', 'wss://')
177
+ .replace('http://', 'ws://');
178
+ const url = `${realtimeEndpoint}/realtime?${queryParams}`;
179
+
180
+ if (this.socket) {
181
+ this.reconnect = false;
182
+ if (this.socket.readyState < WebSocket.CLOSING) {
183
+ await this.closeSocket();
184
+ }
185
+ // Ensure reconnect isn't stuck false if close event was missed.
186
+ this.reconnect = true;
187
+ }
188
+
189
+ return new Promise((resolve, reject) => {
190
+ try {
191
+ const connectionId = ++this.connectionId;
192
+ const socket = (this.socket = new WebSocket(url));
193
+
194
+ socket.addEventListener('open', () => {
195
+ if (connectionId !== this.connectionId) {
196
+ return;
197
+ }
198
+ this.reconnectAttempts = 0;
199
+ this.onOpenCallbacks.forEach(callback => callback());
200
+ this.startHeartbeat();
201
+ resolve();
202
+ });
203
+
204
+ socket.addEventListener('message', (event: MessageEvent) => {
205
+ if (connectionId !== this.connectionId) {
206
+ return;
207
+ }
208
+ try {
209
+ const message = JSON.parse(event.data) as RealtimeResponse;
210
+ this.handleMessage(message);
211
+ } catch (error) {
212
+ console.error('Failed to parse message:', error);
213
+ }
214
+ });
215
+
216
+ socket.addEventListener('close', async (event: CloseEvent) => {
217
+ if (connectionId !== this.connectionId || socket !== this.socket) {
218
+ return;
219
+ }
220
+ this.stopHeartbeat();
221
+ this.onCloseCallbacks.forEach(callback => callback());
222
+
223
+ if (!this.reconnect || event.code === RealtimeCode.POLICY_VIOLATION) {
224
+ this.reconnect = true;
225
+ return;
226
+ }
227
+
228
+ const timeout = this.getTimeout();
229
+ console.log(`Realtime disconnected. Re-connecting in ${timeout / 1000} seconds.`);
230
+
231
+ await this.sleep(timeout);
232
+ this.reconnectAttempts++;
233
+
234
+ try {
235
+ await this.createSocket();
236
+ } catch (error) {
237
+ console.error('Failed to reconnect:', error);
238
+ }
239
+ });
240
+
241
+ socket.addEventListener('error', (event: Event) => {
242
+ if (connectionId !== this.connectionId || socket !== this.socket) {
243
+ return;
244
+ }
245
+ this.stopHeartbeat();
246
+ const error = new Error('WebSocket error');
247
+ console.error('WebSocket error:', error.message);
248
+ this.onErrorCallbacks.forEach(callback => callback(error));
249
+ reject(error);
250
+ });
251
+ } catch (error) {
252
+ reject(error);
253
+ }
254
+ });
255
+ }
256
+
257
+ private async closeSocket(): Promise<void> {
258
+ this.stopHeartbeat();
259
+
260
+ if (this.socket) {
261
+ return new Promise((resolve) => {
262
+ if (!this.socket) {
263
+ resolve();
264
+ return;
265
+ }
266
+
267
+ if (this.socket.readyState === WebSocket.OPEN ||
268
+ this.socket.readyState === WebSocket.CONNECTING) {
269
+ this.socket.addEventListener('close', () => {
270
+ resolve();
271
+ }, { once: true });
272
+ this.socket.close(RealtimeCode.NORMAL_CLOSURE);
273
+ } else {
274
+ resolve();
275
+ }
276
+ });
277
+ }
278
+ }
279
+
280
+ private getTimeout(): number {
281
+ if (this.reconnectAttempts < 5) {
282
+ return 1000;
283
+ } else if (this.reconnectAttempts < 15) {
284
+ return 5000;
285
+ } else if (this.reconnectAttempts < 100) {
286
+ return 10000;
287
+ } else {
288
+ return 60000;
289
+ }
290
+ }
291
+
292
+ private sleep(ms: number): Promise<void> {
293
+ return new Promise(resolve => setTimeout(resolve, ms));
294
+ }
295
+
296
+ /**
297
+ * Convert a channel value to a string
298
+ *
299
+ * @private
300
+ * @param {string | Channel<any> | ActionableChannel | ResolvedChannel} channel - Channel value (string or Channel builder instance)
301
+ * @returns {string} Channel string representation
302
+ */
303
+ private channelToString(channel: string | Channel<any> | ActionableChannel | ResolvedChannel): string {
304
+ if (typeof channel === 'string') {
305
+ return channel;
306
+ }
307
+ // All Channel instances have toString() method
308
+ if (channel && typeof (channel as Channel<any>).toString === 'function') {
309
+ return (channel as Channel<any>).toString();
310
+ }
311
+ return String(channel);
312
+ }
313
+
314
+ /**
315
+ * Subscribe to a single channel
316
+ *
317
+ * @param {string | Channel<any> | ActionableChannel | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance)
318
+ * @param {Function} callback - Callback function to handle events
319
+ * @returns {Promise<RealtimeSubscription>} Subscription object with close method
320
+ */
321
+ public async subscribe(
322
+ channel: string | Channel<any> | ActionableChannel | ResolvedChannel,
323
+ callback: (event: RealtimeResponseEvent<any>) => void,
324
+ queries?: (string | Query)[]
325
+ ): Promise<RealtimeSubscription>;
326
+
327
+ /**
328
+ * Subscribe to multiple channels
329
+ *
330
+ * @param {(string | Channel<any> | ActionableChannel | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances)
331
+ * @param {Function} callback - Callback function to handle events
332
+ * @returns {Promise<RealtimeSubscription>} Subscription object with close method
333
+ */
334
+ public async subscribe(
335
+ channels: (string | Channel<any> | ActionableChannel | ResolvedChannel)[],
336
+ callback: (event: RealtimeResponseEvent<any>) => void,
337
+ queries?: (string | Query)[]
338
+ ): Promise<RealtimeSubscription>;
339
+
340
+ /**
341
+ * Subscribe to a single channel with typed payload
342
+ *
343
+ * @param {string | Channel<any> | ActionableChannel | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance)
344
+ * @param {Function} callback - Callback function to handle events with typed payload
345
+ * @returns {Promise<RealtimeSubscription>} Subscription object with close method
346
+ */
347
+ public async subscribe<T>(
348
+ channel: string | Channel<any> | ActionableChannel | ResolvedChannel,
349
+ callback: (event: RealtimeResponseEvent<T>) => void,
350
+ queries?: (string | Query)[]
351
+ ): Promise<RealtimeSubscription>;
352
+
353
+ /**
354
+ * Subscribe to multiple channels with typed payload
355
+ *
356
+ * @param {(string | Channel<any> | ActionableChannel | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances)
357
+ * @param {Function} callback - Callback function to handle events with typed payload
358
+ * @returns {Promise<RealtimeSubscription>} Subscription object with close method
359
+ */
360
+ public async subscribe<T>(
361
+ channels: (string | Channel<any> | ActionableChannel | ResolvedChannel)[],
362
+ callback: (event: RealtimeResponseEvent<T>) => void,
363
+ queries?: (string | Query)[]
364
+ ): Promise<RealtimeSubscription>;
365
+
366
+ public async subscribe<T = any>(
367
+ channelsOrChannel: string | Channel<any> | ActionableChannel | ResolvedChannel | (string | Channel<any> | ActionableChannel | ResolvedChannel)[],
368
+ callback: (event: RealtimeResponseEvent<T>) => void,
369
+ queries: (string | Query)[] = []
370
+ ): Promise<RealtimeSubscription> {
371
+ const channelArray = Array.isArray(channelsOrChannel)
372
+ ? channelsOrChannel
373
+ : [channelsOrChannel];
374
+
375
+ // Convert all channels to strings
376
+ const channelStrings = channelArray.map(ch => this.channelToString(ch));
377
+ const channels = new Set(channelStrings);
378
+
379
+ // Convert queries to array of strings
380
+ // Ensure each query is a separate string in the array
381
+ const queryStrings: string[] = [];
382
+ for (const q of (queries ?? [])) {
383
+ if (Array.isArray(q)) {
384
+ // Handle nested arrays: [[q1, q2]] -> [q1, q2]
385
+ for (const inner of q) {
386
+ queryStrings.push(typeof inner === 'string' ? inner : inner.toString());
387
+ }
388
+ } else {
389
+ queryStrings.push(typeof q === 'string' ? q : q.toString());
390
+ }
391
+ }
392
+
393
+ // Allocate a new slot index
394
+ this.subscriptionsCounter++;
395
+ const slot = this.subscriptionsCounter;
396
+
397
+ // Store slot-centric data: channels, queries, and callback belong to the slot
398
+ // queries is stored as string[] (array of query strings)
399
+ // No channel mutation occurs here - channels are derived from slots in createSocket()
400
+ this.activeSubscriptions.set(slot, {
401
+ channels,
402
+ queries: queryStrings,
403
+ callback
404
+ });
405
+
406
+ this.subCallDepth++;
407
+
408
+ await this.sleep(this.DEBOUNCE_MS);
409
+
410
+ if (this.subCallDepth === 1) {
411
+ await this.createSocket();
412
+ }
413
+
414
+ this.subCallDepth--;
415
+
416
+ return {
417
+ close: async () => {
418
+ const subscriptionId = this.slotToSubscriptionId.get(slot);
419
+ this.activeSubscriptions.delete(slot);
420
+ this.slotToSubscriptionId.delete(slot);
421
+ if (subscriptionId) {
422
+ this.subscriptionIdToSlot.delete(subscriptionId);
423
+ }
424
+ await this.createSocket();
425
+ }
426
+ };
427
+ }
428
+
429
+ // cleanUp is no longer needed - slots are removed directly in subscribe().close()
430
+ // Channels are automatically rebuilt from remaining slots in createSocket()
431
+
432
+ private handleMessage(message: RealtimeResponse): void {
433
+ if (!message.type) {
434
+ return;
435
+ }
436
+
437
+ switch (message.type) {
438
+ case this.TYPE_CONNECTED:
439
+ this.handleResponseConnected(message);
440
+ break;
441
+ case this.TYPE_ERROR:
442
+ this.handleResponseError(message);
443
+ break;
444
+ case this.TYPE_EVENT:
445
+ this.handleResponseEvent(message);
446
+ break;
447
+ case this.TYPE_PONG:
448
+ // Handle pong response if needed
449
+ break;
450
+ }
451
+ }
452
+
453
+ private handleResponseConnected(message: RealtimeResponse): void {
454
+ if (!message.data) {
455
+ return;
456
+ }
457
+
458
+ const messageData = message.data as RealtimeResponseConnected;
459
+
460
+ // Store subscription ID mappings from backend
461
+ // Format: { "0": "sub_a1f9", "1": "sub_b83c", ... }
462
+ if (messageData.subscriptions) {
463
+ this.slotToSubscriptionId.clear();
464
+ this.subscriptionIdToSlot.clear();
465
+ for (const [slotStr, subscriptionId] of Object.entries(messageData.subscriptions)) {
466
+ const slot = Number(slotStr);
467
+ if (!isNaN(slot)) {
468
+ this.slotToSubscriptionId.set(slot, subscriptionId);
469
+ this.subscriptionIdToSlot.set(subscriptionId, slot);
470
+ }
471
+ }
472
+ }
473
+
474
+ let session;
475
+ if (!session) {
476
+ try {
477
+ const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? '{}');
478
+ session = cookie?.[`a_session_${this.client.config.tenant}`];
479
+ } catch (error) {
480
+ console.error('Failed to parse cookie fallback:', error);
481
+ }
482
+ }
483
+
484
+ if (session && !messageData.user) {
485
+ this.socket?.send(JSON.stringify(<RealtimeRequest>{
486
+ type: 'authentication',
487
+ data: {
488
+ session
489
+ }
490
+ }));
491
+ }
492
+ }
493
+
494
+ private handleResponseError(message: RealtimeResponse): void {
495
+ const error = new RevenexxException(
496
+ message.data?.message || 'Unknown error'
497
+ );
498
+ const statusCode = message.data?.code;
499
+ this.onErrorCallbacks.forEach(callback => callback(error, statusCode));
500
+ }
501
+
502
+ private handleResponseEvent(message: RealtimeResponse): void {
503
+ const data = message.data;
504
+ if (!data) {
505
+ return;
506
+ }
507
+
508
+ const channels = data.channels as string[];
509
+ const events = data.events as string[];
510
+ const payload = data.payload;
511
+ const timestamp = data.timestamp as string;
512
+ const subscriptions = data.subscriptions as string[] | undefined;
513
+
514
+ if (!channels || !events || !payload || !subscriptions || subscriptions.length === 0) {
515
+ return;
516
+ }
517
+
518
+ // Iterate over all matching subscriptionIds and call callback for each
519
+ for (const subscriptionId of subscriptions) {
520
+ // O(1) lookup using subscriptionId
521
+ const slot = this.subscriptionIdToSlot.get(subscriptionId);
522
+ if (slot !== undefined) {
523
+ const subscription = this.activeSubscriptions.get(slot);
524
+ if (subscription) {
525
+ const response: RealtimeResponseEvent<any> = {
526
+ events,
527
+ channels,
528
+ timestamp,
529
+ payload,
530
+ subscriptions
531
+ };
532
+ subscription.callback(response);
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }