kalam-link 0.2.0-alpha1
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 +17 -0
- package/README.md +423 -0
- package/dist/auth.d.ts +113 -0
- package/dist/auth.js +131 -0
- package/dist/index.d.ts +781 -0
- package/dist/index.js +683 -0
- package/dist/wasm/README.md +460 -0
- package/dist/wasm/kalam_link.d.ts +368 -0
- package/dist/wasm/kalam_link.js +1237 -0
- package/dist/wasm/kalam_link_bg.wasm +0 -0
- package/dist/wasm/kalam_link_bg.wasm.d.ts +45 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kalam-link - Official TypeScript/JavaScript client for KalamDB
|
|
3
|
+
*
|
|
4
|
+
* This package provides a type-safe wrapper around the KalamDB WASM bindings
|
|
5
|
+
* for use in Node.js and browser environments.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - SQL query execution via HTTP
|
|
9
|
+
* - Real-time subscriptions via WebSocket (single connection, multiple subscriptions)
|
|
10
|
+
* - Subscription management with modern patterns (unsubscribe functions)
|
|
11
|
+
* - Cross-platform support (Node.js & Browser)
|
|
12
|
+
* - Type-safe authentication with multiple providers (Basic Auth, JWT, Anonymous)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { createClient, Auth } from 'kalam-link';
|
|
17
|
+
*
|
|
18
|
+
* // Basic Auth (username/password)
|
|
19
|
+
* const client = createClient({
|
|
20
|
+
* url: 'http://localhost:8080',
|
|
21
|
+
* auth: Auth.basic('admin', 'admin')
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // JWT Token Auth
|
|
25
|
+
* const jwtClient = createClient({
|
|
26
|
+
* url: 'http://localhost:8080',
|
|
27
|
+
* auth: Auth.jwt('eyJhbGciOiJIUzI1NiIs...')
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Anonymous (localhost bypass)
|
|
31
|
+
* const anonClient = createClient({
|
|
32
|
+
* url: 'http://localhost:8080',
|
|
33
|
+
* auth: Auth.none()
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* await client.connect();
|
|
37
|
+
*
|
|
38
|
+
* // Subscribe to changes (returns unsubscribe function - Firebase/Supabase style)
|
|
39
|
+
* const unsubscribe = await client.subscribe('messages', (event) => {
|
|
40
|
+
* console.log('Change:', event);
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Check subscription count
|
|
44
|
+
* console.log(`Active subscriptions: ${client.getSubscriptionCount()}`);
|
|
45
|
+
*
|
|
46
|
+
* // Later: unsubscribe when done
|
|
47
|
+
* await unsubscribe();
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
import init, { KalamClient as WasmClient } from './wasm/kalam_link.js';
|
|
51
|
+
// Re-export authentication types
|
|
52
|
+
export { Auth, buildAuthHeader, encodeBasicAuth, isAuthenticated, isBasicAuth, isJwtAuth, isNoAuth } from './auth.js';
|
|
53
|
+
/**
|
|
54
|
+
* Message type enum for WebSocket subscription events
|
|
55
|
+
*/
|
|
56
|
+
export var MessageType;
|
|
57
|
+
(function (MessageType) {
|
|
58
|
+
MessageType["SubscriptionAck"] = "subscription_ack";
|
|
59
|
+
MessageType["InitialDataBatch"] = "initial_data_batch";
|
|
60
|
+
MessageType["Change"] = "change";
|
|
61
|
+
MessageType["Error"] = "error";
|
|
62
|
+
})(MessageType || (MessageType = {}));
|
|
63
|
+
/**
|
|
64
|
+
* Change type enum for live subscription change events
|
|
65
|
+
*/
|
|
66
|
+
export var ChangeType;
|
|
67
|
+
(function (ChangeType) {
|
|
68
|
+
ChangeType["Insert"] = "insert";
|
|
69
|
+
ChangeType["Update"] = "update";
|
|
70
|
+
ChangeType["Delete"] = "delete";
|
|
71
|
+
})(ChangeType || (ChangeType = {}));
|
|
72
|
+
/**
|
|
73
|
+
* Batch loading status enum
|
|
74
|
+
*/
|
|
75
|
+
export var BatchStatus;
|
|
76
|
+
(function (BatchStatus) {
|
|
77
|
+
BatchStatus["Loading"] = "loading";
|
|
78
|
+
BatchStatus["LoadingBatch"] = "loading_batch";
|
|
79
|
+
BatchStatus["Ready"] = "ready";
|
|
80
|
+
})(BatchStatus || (BatchStatus = {}));
|
|
81
|
+
/**
|
|
82
|
+
* Type guard to check if options use the new auth API
|
|
83
|
+
*/
|
|
84
|
+
function isAuthOptions(options) {
|
|
85
|
+
return 'auth' in options;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* KalamDB Client - TypeScript wrapper around WASM bindings
|
|
89
|
+
*
|
|
90
|
+
* Provides a type-safe interface to KalamDB with support for:
|
|
91
|
+
* - SQL query execution
|
|
92
|
+
* - Real-time WebSocket subscriptions
|
|
93
|
+
* - Multiple authentication methods (Basic Auth, JWT, Anonymous)
|
|
94
|
+
* - Cross-platform (Node.js & Browser)
|
|
95
|
+
* - Subscription tracking and management
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* // New API with type-safe auth (recommended)
|
|
100
|
+
* import { createClient, Auth } from 'kalam-link';
|
|
101
|
+
*
|
|
102
|
+
* const client = createClient({
|
|
103
|
+
* url: 'http://localhost:8080',
|
|
104
|
+
* auth: Auth.basic('alice', 'password123')
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* // JWT authentication
|
|
108
|
+
* const jwtClient = createClient({
|
|
109
|
+
* url: 'http://localhost:8080',
|
|
110
|
+
* auth: Auth.jwt('eyJhbGciOiJIUzI1NiIs...')
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* // Anonymous (no auth - localhost bypass)
|
|
114
|
+
* const anonClient = createClient({
|
|
115
|
+
* url: 'http://localhost:8080',
|
|
116
|
+
* auth: Auth.none()
|
|
117
|
+
* });
|
|
118
|
+
*
|
|
119
|
+
* await client.connect();
|
|
120
|
+
*
|
|
121
|
+
* // Execute queries
|
|
122
|
+
* const users = await client.query('SELECT * FROM users WHERE active = true');
|
|
123
|
+
* console.log(users.results[0].rows);
|
|
124
|
+
*
|
|
125
|
+
* // Subscribe to changes (returns unsubscribe function)
|
|
126
|
+
* const unsubscribe = await client.subscribe('messages', (event) => {
|
|
127
|
+
* if (event.type === 'change') {
|
|
128
|
+
* console.log('New message:', event.rows);
|
|
129
|
+
* }
|
|
130
|
+
* });
|
|
131
|
+
*
|
|
132
|
+
* // Check subscription count
|
|
133
|
+
* console.log(`Active: ${client.getSubscriptionCount()}`);
|
|
134
|
+
*
|
|
135
|
+
* // Cleanup
|
|
136
|
+
* await unsubscribe();
|
|
137
|
+
* await client.disconnect();
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export class KalamDBClient {
|
|
141
|
+
constructor(urlOrOptions, username, password) {
|
|
142
|
+
this.wasmClient = null;
|
|
143
|
+
this.initialized = false;
|
|
144
|
+
/** Track active subscriptions for management */
|
|
145
|
+
this.subscriptions = new Map();
|
|
146
|
+
// Handle new options-based API
|
|
147
|
+
if (typeof urlOrOptions === 'object') {
|
|
148
|
+
if (!urlOrOptions.url)
|
|
149
|
+
throw new Error('KalamDBClient: url is required');
|
|
150
|
+
if (!urlOrOptions.auth)
|
|
151
|
+
throw new Error('KalamDBClient: auth is required');
|
|
152
|
+
this.url = urlOrOptions.url;
|
|
153
|
+
this.auth = urlOrOptions.auth;
|
|
154
|
+
}
|
|
155
|
+
// Handle legacy API (string url, username, password)
|
|
156
|
+
else {
|
|
157
|
+
if (!urlOrOptions)
|
|
158
|
+
throw new Error('KalamDBClient: url parameter is required');
|
|
159
|
+
if (!username)
|
|
160
|
+
throw new Error('KalamDBClient: username parameter is required');
|
|
161
|
+
if (!password)
|
|
162
|
+
throw new Error('KalamDBClient: password parameter is required');
|
|
163
|
+
this.url = urlOrOptions;
|
|
164
|
+
// Convert legacy API to new auth format
|
|
165
|
+
this.auth = { type: 'basic', username, password };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get the current authentication type
|
|
170
|
+
*
|
|
171
|
+
* @returns 'basic', 'jwt', or 'none'
|
|
172
|
+
*/
|
|
173
|
+
getAuthType() {
|
|
174
|
+
return this.auth.type;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Initialize WASM module and create client instance
|
|
178
|
+
*
|
|
179
|
+
* Must be called before any other operations. Automatically called by connect()
|
|
180
|
+
* if not already initialized.
|
|
181
|
+
*
|
|
182
|
+
* @throws Error if WASM initialization fails
|
|
183
|
+
*/
|
|
184
|
+
async initialize() {
|
|
185
|
+
if (this.initialized)
|
|
186
|
+
return;
|
|
187
|
+
try {
|
|
188
|
+
// Browser environment - WASM will be fetched automatically
|
|
189
|
+
await init();
|
|
190
|
+
// Create WASM client based on auth type
|
|
191
|
+
switch (this.auth.type) {
|
|
192
|
+
case 'basic':
|
|
193
|
+
this.wasmClient = new WasmClient(this.url, this.auth.username, this.auth.password);
|
|
194
|
+
break;
|
|
195
|
+
case 'jwt':
|
|
196
|
+
this.wasmClient = WasmClient.withJwt(this.url, this.auth.token);
|
|
197
|
+
break;
|
|
198
|
+
case 'none':
|
|
199
|
+
this.wasmClient = WasmClient.anonymous(this.url);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
this.initialized = true;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw new Error(`Failed to initialize WASM client: ${error}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Connect to KalamDB server via WebSocket
|
|
210
|
+
*
|
|
211
|
+
* Establishes a persistent WebSocket connection for real-time subscriptions.
|
|
212
|
+
* Also initializes the WASM module if not already done.
|
|
213
|
+
*
|
|
214
|
+
* @throws Error if connection fails
|
|
215
|
+
*/
|
|
216
|
+
async connect() {
|
|
217
|
+
await this.initialize();
|
|
218
|
+
if (!this.wasmClient) {
|
|
219
|
+
throw new Error('WASM client not initialized');
|
|
220
|
+
}
|
|
221
|
+
await this.wasmClient.connect();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Enable or disable automatic reconnection
|
|
225
|
+
*
|
|
226
|
+
* When enabled, the client will automatically attempt to reconnect
|
|
227
|
+
* if the WebSocket connection is lost, and will re-subscribe to all
|
|
228
|
+
* active subscriptions with resume_from_seq_id to catch up on missed events.
|
|
229
|
+
*
|
|
230
|
+
* @param enabled - Whether to automatically reconnect on connection loss
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* client.setAutoReconnect(true); // Enable (default)
|
|
235
|
+
* client.setAutoReconnect(false); // Disable for manual control
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
setAutoReconnect(enabled) {
|
|
239
|
+
this.ensureInitialized();
|
|
240
|
+
this.wasmClient.setAutoReconnect(enabled);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Configure reconnection delay parameters
|
|
244
|
+
*
|
|
245
|
+
* The client uses exponential backoff starting from initialDelayMs,
|
|
246
|
+
* doubling each attempt up to maxDelayMs.
|
|
247
|
+
*
|
|
248
|
+
* @param initialDelayMs - Initial delay between reconnection attempts (default: 1000ms)
|
|
249
|
+
* @param maxDelayMs - Maximum delay for exponential backoff (default: 30000ms)
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```typescript
|
|
253
|
+
* // Start with 500ms delay, max out at 10 seconds
|
|
254
|
+
* client.setReconnectDelay(500, 10000);
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
setReconnectDelay(initialDelayMs, maxDelayMs) {
|
|
258
|
+
this.ensureInitialized();
|
|
259
|
+
this.wasmClient.setReconnectDelay(BigInt(initialDelayMs), BigInt(maxDelayMs));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Set maximum number of reconnection attempts
|
|
263
|
+
*
|
|
264
|
+
* @param maxAttempts - Maximum attempts before giving up (0 = infinite)
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* client.setMaxReconnectAttempts(5); // Give up after 5 attempts
|
|
269
|
+
* client.setMaxReconnectAttempts(0); // Never give up (default)
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
setMaxReconnectAttempts(maxAttempts) {
|
|
273
|
+
this.ensureInitialized();
|
|
274
|
+
this.wasmClient.setMaxReconnectAttempts(maxAttempts);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get the current number of reconnection attempts
|
|
278
|
+
*
|
|
279
|
+
* Resets to 0 after a successful reconnection.
|
|
280
|
+
*
|
|
281
|
+
* @returns Current reconnection attempt count
|
|
282
|
+
*/
|
|
283
|
+
getReconnectAttempts() {
|
|
284
|
+
this.ensureInitialized();
|
|
285
|
+
return this.wasmClient.getReconnectAttempts();
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Check if the client is currently attempting to reconnect
|
|
289
|
+
*
|
|
290
|
+
* @returns true if a reconnection is in progress
|
|
291
|
+
*/
|
|
292
|
+
isReconnecting() {
|
|
293
|
+
this.ensureInitialized();
|
|
294
|
+
return this.wasmClient.isReconnecting();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get the last received sequence ID for a subscription
|
|
298
|
+
*
|
|
299
|
+
* Useful for debugging or manual tracking of subscription progress.
|
|
300
|
+
* This seq_id is automatically used during reconnection to resume
|
|
301
|
+
* from where the subscription left off.
|
|
302
|
+
*
|
|
303
|
+
* @param subscriptionId - The subscription ID to query
|
|
304
|
+
* @returns The last seq_id as a string, or undefined if not set
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* const lastSeq = client.getLastSeqId(subscriptionId);
|
|
309
|
+
* console.log(`Last received seq: ${lastSeq}`);
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
getLastSeqId(subscriptionId) {
|
|
313
|
+
this.ensureInitialized();
|
|
314
|
+
return this.wasmClient.getLastSeqId(subscriptionId) ?? undefined;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Helper to ensure WASM client is initialized
|
|
318
|
+
* @private
|
|
319
|
+
*/
|
|
320
|
+
ensureInitialized() {
|
|
321
|
+
if (!this.wasmClient) {
|
|
322
|
+
throw new Error('WASM client not initialized. Call connect() first.');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Disconnect from KalamDB server
|
|
327
|
+
*
|
|
328
|
+
* Closes the WebSocket connection and cleans up all active subscriptions.
|
|
329
|
+
*/
|
|
330
|
+
async disconnect() {
|
|
331
|
+
if (this.wasmClient) {
|
|
332
|
+
await this.wasmClient.disconnect();
|
|
333
|
+
}
|
|
334
|
+
// Clear subscription tracking
|
|
335
|
+
this.subscriptions.clear();
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Check if client is currently connected
|
|
339
|
+
*
|
|
340
|
+
* @returns true if WebSocket connection is active, false otherwise
|
|
341
|
+
*/
|
|
342
|
+
isConnected() {
|
|
343
|
+
return this.wasmClient?.isConnected() ?? false;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Execute a SQL query with optional parameters
|
|
347
|
+
*
|
|
348
|
+
* Supports all SQL statements: SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, etc.
|
|
349
|
+
* Use parameterized queries to prevent SQL injection.
|
|
350
|
+
*
|
|
351
|
+
* @param sql - SQL query string (may contain $1, $2, ... placeholders)
|
|
352
|
+
* @param params - Optional array of parameter values for placeholders
|
|
353
|
+
* @returns Parsed query response with results
|
|
354
|
+
*
|
|
355
|
+
* @throws Error if query execution fails
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```typescript
|
|
359
|
+
* // Simple query
|
|
360
|
+
* const result = await client.query('SELECT * FROM users');
|
|
361
|
+
*
|
|
362
|
+
* // Parameterized query (recommended for user input)
|
|
363
|
+
* const users = await client.query(
|
|
364
|
+
* 'SELECT * FROM users WHERE id = $1 AND age > $2',
|
|
365
|
+
* [42, 18]
|
|
366
|
+
* );
|
|
367
|
+
* console.log(users.results[0].rows);
|
|
368
|
+
*
|
|
369
|
+
* // INSERT with parameters
|
|
370
|
+
* await client.query(
|
|
371
|
+
* "INSERT INTO users (name, email) VALUES ($1, $2)",
|
|
372
|
+
* ['Alice', 'alice@example.com']
|
|
373
|
+
* );
|
|
374
|
+
*
|
|
375
|
+
* // DDL statements (no params)
|
|
376
|
+
* await client.query('CREATE TABLE products (id BIGINT PRIMARY KEY, name TEXT)');
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
async query(sql, params) {
|
|
380
|
+
await this.initialize();
|
|
381
|
+
if (!this.wasmClient) {
|
|
382
|
+
throw new Error('WASM client not initialized');
|
|
383
|
+
}
|
|
384
|
+
let resultStr;
|
|
385
|
+
if (params && params.length > 0) {
|
|
386
|
+
resultStr = await this.wasmClient.queryWithParams(sql, JSON.stringify(params));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
resultStr = await this.wasmClient.query(sql);
|
|
390
|
+
}
|
|
391
|
+
return JSON.parse(resultStr);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Insert data into a table (convenience method)
|
|
395
|
+
*
|
|
396
|
+
* @param tableName - Name of the table (can include namespace, e.g., 'app.users')
|
|
397
|
+
* @param data - Object containing column values
|
|
398
|
+
* @returns Query response
|
|
399
|
+
*
|
|
400
|
+
* @throws Error if insert fails
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```typescript
|
|
404
|
+
* await client.insert('todos', {
|
|
405
|
+
* title: 'Buy groceries',
|
|
406
|
+
* completed: false
|
|
407
|
+
* });
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
async insert(tableName, data) {
|
|
411
|
+
const dataJson = JSON.stringify(data);
|
|
412
|
+
const resultStr = await this.wasmClient.insert(tableName, dataJson);
|
|
413
|
+
return JSON.parse(resultStr);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Delete a row from a table (convenience method)
|
|
417
|
+
*
|
|
418
|
+
* @param tableName - Name of the table
|
|
419
|
+
* @param rowId - ID of the row to delete
|
|
420
|
+
*
|
|
421
|
+
* @throws Error if delete fails
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```typescript
|
|
425
|
+
* await client.delete('todos', '123456789');
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
428
|
+
async delete(tableName, rowId) {
|
|
429
|
+
await this.wasmClient.delete(tableName, String(rowId));
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Subscribe to real-time changes in a table
|
|
433
|
+
*
|
|
434
|
+
* The callback will be invoked for:
|
|
435
|
+
* - Initial data batches (type: 'initial_data_batch')
|
|
436
|
+
* - Live changes (type: 'change')
|
|
437
|
+
* - Errors (type: 'error')
|
|
438
|
+
*
|
|
439
|
+
* Returns an unsubscribe function (Firebase/Supabase style) for easy cleanup.
|
|
440
|
+
*
|
|
441
|
+
* @param tableName - Name of the table to subscribe to
|
|
442
|
+
* @param callback - Function called when changes occur
|
|
443
|
+
* @param options - Optional subscription options (batch_size, last_rows, from_seq_id)
|
|
444
|
+
* @returns Unsubscribe function to stop receiving updates
|
|
445
|
+
*
|
|
446
|
+
* @throws Error if subscription fails or not connected
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```typescript
|
|
450
|
+
* // Simple subscription
|
|
451
|
+
* const unsubscribe = await client.subscribe('messages', (event) => {
|
|
452
|
+
* if (event.type === 'change') {
|
|
453
|
+
* console.log('New data:', event.rows);
|
|
454
|
+
* }
|
|
455
|
+
* });
|
|
456
|
+
*
|
|
457
|
+
* // With options
|
|
458
|
+
* const unsubscribe = await client.subscribe('messages', callback, {
|
|
459
|
+
* batch_size: 100, // Load initial data in batches of 100
|
|
460
|
+
* last_rows: 50 // Only fetch last 50 rows initially
|
|
461
|
+
* });
|
|
462
|
+
*
|
|
463
|
+
* // Later: unsubscribe when done
|
|
464
|
+
* await unsubscribe();
|
|
465
|
+
* ```
|
|
466
|
+
*/
|
|
467
|
+
async subscribe(tableName, callback, options) {
|
|
468
|
+
// Use subscribeWithSql internally with SELECT * FROM tableName
|
|
469
|
+
const sql = `SELECT * FROM ${tableName}`;
|
|
470
|
+
return this.subscribeWithSql(sql, callback, options);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Subscribe to a SQL query with real-time updates
|
|
474
|
+
*
|
|
475
|
+
* More flexible than subscribe() - allows custom SQL queries with WHERE clauses,
|
|
476
|
+
* JOINs, and other SQL features.
|
|
477
|
+
*
|
|
478
|
+
* @param sql - SQL SELECT query to subscribe to
|
|
479
|
+
* @param callback - Function called when changes occur
|
|
480
|
+
* @param options - Optional subscription options (batch_size, last_rows, from_seq_id)
|
|
481
|
+
* @returns Unsubscribe function to stop receiving updates
|
|
482
|
+
*
|
|
483
|
+
* @throws Error if subscription fails or not connected
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```typescript
|
|
487
|
+
* // Subscribe to filtered query
|
|
488
|
+
* const unsubscribe = await client.subscribeWithSql(
|
|
489
|
+
* 'SELECT * FROM chat.messages WHERE conversation_id = 1',
|
|
490
|
+
* (event) => {
|
|
491
|
+
* if (event.type === 'change') {
|
|
492
|
+
* console.log('New message:', event.rows);
|
|
493
|
+
* }
|
|
494
|
+
* },
|
|
495
|
+
* { batch_size: 50, last_rows: 100 }
|
|
496
|
+
* );
|
|
497
|
+
*
|
|
498
|
+
* // Later: unsubscribe when done
|
|
499
|
+
* await unsubscribe();
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
async subscribeWithSql(sql, callback, options) {
|
|
503
|
+
await this.initialize();
|
|
504
|
+
if (!this.wasmClient) {
|
|
505
|
+
throw new Error('WASM client not initialized');
|
|
506
|
+
}
|
|
507
|
+
// Wrap callback to parse JSON and provide typed event
|
|
508
|
+
const wrappedCallback = (eventJson) => {
|
|
509
|
+
try {
|
|
510
|
+
console.log('[KalamClient SDK] Received event JSON:', eventJson.substring(0, 200));
|
|
511
|
+
const event = JSON.parse(eventJson);
|
|
512
|
+
console.log('[KalamClient SDK] Parsed event type:', event.type);
|
|
513
|
+
callback(event);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
console.error('Failed to parse subscription event:', error);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
// Convert options to JSON string if provided
|
|
520
|
+
const optionsJson = options ? JSON.stringify(options) : undefined;
|
|
521
|
+
const subscriptionId = await this.wasmClient.subscribeWithSql(sql, optionsJson, wrappedCallback);
|
|
522
|
+
// Track the subscription (use SQL as tableName for tracking)
|
|
523
|
+
this.subscriptions.set(subscriptionId, {
|
|
524
|
+
id: subscriptionId,
|
|
525
|
+
tableName: sql,
|
|
526
|
+
createdAt: new Date()
|
|
527
|
+
});
|
|
528
|
+
// Return unsubscribe function (Firebase/Supabase style)
|
|
529
|
+
return async () => {
|
|
530
|
+
await this.unsubscribe(subscriptionId);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Unsubscribe from table changes
|
|
535
|
+
*
|
|
536
|
+
* @param subscriptionId - ID returned from subscribe()
|
|
537
|
+
*
|
|
538
|
+
* @throws Error if unsubscribe fails or not connected
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```typescript
|
|
542
|
+
* // Using the returned unsubscribe function (preferred)
|
|
543
|
+
* const unsubscribe = await client.subscribe('messages', handleChange);
|
|
544
|
+
* await unsubscribe();
|
|
545
|
+
*
|
|
546
|
+
* // Or manually with subscription ID
|
|
547
|
+
* const unsubscribe = await client.subscribe('messages', handleChange);
|
|
548
|
+
* const subs = client.getSubscriptions();
|
|
549
|
+
* await client.unsubscribe(subs[0].id);
|
|
550
|
+
* ```
|
|
551
|
+
*/
|
|
552
|
+
async unsubscribe(subscriptionId) {
|
|
553
|
+
if (!this.wasmClient) {
|
|
554
|
+
throw new Error('WASM client not initialized');
|
|
555
|
+
}
|
|
556
|
+
await this.wasmClient.unsubscribe(subscriptionId);
|
|
557
|
+
// Remove from tracking
|
|
558
|
+
this.subscriptions.delete(subscriptionId);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Get the number of active subscriptions
|
|
562
|
+
*
|
|
563
|
+
* @returns Number of active subscriptions
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* ```typescript
|
|
567
|
+
* console.log(`Active subscriptions: ${client.getSubscriptionCount()}`);
|
|
568
|
+
*
|
|
569
|
+
* // Prevent too many subscriptions
|
|
570
|
+
* if (client.getSubscriptionCount() >= 10) {
|
|
571
|
+
* console.warn('Too many subscriptions!');
|
|
572
|
+
* }
|
|
573
|
+
* ```
|
|
574
|
+
*/
|
|
575
|
+
getSubscriptionCount() {
|
|
576
|
+
return this.subscriptions.size;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get information about all active subscriptions
|
|
580
|
+
*
|
|
581
|
+
* @returns Array of subscription info objects
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```typescript
|
|
585
|
+
* const subs = client.getSubscriptions();
|
|
586
|
+
* for (const sub of subs) {
|
|
587
|
+
* console.log(`Subscribed to ${sub.tableName} since ${sub.createdAt}`);
|
|
588
|
+
* }
|
|
589
|
+
* ```
|
|
590
|
+
*/
|
|
591
|
+
getSubscriptions() {
|
|
592
|
+
return Array.from(this.subscriptions.values());
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Check if subscribed to a specific table
|
|
596
|
+
*
|
|
597
|
+
* @param tableName - Name of the table to check
|
|
598
|
+
* @returns true if there's an active subscription to this table
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```typescript
|
|
602
|
+
* if (!client.isSubscribedTo('messages')) {
|
|
603
|
+
* await client.subscribe('messages', handleChange);
|
|
604
|
+
* }
|
|
605
|
+
* ```
|
|
606
|
+
*/
|
|
607
|
+
isSubscribedTo(tableName) {
|
|
608
|
+
for (const sub of this.subscriptions.values()) {
|
|
609
|
+
if (sub.tableName === tableName) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Unsubscribe from all active subscriptions
|
|
617
|
+
*
|
|
618
|
+
* Useful for cleanup before disconnecting or switching contexts.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```typescript
|
|
622
|
+
* // Cleanup all subscriptions
|
|
623
|
+
* await client.unsubscribeAll();
|
|
624
|
+
* console.log(`Subscriptions remaining: ${client.getSubscriptionCount()}`); // 0
|
|
625
|
+
* ```
|
|
626
|
+
*/
|
|
627
|
+
async unsubscribeAll() {
|
|
628
|
+
const subscriptionIds = Array.from(this.subscriptions.keys());
|
|
629
|
+
for (const id of subscriptionIds) {
|
|
630
|
+
await this.unsubscribe(id);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Create a KalamDB client with the given configuration
|
|
636
|
+
*
|
|
637
|
+
* Factory function that supports both the new type-safe auth API and legacy API.
|
|
638
|
+
*
|
|
639
|
+
* @param options - Client configuration with URL and authentication
|
|
640
|
+
* @returns Configured KalamDB client
|
|
641
|
+
*
|
|
642
|
+
* @example
|
|
643
|
+
* ```typescript
|
|
644
|
+
* import { createClient, Auth } from 'kalam-link';
|
|
645
|
+
*
|
|
646
|
+
* // New type-safe API (recommended)
|
|
647
|
+
* const client = createClient({
|
|
648
|
+
* url: 'http://localhost:8080',
|
|
649
|
+
* auth: Auth.basic('admin', 'admin')
|
|
650
|
+
* });
|
|
651
|
+
*
|
|
652
|
+
* // JWT authentication
|
|
653
|
+
* const jwtClient = createClient({
|
|
654
|
+
* url: 'http://localhost:8080',
|
|
655
|
+
* auth: Auth.jwt('eyJhbGciOiJIUzI1NiIs...')
|
|
656
|
+
* });
|
|
657
|
+
*
|
|
658
|
+
* // Anonymous (no authentication)
|
|
659
|
+
* const anonClient = createClient({
|
|
660
|
+
* url: 'http://localhost:8080',
|
|
661
|
+
* auth: Auth.none()
|
|
662
|
+
* });
|
|
663
|
+
*
|
|
664
|
+
* // Legacy API (deprecated but still works)
|
|
665
|
+
* const legacyClient = createClient({
|
|
666
|
+
* url: 'http://localhost:8080',
|
|
667
|
+
* username: 'admin',
|
|
668
|
+
* password: 'admin'
|
|
669
|
+
* });
|
|
670
|
+
* ```
|
|
671
|
+
*/
|
|
672
|
+
export function createClient(options) {
|
|
673
|
+
// Handle both new and legacy API
|
|
674
|
+
if (isAuthOptions(options)) {
|
|
675
|
+
return new KalamDBClient(options);
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
// Legacy API: convert to new format
|
|
679
|
+
return new KalamDBClient(options.url, options.username, options.password);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// Default export
|
|
683
|
+
export default KalamDBClient;
|