@aerostack/sdk-web 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 -69
- package/examples/e2e/package.json +0 -15
- package/examples/e2e/vitest.config.ts +0 -8
- package/examples/package.json +0 -18
- package/jsr.json +0 -27
- package/src/__tests__/realtime.test.ts +0 -388
- package/src/__tests__/sdk.test.ts +0 -181
- 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 -456
- package/src/sdk.ts +0 -197
- package/tsconfig.json +0 -41
package/src/realtime.ts
DELETED
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Aerostack Realtime Client for Web/Browser SDK
|
|
3
|
-
*/
|
|
4
|
-
export type RealtimeEvent = 'INSERT' | 'UPDATE' | 'DELETE' | '*' | string;
|
|
5
|
-
|
|
6
|
-
export interface RealtimeMessage {
|
|
7
|
-
type: string;
|
|
8
|
-
topic: string;
|
|
9
|
-
[key: string]: any;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface RealtimeSubscriptionOptions {
|
|
13
|
-
event?: RealtimeEvent;
|
|
14
|
-
filter?: Record<string, any>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type RealtimeCallback<T = any> = (payload: RealtimePayload<T>) => void;
|
|
18
|
-
|
|
19
|
-
/** Typed payload for realtime events */
|
|
20
|
-
export interface RealtimePayload<T = any> {
|
|
21
|
-
type: 'db_change' | 'chat_message' | 'event';
|
|
22
|
-
topic: string;
|
|
23
|
-
operation?: RealtimeEvent;
|
|
24
|
-
event?: string;
|
|
25
|
-
data: T;
|
|
26
|
-
old?: T;
|
|
27
|
-
userId?: string;
|
|
28
|
-
timestamp?: number | string;
|
|
29
|
-
[key: string]: any;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Chat history message returned from REST API */
|
|
33
|
-
export interface HistoryMessage {
|
|
34
|
-
id: string;
|
|
35
|
-
room_id: string;
|
|
36
|
-
user_id: string;
|
|
37
|
-
event: string;
|
|
38
|
-
data: any;
|
|
39
|
-
created_at: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class RealtimeSubscription<T = any> {
|
|
43
|
-
private client: RealtimeClient;
|
|
44
|
-
topic: string;
|
|
45
|
-
private options: RealtimeSubscriptionOptions;
|
|
46
|
-
private callbacks: Map<string, Set<RealtimeCallback<T>>> = new Map();
|
|
47
|
-
private isSubscribed: boolean = false;
|
|
48
|
-
|
|
49
|
-
constructor(client: RealtimeClient, topic: string, options: RealtimeSubscriptionOptions = {}) {
|
|
50
|
-
this.client = client;
|
|
51
|
-
this.topic = topic;
|
|
52
|
-
this.options = options;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Listen for DB change events (INSERT/UPDATE/DELETE/*) or custom named events */
|
|
56
|
-
on(event: RealtimeEvent | string, callback: RealtimeCallback<T>): this {
|
|
57
|
-
if (!this.callbacks.has(event)) {
|
|
58
|
-
this.callbacks.set(event, new Set());
|
|
59
|
-
}
|
|
60
|
-
this.callbacks.get(event)!.add(callback);
|
|
61
|
-
return this;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Remove a specific callback for an event */
|
|
65
|
-
off(event: RealtimeEvent | string, callback: RealtimeCallback<T>): this {
|
|
66
|
-
this.callbacks.get(event)?.delete(callback);
|
|
67
|
-
return this;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
subscribe(): this {
|
|
71
|
-
if (this.isSubscribed) return this;
|
|
72
|
-
this.client._send({
|
|
73
|
-
type: 'subscribe',
|
|
74
|
-
topic: this.topic,
|
|
75
|
-
filter: this.options.filter
|
|
76
|
-
});
|
|
77
|
-
this.isSubscribed = true;
|
|
78
|
-
return this;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
unsubscribe(): void {
|
|
82
|
-
if (!this.isSubscribed) return;
|
|
83
|
-
this.client._send({
|
|
84
|
-
type: 'unsubscribe',
|
|
85
|
-
topic: this.topic
|
|
86
|
-
});
|
|
87
|
-
this.isSubscribed = false;
|
|
88
|
-
this.callbacks.clear();
|
|
89
|
-
this.client._removeSubscription(this.topic);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ─── Phase 1: Pub/Sub — Publish custom events ─────────────────────────
|
|
93
|
-
/** Publish a custom event to all subscribers on this channel */
|
|
94
|
-
publish(event: string, data: any, options?: { persist?: boolean }): void {
|
|
95
|
-
this.client._send({
|
|
96
|
-
type: 'publish',
|
|
97
|
-
topic: this.topic,
|
|
98
|
-
event,
|
|
99
|
-
data,
|
|
100
|
-
persist: options?.persist,
|
|
101
|
-
id: this.client._generateId(),
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── Phase 2: Chat History ────────────────────────────────────────────
|
|
106
|
-
/** Fetch persisted message history for this channel (requires persist: true on publish) */
|
|
107
|
-
async getHistory(limit: number = 50, before?: number): Promise<HistoryMessage[]> {
|
|
108
|
-
return this.client._fetchHistory(this.topic, limit, before);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ─── Phase 3: Presence ────────────────────────────────────────────────
|
|
112
|
-
/** Track this user's presence state on this channel (auto-synced to subscribers) */
|
|
113
|
-
track(state: Record<string, any>): void {
|
|
114
|
-
this.client._send({
|
|
115
|
-
type: 'track',
|
|
116
|
-
topic: this.topic,
|
|
117
|
-
state,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Stop tracking presence on this channel */
|
|
122
|
-
untrack(): void {
|
|
123
|
-
this.client._send({
|
|
124
|
-
type: 'untrack',
|
|
125
|
-
topic: this.topic,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** @internal */
|
|
130
|
-
_emit(payload: RealtimePayload<T>): void {
|
|
131
|
-
// DB change events (INSERT/UPDATE/DELETE)
|
|
132
|
-
if (payload.operation) {
|
|
133
|
-
const event = payload.operation as string;
|
|
134
|
-
this.callbacks.get(event)?.forEach(cb => cb(payload));
|
|
135
|
-
}
|
|
136
|
-
// Custom named events ('player-moved', 'presence:join', etc.)
|
|
137
|
-
if (payload.event) {
|
|
138
|
-
this.callbacks.get(payload.event)?.forEach(cb => cb(payload));
|
|
139
|
-
}
|
|
140
|
-
// Catch-all
|
|
141
|
-
this.callbacks.get('*')?.forEach(cb => cb(payload));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export type RealtimeStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
|
|
146
|
-
|
|
147
|
-
export interface RealtimeClientOptions {
|
|
148
|
-
baseUrl: string;
|
|
149
|
-
projectId: string;
|
|
150
|
-
token?: string;
|
|
151
|
-
userId?: string;
|
|
152
|
-
apiKey?: string;
|
|
153
|
-
/** Max reconnect attempts before giving up (default: Infinity) */
|
|
154
|
-
maxReconnectAttempts?: number;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export class RealtimeClient {
|
|
158
|
-
private baseUrl: string;
|
|
159
|
-
private projectId: string;
|
|
160
|
-
private token?: string;
|
|
161
|
-
private userId?: string;
|
|
162
|
-
private apiKey?: string;
|
|
163
|
-
private ws: WebSocket | null = null;
|
|
164
|
-
private subscriptions: Map<string, RealtimeSubscription> = new Map();
|
|
165
|
-
private reconnectTimer: any = null;
|
|
166
|
-
private heartbeatTimer: any = null;
|
|
167
|
-
private reconnectAttempts: number = 0;
|
|
168
|
-
private _sendQueue: any[] = [];
|
|
169
|
-
private _connectingPromise: Promise<void> | null = null;
|
|
170
|
-
private _status: RealtimeStatus = 'idle';
|
|
171
|
-
private _statusListeners: Set<(s: RealtimeStatus) => void> = new Set();
|
|
172
|
-
// HTTP base URL for REST endpoints (history, etc.)
|
|
173
|
-
private _httpBaseUrl: string;
|
|
174
|
-
// Pong tracking
|
|
175
|
-
private _lastPong: number = 0;
|
|
176
|
-
// Max reconnect attempts
|
|
177
|
-
private _maxReconnectAttempts: number;
|
|
178
|
-
private _maxRetriesListeners: Set<() => void> = new Set();
|
|
179
|
-
|
|
180
|
-
constructor(options: RealtimeClientOptions) {
|
|
181
|
-
const wsBase = options.baseUrl.replace(/\/v1\/?$/, '').replace(/^http/, 'ws');
|
|
182
|
-
this.baseUrl = `${wsBase}/api/realtime`;
|
|
183
|
-
this._httpBaseUrl = options.baseUrl.replace(/\/v1\/?$/, '');
|
|
184
|
-
this.projectId = options.projectId;
|
|
185
|
-
this.token = options.token;
|
|
186
|
-
this.userId = options.userId;
|
|
187
|
-
this.apiKey = options.apiKey;
|
|
188
|
-
this._maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
get status(): RealtimeStatus { return this._status; }
|
|
192
|
-
get connected(): boolean { return this._status === 'connected'; }
|
|
193
|
-
|
|
194
|
-
/** Subscribe to connection status changes. Returns unsubscribe fn. */
|
|
195
|
-
onStatusChange(cb: (status: RealtimeStatus) => void): () => void {
|
|
196
|
-
this._statusListeners.add(cb);
|
|
197
|
-
return () => this._statusListeners.delete(cb);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** Called when max reconnect attempts exceeded. Returns unsubscribe fn. */
|
|
201
|
-
onMaxRetriesExceeded(cb: () => void): () => void {
|
|
202
|
-
this._maxRetriesListeners.add(cb);
|
|
203
|
-
return () => this._maxRetriesListeners.delete(cb);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private _setStatus(s: RealtimeStatus) {
|
|
207
|
-
this._status = s;
|
|
208
|
-
this._statusListeners.forEach(cb => cb(s));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Update the auth token on a live connection */
|
|
212
|
-
setToken(newToken: string): void {
|
|
213
|
-
this.token = newToken;
|
|
214
|
-
this._send({ type: 'auth', token: newToken });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async connect(): Promise<void> {
|
|
218
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
219
|
-
if (this._connectingPromise) return this._connectingPromise;
|
|
220
|
-
this._connectingPromise = this._doConnect().finally(() => {
|
|
221
|
-
this._connectingPromise = null;
|
|
222
|
-
});
|
|
223
|
-
return this._connectingPromise;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private _doConnect(): Promise<void> {
|
|
227
|
-
this._setStatus('connecting');
|
|
228
|
-
return new Promise((resolve, reject) => {
|
|
229
|
-
const url = new URL(this.baseUrl);
|
|
230
|
-
url.searchParams.set('projectId', this.projectId);
|
|
231
|
-
if (this.userId) url.searchParams.set('userId', this.userId);
|
|
232
|
-
|
|
233
|
-
// SECURITY: Pass credentials via Sec-WebSocket-Protocol header — never as URL query params
|
|
234
|
-
// (URL params appear in CDN logs, browser history, and Referer headers).
|
|
235
|
-
const protocols: string[] = [];
|
|
236
|
-
if (this.apiKey) protocols.push(`aerostack-key.${this.apiKey}`);
|
|
237
|
-
if (this.token) protocols.push(`aerostack-token.${this.token}`);
|
|
238
|
-
if (protocols.length > 0) protocols.push('aerostack-v1');
|
|
239
|
-
|
|
240
|
-
this.ws = protocols.length > 0
|
|
241
|
-
? new WebSocket(url.toString(), protocols)
|
|
242
|
-
: new WebSocket(url.toString());
|
|
243
|
-
|
|
244
|
-
this.ws.onopen = () => {
|
|
245
|
-
this._setStatus('connected');
|
|
246
|
-
this.reconnectAttempts = 0;
|
|
247
|
-
this._lastPong = Date.now();
|
|
248
|
-
this.startHeartbeat();
|
|
249
|
-
this._setupOfflineDetection();
|
|
250
|
-
// Flush queued messages
|
|
251
|
-
while (this._sendQueue.length > 0) {
|
|
252
|
-
this.ws!.send(JSON.stringify(this._sendQueue.shift()));
|
|
253
|
-
}
|
|
254
|
-
for (const sub of this.subscriptions.values()) {
|
|
255
|
-
sub.subscribe();
|
|
256
|
-
}
|
|
257
|
-
resolve();
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
this.ws.onmessage = (event) => {
|
|
261
|
-
try {
|
|
262
|
-
const data = JSON.parse(event.data);
|
|
263
|
-
this.handleMessage(data);
|
|
264
|
-
} catch (e) {
|
|
265
|
-
console.error('Realtime message parse error:', e);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
this.ws.onclose = () => {
|
|
270
|
-
this._setStatus('reconnecting');
|
|
271
|
-
this.stopHeartbeat();
|
|
272
|
-
this.ws = null;
|
|
273
|
-
this.scheduleReconnect();
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
this.ws.onerror = (err) => {
|
|
277
|
-
console.error('Realtime connection error:', err);
|
|
278
|
-
this._setStatus('disconnected');
|
|
279
|
-
reject(err);
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
disconnect() {
|
|
285
|
-
this._setStatus('disconnected');
|
|
286
|
-
this.stopReconnect();
|
|
287
|
-
this.stopHeartbeat();
|
|
288
|
-
this._teardownOfflineDetection();
|
|
289
|
-
if (this.ws) {
|
|
290
|
-
this.ws.close();
|
|
291
|
-
this.ws = null;
|
|
292
|
-
}
|
|
293
|
-
this._sendQueue = [];
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
channel<T = any>(topic: string, options: RealtimeSubscriptionOptions = {}): RealtimeSubscription<T> {
|
|
297
|
-
let fullTopic: string;
|
|
298
|
-
if (!topic.includes('/')) {
|
|
299
|
-
fullTopic = `table/${topic}/${this.projectId}`;
|
|
300
|
-
} else if (this.projectId && topic.endsWith(`/${this.projectId}`)) {
|
|
301
|
-
fullTopic = topic; // already fully qualified
|
|
302
|
-
} else {
|
|
303
|
-
fullTopic = `${topic}/${this.projectId}`; // e.g. 'table/orders' → 'table/orders/<projectId>'
|
|
304
|
-
}
|
|
305
|
-
let sub = this.subscriptions.get(fullTopic);
|
|
306
|
-
if (!sub) {
|
|
307
|
-
sub = new RealtimeSubscription<T>(this, fullTopic, options);
|
|
308
|
-
this.subscriptions.set(fullTopic, sub);
|
|
309
|
-
}
|
|
310
|
-
return sub as RealtimeSubscription<T>;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** Legacy: send a chat message (now persisted to DB) */
|
|
314
|
-
sendChat(roomId: string, text: string, metadata?: Record<string, any>): void {
|
|
315
|
-
this._send({ type: 'chat', roomId, text, metadata });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** Legacy: get a chat room subscription */
|
|
319
|
-
chatRoom(roomId: string): RealtimeSubscription {
|
|
320
|
-
return this.channel(`chat/${roomId}/${this.projectId}`);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** @internal — Generate unique message ID for ack tracking */
|
|
324
|
-
_generateId(): string {
|
|
325
|
-
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** @internal — Remove a subscription from the map (called on unsubscribe) */
|
|
329
|
-
_removeSubscription(topic: string): void {
|
|
330
|
-
this.subscriptions.delete(topic);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** @internal */
|
|
334
|
-
_send(data: any) {
|
|
335
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
336
|
-
this.ws.send(JSON.stringify(data));
|
|
337
|
-
} else {
|
|
338
|
-
this._sendQueue.push(data);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/** @internal — Fetch chat/event history via REST API */
|
|
343
|
-
async _fetchHistory(room: string, limit: number = 50, before?: number): Promise<HistoryMessage[]> {
|
|
344
|
-
const url = new URL(`${this._httpBaseUrl}/api/v1/public/realtime/history`);
|
|
345
|
-
url.searchParams.set('room', room);
|
|
346
|
-
url.searchParams.set('limit', String(limit));
|
|
347
|
-
if (before) url.searchParams.set('before', String(before));
|
|
348
|
-
|
|
349
|
-
const headers: Record<string, string> = {};
|
|
350
|
-
if (this.apiKey) headers['X-Aerostack-Key'] = this.apiKey;
|
|
351
|
-
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
|
|
352
|
-
|
|
353
|
-
const res = await fetch(url.toString(), { headers });
|
|
354
|
-
const json = await res.json() as any;
|
|
355
|
-
return json.messages || [];
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private handleMessage(data: RealtimeMessage) {
|
|
359
|
-
// Track pong for liveness
|
|
360
|
-
if (data.type === 'pong') {
|
|
361
|
-
this._lastPong = Date.now();
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Ack (fire-and-forget acknowledgment from server)
|
|
366
|
-
if (data.type === 'ack') {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Route to subscription: db_change, chat_message, event, presence:*
|
|
371
|
-
if (data.type === 'db_change' || data.type === 'chat_message' || data.type === 'event') {
|
|
372
|
-
const sub = this.subscriptions.get(data.topic);
|
|
373
|
-
if (sub) {
|
|
374
|
-
sub._emit(data as any);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Re-key subscription on server-confirmed topic (for non-TS SDKs compatibility)
|
|
379
|
-
if (data.type === 'subscribed' && data.topic) {
|
|
380
|
-
for (const [origTopic, sub] of this.subscriptions.entries()) {
|
|
381
|
-
if (data.topic !== origTopic && data.topic.startsWith(origTopic)) {
|
|
382
|
-
this.subscriptions.delete(origTopic);
|
|
383
|
-
sub.topic = data.topic;
|
|
384
|
-
this.subscriptions.set(data.topic, sub);
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private startHeartbeat() {
|
|
392
|
-
this.heartbeatTimer = setInterval(() => {
|
|
393
|
-
this._send({ type: 'ping' });
|
|
394
|
-
if (this._lastPong > 0 && Date.now() - this._lastPong > 70000) {
|
|
395
|
-
console.warn('Realtime: no pong received, forcing reconnect');
|
|
396
|
-
this.ws?.close();
|
|
397
|
-
}
|
|
398
|
-
}, 30000);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
private stopHeartbeat() {
|
|
402
|
-
if (this.heartbeatTimer) {
|
|
403
|
-
clearInterval(this.heartbeatTimer);
|
|
404
|
-
this.heartbeatTimer = null;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private scheduleReconnect() {
|
|
409
|
-
this.stopReconnect();
|
|
410
|
-
if (this.reconnectAttempts >= this._maxReconnectAttempts) {
|
|
411
|
-
this._setStatus('disconnected');
|
|
412
|
-
this._maxRetriesListeners.forEach(cb => cb());
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
416
|
-
const jitter = delay * 0.3 * Math.random();
|
|
417
|
-
this.reconnectAttempts++;
|
|
418
|
-
this.reconnectTimer = setTimeout(() => {
|
|
419
|
-
this.connect().catch(() => { });
|
|
420
|
-
}, delay + jitter);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
private stopReconnect() {
|
|
424
|
-
if (this.reconnectTimer) {
|
|
425
|
-
clearTimeout(this.reconnectTimer);
|
|
426
|
-
this.reconnectTimer = null;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Offline/online detection (browser only)
|
|
431
|
-
private _handleOnline = () => {
|
|
432
|
-
if (this._status !== 'connected') {
|
|
433
|
-
this.reconnectAttempts = 0;
|
|
434
|
-
this.connect().catch(() => { });
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
private _handleOffline = () => {
|
|
439
|
-
this.stopReconnect();
|
|
440
|
-
this._setStatus('disconnected');
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
private _setupOfflineDetection() {
|
|
444
|
-
if (typeof window !== 'undefined') {
|
|
445
|
-
window.addEventListener('online', this._handleOnline);
|
|
446
|
-
window.addEventListener('offline', this._handleOffline);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
private _teardownOfflineDetection() {
|
|
451
|
-
if (typeof window !== 'undefined') {
|
|
452
|
-
window.removeEventListener('online', this._handleOnline);
|
|
453
|
-
window.removeEventListener('offline', this._handleOffline);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
package/src/sdk.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import * as gen from './_generated/index.js';
|
|
2
|
-
import { RealtimeClient } from './realtime.js';
|
|
3
|
-
|
|
4
|
-
export interface SDKOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Aerostack API Key.
|
|
7
|
-
* Use a Public Key for client-side environments.
|
|
8
|
-
*/
|
|
9
|
-
apiKey?: string;
|
|
10
|
-
/** Alias for apiKey for backward compatibility */
|
|
11
|
-
apiKeyAuth?: string;
|
|
12
|
-
serverUrl?: string;
|
|
13
|
-
/** Alias for serverUrl for backward compatibility */
|
|
14
|
-
serverURL?: string;
|
|
15
|
-
maxReconnectAttempts?: number;
|
|
16
|
-
projectId?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Compatibility wrapper for Database API
|
|
21
|
-
*/
|
|
22
|
-
class DatabaseFacade {
|
|
23
|
-
constructor(private api: gen.DatabaseApi) { }
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Run a SQL query against your project database
|
|
27
|
-
*/
|
|
28
|
-
async dbQuery(params: {
|
|
29
|
-
dbQueryRequest?: gen.DbQueryRequest,
|
|
30
|
-
requestBody?: gen.DbQueryRequest,
|
|
31
|
-
xSDKVersion?: string,
|
|
32
|
-
xRequestID?: string
|
|
33
|
-
}) {
|
|
34
|
-
return this.api.dbQuery({
|
|
35
|
-
dbQueryRequest: params.dbQueryRequest || params.requestBody!,
|
|
36
|
-
xSDKVersion: params.xSDKVersion,
|
|
37
|
-
xRequestID: params.xRequestID
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Aerostack SDK Facade for Web/Browser.
|
|
44
|
-
* Provides a clean, ergonomic API for client-safe Aerostack services.
|
|
45
|
-
*/
|
|
46
|
-
export class SDK {
|
|
47
|
-
public readonly auth: gen.AuthenticationApi;
|
|
48
|
-
public readonly ai: gen.AIApi;
|
|
49
|
-
public readonly storage: gen.StorageApi;
|
|
50
|
-
public readonly realtime: RealtimeClient;
|
|
51
|
-
public readonly database: DatabaseFacade;
|
|
52
|
-
|
|
53
|
-
private config: gen.Configuration;
|
|
54
|
-
|
|
55
|
-
constructor(options: SDKOptions = {}) {
|
|
56
|
-
const serverUrl = options.serverUrl || options.serverURL || 'https://api.aerostack.dev/v1';
|
|
57
|
-
const apiKey = options.apiKey || options.apiKeyAuth;
|
|
58
|
-
|
|
59
|
-
this.config = new gen.Configuration({
|
|
60
|
-
basePath: serverUrl,
|
|
61
|
-
headers: apiKey ? { 'X-Aerostack-Key': apiKey } : {},
|
|
62
|
-
apiKey: apiKey,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
this.auth = new gen.AuthenticationApi(this.config);
|
|
66
|
-
this.ai = new gen.AIApi(this.config);
|
|
67
|
-
this.storage = new gen.StorageApi(this.config);
|
|
68
|
-
this.database = new DatabaseFacade(new gen.DatabaseApi(this.config));
|
|
69
|
-
|
|
70
|
-
this.realtime = new RealtimeClient({
|
|
71
|
-
baseUrl: serverUrl,
|
|
72
|
-
apiKey: apiKey,
|
|
73
|
-
projectId: options.projectId || '',
|
|
74
|
-
maxReconnectAttempts: options.maxReconnectAttempts
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Stream a gateway chat completion with token-by-token callbacks.
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* await sdk.streamGateway({
|
|
83
|
-
* apiSlug: 'my-chatbot',
|
|
84
|
-
* messages: [{ role: 'user', content: 'Hello' }],
|
|
85
|
-
* consumerKey: 'ask_live_...',
|
|
86
|
-
* onToken: (delta) => process.stdout.write(delta),
|
|
87
|
-
* });
|
|
88
|
-
*/
|
|
89
|
-
async streamGateway(opts: {
|
|
90
|
-
apiSlug: string;
|
|
91
|
-
messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>;
|
|
92
|
-
consumerKey?: string;
|
|
93
|
-
token?: string;
|
|
94
|
-
systemPrompt?: string;
|
|
95
|
-
onToken?: (delta: string) => void;
|
|
96
|
-
onDone?: (usage: { tokensUsed: number }) => void;
|
|
97
|
-
onError?: (error: Error) => void;
|
|
98
|
-
signal?: AbortSignal;
|
|
99
|
-
}): Promise<{ text: string; tokensUsed: number }> {
|
|
100
|
-
const baseUrl = this.config.basePath.replace(/\/v1\/?$/, '');
|
|
101
|
-
const endpoint = `${baseUrl}/api/gateway/${opts.apiSlug}/v1/chat/completions`;
|
|
102
|
-
|
|
103
|
-
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
104
|
-
if (opts.consumerKey) {
|
|
105
|
-
headers['Authorization'] = `Bearer ${opts.consumerKey}`;
|
|
106
|
-
} else if (opts.token) {
|
|
107
|
-
headers['Authorization'] = `Bearer ${opts.token}`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const messages = opts.systemPrompt
|
|
111
|
-
? [{ role: 'system' as const, content: opts.systemPrompt }, ...opts.messages]
|
|
112
|
-
: opts.messages;
|
|
113
|
-
|
|
114
|
-
let text = '';
|
|
115
|
-
let totalTokens = 0;
|
|
116
|
-
let estimatedTokens = 0;
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
const response = await fetch(endpoint, {
|
|
120
|
-
method: 'POST',
|
|
121
|
-
headers,
|
|
122
|
-
body: JSON.stringify({ messages, stream: true, stream_options: { include_usage: true } }),
|
|
123
|
-
signal: opts.signal,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (!response.ok) {
|
|
127
|
-
const err = await response.json().catch(() => ({ error: 'Request failed' }));
|
|
128
|
-
throw new Error((err as any).error || `HTTP ${response.status}`);
|
|
129
|
-
}
|
|
130
|
-
if (!response.body) throw new Error('No response body');
|
|
131
|
-
|
|
132
|
-
const reader = response.body.getReader();
|
|
133
|
-
const decoder = new TextDecoder();
|
|
134
|
-
let buffer = '';
|
|
135
|
-
|
|
136
|
-
while (true) {
|
|
137
|
-
const { done, value } = await reader.read();
|
|
138
|
-
if (done) break;
|
|
139
|
-
buffer += decoder.decode(value, { stream: true });
|
|
140
|
-
const lines = buffer.split('\n');
|
|
141
|
-
buffer = lines.pop() || '';
|
|
142
|
-
|
|
143
|
-
for (const line of lines) {
|
|
144
|
-
if (!line.startsWith('data: ')) continue;
|
|
145
|
-
const payload = line.slice(6).trim();
|
|
146
|
-
if (payload === '[DONE]') {
|
|
147
|
-
reader.cancel();
|
|
148
|
-
const result = { text, tokensUsed: totalTokens || estimatedTokens };
|
|
149
|
-
opts.onDone?.(result);
|
|
150
|
-
return result;
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
const parsed = JSON.parse(payload);
|
|
154
|
-
const delta = parsed.choices?.[0]?.delta?.content;
|
|
155
|
-
if (delta) {
|
|
156
|
-
text += delta;
|
|
157
|
-
opts.onToken?.(delta);
|
|
158
|
-
estimatedTokens += Math.ceil(delta.length / 4);
|
|
159
|
-
}
|
|
160
|
-
if (parsed.usage?.total_tokens) totalTokens = parsed.usage.total_tokens;
|
|
161
|
-
else if (parsed.usage?.completion_tokens) totalTokens = parsed.usage.completion_tokens;
|
|
162
|
-
} catch { /* skip malformed frames */ }
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const result = { text, tokensUsed: totalTokens || estimatedTokens };
|
|
167
|
-
opts.onDone?.(result);
|
|
168
|
-
return result;
|
|
169
|
-
} catch (err: any) {
|
|
170
|
-
if (err.name === 'AbortError') return { text, tokensUsed: totalTokens || estimatedTokens };
|
|
171
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
172
|
-
opts.onError?.(error);
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Update the API key for subsequent requests.
|
|
179
|
-
*/
|
|
180
|
-
setApiKey(apiKey: string): void {
|
|
181
|
-
this.config = new gen.Configuration({
|
|
182
|
-
...this.config,
|
|
183
|
-
headers: { ...this.config.headers, 'X-Aerostack-Key': apiKey },
|
|
184
|
-
apiKey,
|
|
185
|
-
});
|
|
186
|
-
(this as any).auth = new gen.AuthenticationApi(this.config);
|
|
187
|
-
(this as any).ai = new gen.AIApi(this.config);
|
|
188
|
-
(this as any).storage = new gen.StorageApi(this.config);
|
|
189
|
-
(this as any).database = new DatabaseFacade(new gen.DatabaseApi(this.config));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** @deprecated Use SDK instead */
|
|
194
|
-
export const Aerostack = SDK;
|
|
195
|
-
|
|
196
|
-
// Export a default instance factory or just the class
|
|
197
|
-
export const createClient = (options: SDKOptions) => new SDK(options);
|
package/tsconfig.json
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"incremental": false,
|
|
4
|
-
"target": "ES2020",
|
|
5
|
-
"lib": [
|
|
6
|
-
"ES2022",
|
|
7
|
-
"DOM",
|
|
8
|
-
"DOM.Iterable"
|
|
9
|
-
],
|
|
10
|
-
"jsx": "react-jsx",
|
|
11
|
-
"module": "Node16",
|
|
12
|
-
"moduleResolution": "Node16",
|
|
13
|
-
"allowJs": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"declarationMap": true,
|
|
16
|
-
"sourceMap": true,
|
|
17
|
-
"outDir": ".",
|
|
18
|
-
// https://github.com/tsconfig/bases/blob/a1bf7c0fa2e094b068ca3e1448ca2ece4157977e/bases/strictest.json
|
|
19
|
-
"strict": true,
|
|
20
|
-
"allowUnusedLabels": false,
|
|
21
|
-
"allowUnreachableCode": false,
|
|
22
|
-
"noImplicitOverride": false,
|
|
23
|
-
"noImplicitReturns": true,
|
|
24
|
-
"noPropertyAccessFromIndexSignature": true,
|
|
25
|
-
"noUncheckedIndexedAccess": true,
|
|
26
|
-
"noUnusedLocals": false,
|
|
27
|
-
"noUnusedParameters": false,
|
|
28
|
-
"exactOptionalPropertyTypes": false,
|
|
29
|
-
"isolatedModules": true,
|
|
30
|
-
"checkJs": true,
|
|
31
|
-
"esModuleInterop": true,
|
|
32
|
-
"skipLibCheck": true,
|
|
33
|
-
"forceConsistentCasingInFileNames": true
|
|
34
|
-
},
|
|
35
|
-
"include": [
|
|
36
|
-
"src"
|
|
37
|
-
],
|
|
38
|
-
"exclude": [
|
|
39
|
-
"node_modules"
|
|
40
|
-
]
|
|
41
|
-
}
|