@aerostack/sdk-node 0.8.7 → 0.8.9
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/package.json +10 -3
- package/FUNCTIONS.md +0 -95
- package/RUNTIMES.md +0 -48
- package/examples/aiAIChat.example.ts +0 -31
- package/examples/databaseDbQuery.example.ts +0 -34
- package/examples/e2e/__tests__/e2e.test.ts +0 -118
- package/examples/e2e/package.json +0 -15
- package/examples/e2e/vitest.config.ts +0 -8
- package/examples/express-integration.ts +0 -67
- package/examples/next-api-route.ts +0 -46
- package/examples/package.json +0 -18
- package/examples/standalone-auth.ts +0 -44
- package/jsr.json +0 -27
- package/src/__tests__/realtime.test.ts +0 -430
- package/src/__tests__/sdk.test.ts +0 -412
- package/src/_generated/apis/AIApi.ts +0 -477
- package/src/_generated/apis/AuthenticationApi.ts +0 -121
- package/src/_generated/apis/CacheApi.ts +0 -551
- package/src/_generated/apis/DatabaseApi.ts +0 -138
- package/src/_generated/apis/GatewayApi.ts +0 -204
- package/src/_generated/apis/QueueApi.ts +0 -218
- package/src/_generated/apis/ServicesApi.ts +0 -74
- package/src/_generated/apis/StorageApi.ts +0 -476
- package/src/_generated/apis/index.ts +0 -10
- package/src/_generated/index.ts +0 -5
- package/src/_generated/models/AuthResponse.ts +0 -88
- package/src/_generated/models/AuthSigninRequest.ts +0 -75
- package/src/_generated/models/AuthSignupRequest.ts +0 -91
- package/src/_generated/models/CacheDeleteMany200Response.ts +0 -81
- package/src/_generated/models/CacheDeleteManyRequest.ts +0 -66
- package/src/_generated/models/CacheExpireRequest.ts +0 -75
- package/src/_generated/models/CacheFlush200Response.ts +0 -73
- package/src/_generated/models/CacheFlushRequest.ts +0 -65
- package/src/_generated/models/CacheGet200Response.ts +0 -73
- package/src/_generated/models/CacheGetMany200Response.ts +0 -72
- package/src/_generated/models/CacheGetManyEntry.ts +0 -81
- package/src/_generated/models/CacheGetManyRequest.ts +0 -66
- package/src/_generated/models/CacheGetRequest.ts +0 -66
- package/src/_generated/models/CacheIncrement200Response.ts +0 -65
- package/src/_generated/models/CacheIncrementRequest.ts +0 -90
- package/src/_generated/models/CacheKeyEntry.ts +0 -73
- package/src/_generated/models/CacheKeys200Response.ts +0 -73
- package/src/_generated/models/CacheKeysRequest.ts +0 -65
- package/src/_generated/models/CacheListRequest.ts +0 -81
- package/src/_generated/models/CacheListResult.ts +0 -88
- package/src/_generated/models/CacheSet200Response.ts +0 -65
- package/src/_generated/models/CacheSetEntry.ts +0 -83
- package/src/_generated/models/CacheSetMany200Response.ts +0 -73
- package/src/_generated/models/CacheSetManyRequest.ts +0 -73
- package/src/_generated/models/CacheSetRequest.ts +0 -83
- package/src/_generated/models/ChatCompletionRequest.ts +0 -130
- package/src/_generated/models/ChatCompletionRequestStreamOptions.ts +0 -67
- package/src/_generated/models/ChatCompletionResponse.ts +0 -128
- package/src/_generated/models/ChatCompletionResponseChoicesInner.ts +0 -100
- package/src/_generated/models/ChatMessage.ts +0 -87
- package/src/_generated/models/ConfigureRequest.ts +0 -77
- package/src/_generated/models/DbBatchRequest.ts +0 -73
- package/src/_generated/models/DbBatchRequestQueriesInner.ts +0 -74
- package/src/_generated/models/DbBatchResult.ts +0 -80
- package/src/_generated/models/DbBatchResultResultsInner.ts +0 -81
- package/src/_generated/models/DbQueryRequest.ts +0 -74
- package/src/_generated/models/DbQueryResult.ts +0 -73
- package/src/_generated/models/DeleteByTypeRequest.ts +0 -66
- package/src/_generated/models/DeleteRequest.ts +0 -66
- package/src/_generated/models/ErrorResponse.ts +0 -99
- package/src/_generated/models/GatewayBillingLog200Response.ts +0 -73
- package/src/_generated/models/GatewayBillingLogRequest.ts +0 -92
- package/src/_generated/models/GatewayGetWallet200Response.ts +0 -72
- package/src/_generated/models/IngestRequest.ts +0 -91
- package/src/_generated/models/JobRecord.ts +0 -119
- package/src/_generated/models/ListTypes200Response.ts +0 -72
- package/src/_generated/models/Query200Response.ts +0 -72
- package/src/_generated/models/QueryRequest.ts +0 -90
- package/src/_generated/models/QueueCancelJob200Response.ts +0 -73
- package/src/_generated/models/QueueEnqueue201Response.ts +0 -73
- package/src/_generated/models/QueueEnqueueRequest.ts +0 -83
- package/src/_generated/models/QueueGetJob200Response.ts +0 -80
- package/src/_generated/models/QueueGetJobRequest.ts +0 -66
- package/src/_generated/models/QueueListJobs200Response.ts +0 -88
- package/src/_generated/models/QueueListJobsRequest.ts +0 -103
- package/src/_generated/models/SearchCount200Response.ts +0 -65
- package/src/_generated/models/SearchCountRequest.ts +0 -65
- package/src/_generated/models/SearchGet200Response.ts +0 -80
- package/src/_generated/models/SearchGetRequest.ts +0 -66
- package/src/_generated/models/SearchResult.ts +0 -97
- package/src/_generated/models/SearchUpdateRequest.ts +0 -91
- package/src/_generated/models/ServicesInvoke200Response.ts +0 -73
- package/src/_generated/models/ServicesInvokeRequest.ts +0 -75
- package/src/_generated/models/StorageCopy200Response.ts +0 -73
- package/src/_generated/models/StorageCopyRequest.ts +0 -75
- package/src/_generated/models/StorageExists200Response.ts +0 -65
- package/src/_generated/models/StorageGetRequest.ts +0 -66
- package/src/_generated/models/StorageListRequest.ts +0 -81
- package/src/_generated/models/StorageListResult.ts +0 -88
- package/src/_generated/models/StorageMetadata.ts +0 -97
- package/src/_generated/models/StorageMove200Response.ts +0 -73
- package/src/_generated/models/StorageMoveRequest.ts +0 -75
- package/src/_generated/models/StorageObject.ts +0 -97
- package/src/_generated/models/StorageUpload200Response.ts +0 -65
- package/src/_generated/models/TokenUsage.ts +0 -81
- package/src/_generated/models/TokenWallet.ts +0 -73
- package/src/_generated/models/TypeStats.ts +0 -73
- package/src/_generated/models/User.ts +0 -97
- package/src/_generated/models/index.ts +0 -80
- package/src/_generated/runtime.ts +0 -431
- package/src/index.ts +0 -3
- package/src/realtime.ts +0 -439
- package/src/sdk.ts +0 -317
- package/test_sdk.ts +0 -19
- package/tsconfig.json +0 -43
package/src/realtime.ts
DELETED
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Aerostack Realtime Client for Node.js SDK
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export type RealtimeEvent = 'INSERT' | 'UPDATE' | 'DELETE' | '*' | string;
|
|
6
|
-
|
|
7
|
-
export interface RealtimeMessage {
|
|
8
|
-
type: string;
|
|
9
|
-
topic: string;
|
|
10
|
-
[key: string]: any;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface RealtimeSubscriptionOptions {
|
|
14
|
-
event?: RealtimeEvent;
|
|
15
|
-
filter?: Record<string, any>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type RealtimeCallback<T = any> = (payload: RealtimePayload<T>) => void;
|
|
19
|
-
|
|
20
|
-
/** Typed payload for realtime events */
|
|
21
|
-
export interface RealtimePayload<T = any> {
|
|
22
|
-
type: 'db_change' | 'chat_message' | 'event';
|
|
23
|
-
topic: string;
|
|
24
|
-
operation?: RealtimeEvent;
|
|
25
|
-
event?: string;
|
|
26
|
-
data: T;
|
|
27
|
-
old?: T;
|
|
28
|
-
userId?: string;
|
|
29
|
-
timestamp?: number | string;
|
|
30
|
-
[key: string]: any;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Chat history message returned from REST API */
|
|
34
|
-
export interface HistoryMessage {
|
|
35
|
-
id: string;
|
|
36
|
-
room_id: string;
|
|
37
|
-
user_id: string;
|
|
38
|
-
event: string;
|
|
39
|
-
data: any;
|
|
40
|
-
created_at: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface NodeRealtimeOptions {
|
|
44
|
-
serverUrl: string;
|
|
45
|
-
apiKey?: string;
|
|
46
|
-
token?: string;
|
|
47
|
-
projectId?: string;
|
|
48
|
-
maxReconnectAttempts?: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const BASE_RECONNECT_MS = 1000;
|
|
52
|
-
const MAX_RECONNECT_MS = 30000;
|
|
53
|
-
const JITTER_FACTOR = 0.3;
|
|
54
|
-
|
|
55
|
-
export class RealtimeSubscription<T = any> {
|
|
56
|
-
private client: NodeRealtimeClient;
|
|
57
|
-
topic: string;
|
|
58
|
-
private options: RealtimeSubscriptionOptions;
|
|
59
|
-
private callbacks: Map<string, Set<RealtimeCallback<T>>> = new Map();
|
|
60
|
-
private _isSubscribed: boolean = false;
|
|
61
|
-
|
|
62
|
-
constructor(client: NodeRealtimeClient, topic: string, options: RealtimeSubscriptionOptions = {}) {
|
|
63
|
-
this.client = client;
|
|
64
|
-
this.topic = topic;
|
|
65
|
-
this.options = options;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Listen for DB change events (INSERT/UPDATE/DELETE/*) or custom named events */
|
|
69
|
-
on(event: RealtimeEvent | string, callback: RealtimeCallback<T>): this {
|
|
70
|
-
if (!this.callbacks.has(event)) {
|
|
71
|
-
this.callbacks.set(event, new Set());
|
|
72
|
-
}
|
|
73
|
-
this.callbacks.get(event)!.add(callback);
|
|
74
|
-
return this;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Remove a specific callback for an event */
|
|
78
|
-
off(event: RealtimeEvent | string, callback: RealtimeCallback<T>): this {
|
|
79
|
-
this.callbacks.get(event)?.delete(callback);
|
|
80
|
-
return this;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
subscribe(): this {
|
|
84
|
-
if (this._isSubscribed) return this;
|
|
85
|
-
this.client._send({
|
|
86
|
-
type: 'subscribe',
|
|
87
|
-
topic: this.topic,
|
|
88
|
-
filter: this.options.filter
|
|
89
|
-
});
|
|
90
|
-
this._isSubscribed = true;
|
|
91
|
-
return this;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
unsubscribe(): void {
|
|
95
|
-
if (!this._isSubscribed) return;
|
|
96
|
-
this.client._send({
|
|
97
|
-
type: 'unsubscribe',
|
|
98
|
-
topic: this.topic
|
|
99
|
-
});
|
|
100
|
-
this._isSubscribed = false;
|
|
101
|
-
this.callbacks.clear();
|
|
102
|
-
this.client._removeSubscription(this.topic);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
get isSubscribed() { return this._isSubscribed; }
|
|
106
|
-
|
|
107
|
-
// ─── Phase 1: Pub/Sub — Publish custom events ─────────────────────────
|
|
108
|
-
/** Publish a custom event to all subscribers on this channel */
|
|
109
|
-
publish(event: string, data: any, options?: { persist?: boolean }): void {
|
|
110
|
-
this.client._send({
|
|
111
|
-
type: 'publish',
|
|
112
|
-
topic: this.topic,
|
|
113
|
-
event,
|
|
114
|
-
data,
|
|
115
|
-
persist: options?.persist,
|
|
116
|
-
id: this.client._generateId(),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ─── Phase 2: Chat History ────────────────────────────────────────────
|
|
121
|
-
/** Fetch persisted message history for this channel (requires persist: true on publish) */
|
|
122
|
-
async getHistory(limit: number = 50, before?: number): Promise<HistoryMessage[]> {
|
|
123
|
-
return this.client._fetchHistory(this.topic, limit, before);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ─── Phase 3: Presence ────────────────────────────────────────────────
|
|
127
|
-
/** Track this user's presence state on this channel (auto-synced to subscribers) */
|
|
128
|
-
track(state: Record<string, any>): void {
|
|
129
|
-
this.client._send({
|
|
130
|
-
type: 'track',
|
|
131
|
-
topic: this.topic,
|
|
132
|
-
state,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Stop tracking presence on this channel */
|
|
137
|
-
untrack(): void {
|
|
138
|
-
this.client._send({
|
|
139
|
-
type: 'untrack',
|
|
140
|
-
topic: this.topic,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** @internal */
|
|
145
|
-
_emit(payload: RealtimePayload<T>): void {
|
|
146
|
-
// DB change events (INSERT/UPDATE/DELETE)
|
|
147
|
-
if (payload.operation) {
|
|
148
|
-
const event = payload.operation as string;
|
|
149
|
-
this.callbacks.get(event)?.forEach(cb => cb(payload));
|
|
150
|
-
}
|
|
151
|
-
// Custom named events ('player-moved', 'presence:join', etc.)
|
|
152
|
-
if (payload.event) {
|
|
153
|
-
this.callbacks.get(payload.event)?.forEach(cb => cb(payload));
|
|
154
|
-
}
|
|
155
|
-
// Catch-all
|
|
156
|
-
this.callbacks.get('*')?.forEach(cb => {
|
|
157
|
-
try { cb(payload); } catch (e) { console.error('Realtime callback error:', e); }
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export type RealtimeStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
|
|
163
|
-
|
|
164
|
-
export class NodeRealtimeClient {
|
|
165
|
-
private wsUrl: string;
|
|
166
|
-
private apiKey?: string;
|
|
167
|
-
private token?: string;
|
|
168
|
-
private projectId?: string;
|
|
169
|
-
private ws: WebSocket | null = null;
|
|
170
|
-
private subscriptions: Map<string, RealtimeSubscription> = new Map();
|
|
171
|
-
private reconnectTimer: any = null;
|
|
172
|
-
private heartbeatTimer: any = null;
|
|
173
|
-
private reconnectAttempts: number = 0;
|
|
174
|
-
private _sendQueue: any[] = [];
|
|
175
|
-
private _connectingPromise: Promise<void> | null = null;
|
|
176
|
-
private _status: RealtimeStatus = 'idle';
|
|
177
|
-
private _statusListeners: Set<(s: RealtimeStatus) => void> = new Set();
|
|
178
|
-
// HTTP base URL for REST endpoints (history, etc.)
|
|
179
|
-
private _httpBaseUrl: string;
|
|
180
|
-
private _lastPong: number = 0;
|
|
181
|
-
private _maxReconnectAttempts: number;
|
|
182
|
-
private _maxRetriesListeners: Set<() => void> = new Set();
|
|
183
|
-
|
|
184
|
-
constructor(options: NodeRealtimeOptions) {
|
|
185
|
-
const uri = new URL(options.serverUrl);
|
|
186
|
-
uri.protocol = uri.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
187
|
-
uri.pathname = uri.pathname.replace(/\/v1\/?$/, '') + '/api/realtime';
|
|
188
|
-
this.wsUrl = uri.toString();
|
|
189
|
-
this._httpBaseUrl = options.serverUrl.replace(/\/v1\/?$/, '');
|
|
190
|
-
this.apiKey = options.apiKey;
|
|
191
|
-
this.token = options.token;
|
|
192
|
-
this.projectId = options.projectId;
|
|
193
|
-
this._maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
get status(): RealtimeStatus { return this._status; }
|
|
197
|
-
get connected(): boolean { return this._status === 'connected'; }
|
|
198
|
-
|
|
199
|
-
onStatusChange(cb: (status: RealtimeStatus) => void): () => void {
|
|
200
|
-
this._statusListeners.add(cb);
|
|
201
|
-
return () => this._statusListeners.delete(cb);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
onMaxRetriesExceeded(cb: () => void): () => void {
|
|
205
|
-
this._maxRetriesListeners.add(cb);
|
|
206
|
-
return () => this._maxRetriesListeners.delete(cb);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private _setStatus(s: RealtimeStatus) {
|
|
210
|
-
this._status = s;
|
|
211
|
-
this._statusListeners.forEach(cb => cb(s));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
setToken(newToken: string): void {
|
|
215
|
-
this.token = newToken;
|
|
216
|
-
this._send({ type: 'auth', token: newToken });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async connect(): Promise<void> {
|
|
220
|
-
if (this.ws && this._status === 'connected') return;
|
|
221
|
-
if (this._connectingPromise) return this._connectingPromise;
|
|
222
|
-
this._connectingPromise = this._doConnect().finally(() => {
|
|
223
|
-
this._connectingPromise = null;
|
|
224
|
-
});
|
|
225
|
-
return this._connectingPromise;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private async _doConnect(): Promise<void> {
|
|
229
|
-
this._setStatus('connecting');
|
|
230
|
-
const url = new URL(this.wsUrl);
|
|
231
|
-
if (this.projectId) url.searchParams.set('projectId', this.projectId);
|
|
232
|
-
|
|
233
|
-
return new Promise(async (resolve, reject) => {
|
|
234
|
-
try {
|
|
235
|
-
let WsClass: any;
|
|
236
|
-
if (typeof globalThis.WebSocket !== 'undefined') {
|
|
237
|
-
WsClass = globalThis.WebSocket;
|
|
238
|
-
} else {
|
|
239
|
-
try {
|
|
240
|
-
const ws = await import('ws');
|
|
241
|
-
WsClass = ws.default || ws;
|
|
242
|
-
} catch {
|
|
243
|
-
throw new Error('WebSocket not available. Install "ws" package.');
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// SECURITY: Pass credentials via Sec-WebSocket-Protocol header — never as URL query params
|
|
248
|
-
// (URL params appear in CDN logs, browser history, and Referer headers).
|
|
249
|
-
const protocols: string[] = [];
|
|
250
|
-
if (this.apiKey) protocols.push(`aerostack-key.${this.apiKey}`);
|
|
251
|
-
if (this.token) protocols.push(`aerostack-token.${this.token}`);
|
|
252
|
-
if (protocols.length > 0) protocols.push('aerostack-v1');
|
|
253
|
-
const protocolsArg = protocols.length > 0 ? protocols : undefined;
|
|
254
|
-
this.ws = protocolsArg ? new WsClass(url.toString(), protocolsArg) : new WsClass(url.toString());
|
|
255
|
-
|
|
256
|
-
this.ws!.onopen = () => {
|
|
257
|
-
this._setStatus('connected');
|
|
258
|
-
this.reconnectAttempts = 0;
|
|
259
|
-
this._lastPong = Date.now();
|
|
260
|
-
this.startHeartbeat();
|
|
261
|
-
while (this._sendQueue.length > 0) {
|
|
262
|
-
this.ws!.send(JSON.stringify(this._sendQueue.shift()));
|
|
263
|
-
}
|
|
264
|
-
for (const sub of this.subscriptions.values()) {
|
|
265
|
-
if (sub.isSubscribed) sub.subscribe();
|
|
266
|
-
}
|
|
267
|
-
resolve();
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
this.ws!.onmessage = (event: any) => {
|
|
271
|
-
try {
|
|
272
|
-
const raw = typeof event === 'string' ? event : event.data;
|
|
273
|
-
const data: RealtimeMessage = JSON.parse(raw);
|
|
274
|
-
this.handleMessage(data);
|
|
275
|
-
} catch (e) {
|
|
276
|
-
console.error('Realtime message parse error:', e);
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
this.ws!.onclose = () => {
|
|
281
|
-
this._setStatus('reconnecting');
|
|
282
|
-
this.stopHeartbeat();
|
|
283
|
-
this.ws = null;
|
|
284
|
-
this.scheduleReconnect();
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
this.ws!.onerror = (err: any) => {
|
|
288
|
-
console.error('Realtime connection error:', err);
|
|
289
|
-
this._setStatus('disconnected');
|
|
290
|
-
reject(err);
|
|
291
|
-
};
|
|
292
|
-
} catch (e) {
|
|
293
|
-
this._setStatus('disconnected');
|
|
294
|
-
reject(e);
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
disconnect(): void {
|
|
300
|
-
this._setStatus('disconnected');
|
|
301
|
-
this.stopReconnect();
|
|
302
|
-
this.stopHeartbeat();
|
|
303
|
-
if (this.ws) {
|
|
304
|
-
this.ws.close();
|
|
305
|
-
this.ws = null;
|
|
306
|
-
}
|
|
307
|
-
this._sendQueue = [];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
channel<T = any>(topic: string, options: RealtimeSubscriptionOptions = {}): RealtimeSubscription<T> {
|
|
311
|
-
if (!this.projectId) {
|
|
312
|
-
throw new Error('projectId is required for channel subscriptions. Set it in NodeRealtimeOptions.');
|
|
313
|
-
}
|
|
314
|
-
let fullTopic: string;
|
|
315
|
-
if (!topic.includes('/')) {
|
|
316
|
-
fullTopic = `table/${topic}/${this.projectId}`;
|
|
317
|
-
} else if (this.projectId && topic.endsWith(`/${this.projectId}`)) {
|
|
318
|
-
fullTopic = topic; // already fully qualified
|
|
319
|
-
} else {
|
|
320
|
-
fullTopic = `${topic}/${this.projectId}`; // e.g. 'table/orders' → 'table/orders/<projectId>'
|
|
321
|
-
}
|
|
322
|
-
let sub = this.subscriptions.get(fullTopic);
|
|
323
|
-
if (!sub) {
|
|
324
|
-
sub = new RealtimeSubscription<T>(this, fullTopic, options);
|
|
325
|
-
this.subscriptions.set(fullTopic, sub);
|
|
326
|
-
}
|
|
327
|
-
return sub as RealtimeSubscription<T>;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** Legacy: send a chat message (now persisted to DB) */
|
|
331
|
-
sendChat(roomId: string, text: string, metadata?: Record<string, any>): void {
|
|
332
|
-
this._send({ type: 'chat', roomId, text, metadata });
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/** @internal — Generate unique message ID for ack tracking */
|
|
336
|
-
_generateId(): string {
|
|
337
|
-
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/** @internal — Remove a subscription from the map (called on unsubscribe) */
|
|
341
|
-
_removeSubscription(topic: string): void {
|
|
342
|
-
this.subscriptions.delete(topic);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** @internal */
|
|
346
|
-
_send(data: any): void {
|
|
347
|
-
if (this.ws && this._status === 'connected') {
|
|
348
|
-
this.ws.send(JSON.stringify(data));
|
|
349
|
-
} else {
|
|
350
|
-
this._sendQueue.push(data);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** @internal — Fetch chat/event history via REST API */
|
|
355
|
-
async _fetchHistory(room: string, limit: number = 50, before?: number): Promise<HistoryMessage[]> {
|
|
356
|
-
const url = new URL(`${this._httpBaseUrl}/api/v1/public/realtime/history`);
|
|
357
|
-
url.searchParams.set('room', room);
|
|
358
|
-
url.searchParams.set('limit', String(limit));
|
|
359
|
-
if (before) url.searchParams.set('before', String(before));
|
|
360
|
-
|
|
361
|
-
const headers: Record<string, string> = {};
|
|
362
|
-
if (this.apiKey) headers['X-Aerostack-Key'] = this.apiKey;
|
|
363
|
-
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
|
364
|
-
|
|
365
|
-
const res = await fetch(url.toString(), { headers });
|
|
366
|
-
const json = await res.json() as any;
|
|
367
|
-
return json.messages || [];
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
private handleMessage(data: RealtimeMessage): void {
|
|
371
|
-
// Track pong for liveness
|
|
372
|
-
if (data.type === 'pong') {
|
|
373
|
-
this._lastPong = Date.now();
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Ack (fire-and-forget acknowledgment from server)
|
|
378
|
-
if (data.type === 'ack') {
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Route to subscription: db_change, chat_message, event, presence:*
|
|
383
|
-
if (data.type === 'db_change' || data.type === 'chat_message' || data.type === 'event') {
|
|
384
|
-
const sub = this.subscriptions.get(data.topic);
|
|
385
|
-
if (sub) sub._emit(data as any);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Re-key subscription on server-confirmed topic (for non-TS SDKs compatibility)
|
|
389
|
-
if (data.type === 'subscribed' && data.topic) {
|
|
390
|
-
for (const [origTopic, sub] of this.subscriptions.entries()) {
|
|
391
|
-
if (data.topic !== origTopic && data.topic.startsWith(origTopic)) {
|
|
392
|
-
this.subscriptions.delete(origTopic);
|
|
393
|
-
sub.topic = data.topic;
|
|
394
|
-
this.subscriptions.set(data.topic, sub);
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private startHeartbeat(): void {
|
|
402
|
-
this.heartbeatTimer = setInterval(() => {
|
|
403
|
-
this._send({ type: 'ping' });
|
|
404
|
-
if (this._lastPong > 0 && Date.now() - this._lastPong > 70000) {
|
|
405
|
-
console.warn('Realtime: no pong received, forcing reconnect');
|
|
406
|
-
this.ws?.close();
|
|
407
|
-
}
|
|
408
|
-
}, 30000);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
private stopHeartbeat(): void {
|
|
412
|
-
if (this.heartbeatTimer) {
|
|
413
|
-
clearInterval(this.heartbeatTimer);
|
|
414
|
-
this.heartbeatTimer = null;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
private scheduleReconnect(): void {
|
|
419
|
-
this.stopReconnect();
|
|
420
|
-
if (this.reconnectAttempts >= this._maxReconnectAttempts) {
|
|
421
|
-
this._setStatus('disconnected');
|
|
422
|
-
this._maxRetriesListeners.forEach(cb => cb());
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const delay = Math.min(BASE_RECONNECT_MS * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_MS);
|
|
426
|
-
const jitter = delay * JITTER_FACTOR * Math.random();
|
|
427
|
-
this.reconnectAttempts++;
|
|
428
|
-
this.reconnectTimer = setTimeout(() => {
|
|
429
|
-
this.connect().catch(() => { });
|
|
430
|
-
}, delay + jitter);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private stopReconnect(): void {
|
|
434
|
-
if (this.reconnectTimer) {
|
|
435
|
-
clearTimeout(this.reconnectTimer);
|
|
436
|
-
this.reconnectTimer = null;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|