@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.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/sdk.js +16340 -0
- package/dist/cjs/sdk.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/sdk.js +16250 -0
- package/dist/esm/sdk.js.map +1 -0
- package/dist/iife/sdk.js +20101 -0
- package/package.json +56 -0
- package/src/channel.ts +158 -0
- package/src/client.ts +950 -0
- package/src/enums/adapter.ts +4 -0
- package/src/enums/attribute-boolean-status.ts +7 -0
- package/src/enums/attribute-datetime-status.ts +7 -0
- package/src/enums/attribute-email-status.ts +7 -0
- package/src/enums/attribute-enum-status.ts +7 -0
- package/src/enums/attribute-float-status.ts +7 -0
- package/src/enums/attribute-integer-status.ts +7 -0
- package/src/enums/attribute-ip-status.ts +7 -0
- package/src/enums/attribute-line-status.ts +7 -0
- package/src/enums/attribute-longtext-status.ts +7 -0
- package/src/enums/attribute-mediumtext-status.ts +7 -0
- package/src/enums/attribute-point-status.ts +7 -0
- package/src/enums/attribute-polygon-status.ts +7 -0
- package/src/enums/attribute-relationship-status.ts +7 -0
- package/src/enums/attribute-string-status.ts +7 -0
- package/src/enums/attribute-text-status.ts +7 -0
- package/src/enums/attribute-url-status.ts +7 -0
- package/src/enums/attribute-varchar-status.ts +7 -0
- package/src/enums/build-runtime.ts +73 -0
- package/src/enums/code.ts +16 -0
- package/src/enums/collection.ts +4 -0
- package/src/enums/column-boolean-status.ts +7 -0
- package/src/enums/column-datetime-status.ts +7 -0
- package/src/enums/column-email-status.ts +7 -0
- package/src/enums/column-enum-status.ts +7 -0
- package/src/enums/column-float-status.ts +7 -0
- package/src/enums/column-integer-status.ts +7 -0
- package/src/enums/column-ip-status.ts +7 -0
- package/src/enums/column-line-status.ts +7 -0
- package/src/enums/column-longtext-status.ts +7 -0
- package/src/enums/column-mediumtext-status.ts +7 -0
- package/src/enums/column-point-status.ts +7 -0
- package/src/enums/column-polygon-status.ts +7 -0
- package/src/enums/column-relationship-status.ts +7 -0
- package/src/enums/column-string-status.ts +7 -0
- package/src/enums/column-text-status.ts +7 -0
- package/src/enums/column-url-status.ts +7 -0
- package/src/enums/column-varchar-status.ts +7 -0
- package/src/enums/compression.ts +5 -0
- package/src/enums/database-type.ts +4 -0
- package/src/enums/deployment-status.ts +8 -0
- package/src/enums/execution-status.ts +7 -0
- package/src/enums/execution-trigger.ts +5 -0
- package/src/enums/framework.ts +17 -0
- package/src/enums/gravity.ts +11 -0
- package/src/enums/health-antivirus-status.ts +5 -0
- package/src/enums/health-status-status.ts +4 -0
- package/src/enums/index-status.ts +7 -0
- package/src/enums/message-status.ts +7 -0
- package/src/enums/method.ts +9 -0
- package/src/enums/output.ts +9 -0
- package/src/enums/permissions.ts +22 -0
- package/src/enums/priority.ts +4 -0
- package/src/enums/range.ts +5 -0
- package/src/enums/runtime.ts +73 -0
- package/src/enums/runtimes.ts +73 -0
- package/src/enums/scopes.ts +57 -0
- package/src/enums/theme.ts +4 -0
- package/src/enums/timezone.ts +421 -0
- package/src/enums/type.ts +5 -0
- package/src/enums/use-cases.ts +9 -0
- package/src/id.ts +47 -0
- package/src/index.ts +92 -0
- package/src/models.ts +6013 -0
- package/src/operator.ts +308 -0
- package/src/permission.ts +57 -0
- package/src/query.ts +576 -0
- package/src/role.ts +100 -0
- package/src/service.ts +30 -0
- package/src/services/apps.ts +2473 -0
- package/src/services/avatars.ts +744 -0
- package/src/services/carts.ts +1057 -0
- package/src/services/channels.ts +227 -0
- package/src/services/customers.ts +729 -0
- package/src/services/greetings.ts +294 -0
- package/src/services/locale.ts +198 -0
- package/src/services/markets.ts +796 -0
- package/src/services/messaging.ts +3463 -0
- package/src/services/products.ts +3100 -0
- package/src/services/realtime.ts +537 -0
- package/src/services/search.ts +346 -0
- package/src/services/sites.ts +1847 -0
- package/src/services/storage.ts +1076 -0
- package/src/services/tokens.ts +314 -0
- package/types/channel.d.ts +74 -0
- package/types/client.d.ts +211 -0
- package/types/enums/adapter.d.ts +4 -0
- package/types/enums/attribute-boolean-status.d.ts +7 -0
- package/types/enums/attribute-datetime-status.d.ts +7 -0
- package/types/enums/attribute-email-status.d.ts +7 -0
- package/types/enums/attribute-enum-status.d.ts +7 -0
- package/types/enums/attribute-float-status.d.ts +7 -0
- package/types/enums/attribute-integer-status.d.ts +7 -0
- package/types/enums/attribute-ip-status.d.ts +7 -0
- package/types/enums/attribute-line-status.d.ts +7 -0
- package/types/enums/attribute-longtext-status.d.ts +7 -0
- package/types/enums/attribute-mediumtext-status.d.ts +7 -0
- package/types/enums/attribute-point-status.d.ts +7 -0
- package/types/enums/attribute-polygon-status.d.ts +7 -0
- package/types/enums/attribute-relationship-status.d.ts +7 -0
- package/types/enums/attribute-string-status.d.ts +7 -0
- package/types/enums/attribute-text-status.d.ts +7 -0
- package/types/enums/attribute-url-status.d.ts +7 -0
- package/types/enums/attribute-varchar-status.d.ts +7 -0
- package/types/enums/build-runtime.d.ts +73 -0
- package/types/enums/code.d.ts +16 -0
- package/types/enums/collection.d.ts +4 -0
- package/types/enums/column-boolean-status.d.ts +7 -0
- package/types/enums/column-datetime-status.d.ts +7 -0
- package/types/enums/column-email-status.d.ts +7 -0
- package/types/enums/column-enum-status.d.ts +7 -0
- package/types/enums/column-float-status.d.ts +7 -0
- package/types/enums/column-integer-status.d.ts +7 -0
- package/types/enums/column-ip-status.d.ts +7 -0
- package/types/enums/column-line-status.d.ts +7 -0
- package/types/enums/column-longtext-status.d.ts +7 -0
- package/types/enums/column-mediumtext-status.d.ts +7 -0
- package/types/enums/column-point-status.d.ts +7 -0
- package/types/enums/column-polygon-status.d.ts +7 -0
- package/types/enums/column-relationship-status.d.ts +7 -0
- package/types/enums/column-string-status.d.ts +7 -0
- package/types/enums/column-text-status.d.ts +7 -0
- package/types/enums/column-url-status.d.ts +7 -0
- package/types/enums/column-varchar-status.d.ts +7 -0
- package/types/enums/compression.d.ts +5 -0
- package/types/enums/database-type.d.ts +4 -0
- package/types/enums/deployment-status.d.ts +8 -0
- package/types/enums/execution-status.d.ts +7 -0
- package/types/enums/execution-trigger.d.ts +5 -0
- package/types/enums/framework.d.ts +17 -0
- package/types/enums/gravity.d.ts +11 -0
- package/types/enums/health-antivirus-status.d.ts +5 -0
- package/types/enums/health-status-status.d.ts +4 -0
- package/types/enums/index-status.d.ts +7 -0
- package/types/enums/message-status.d.ts +7 -0
- package/types/enums/method.d.ts +9 -0
- package/types/enums/output.d.ts +9 -0
- package/types/enums/permissions.d.ts +22 -0
- package/types/enums/priority.d.ts +4 -0
- package/types/enums/range.d.ts +5 -0
- package/types/enums/runtime.d.ts +73 -0
- package/types/enums/runtimes.d.ts +73 -0
- package/types/enums/scopes.d.ts +57 -0
- package/types/enums/theme.d.ts +4 -0
- package/types/enums/timezone.d.ts +421 -0
- package/types/enums/type.d.ts +5 -0
- package/types/enums/use-cases.d.ts +9 -0
- package/types/id.d.ts +20 -0
- package/types/index.d.ts +92 -0
- package/types/models.d.ts +5830 -0
- package/types/operator.d.ts +180 -0
- package/types/permission.d.ts +43 -0
- package/types/query.d.ts +442 -0
- package/types/role.d.ts +70 -0
- package/types/service.d.ts +11 -0
- package/types/services/apps.d.ts +932 -0
- package/types/services/avatars.d.ts +318 -0
- package/types/services/carts.d.ts +352 -0
- package/types/services/channels.d.ts +75 -0
- package/types/services/customers.d.ts +231 -0
- package/types/services/greetings.d.ts +101 -0
- package/types/services/locale.d.ts +64 -0
- package/types/services/markets.d.ts +274 -0
- package/types/services/messaging.d.ts +1324 -0
- package/types/services/products.d.ts +1014 -0
- package/types/services/realtime.d.ts +134 -0
- package/types/services/search.d.ts +131 -0
- package/types/services/sites.d.ts +689 -0
- package/types/services/storage.d.ts +421 -0
- 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
|
+
}
|