@rivium/sync-node 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,845 @@
1
+ // src/index.ts
2
+ import mqtt from "mqtt";
3
+ var RiviumSyncErrorCode = /* @__PURE__ */ ((RiviumSyncErrorCode2) => {
4
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["CONNECTION_FAILED"] = 1e3] = "CONNECTION_FAILED";
5
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["CONNECTION_TIMEOUT"] = 1001] = "CONNECTION_TIMEOUT";
6
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["CONNECTION_LOST"] = 1002] = "CONNECTION_LOST";
7
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["AUTHENTICATION_FAILED"] = 1004] = "AUTHENTICATION_FAILED";
8
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["SUBSCRIPTION_FAILED"] = 1100] = "SUBSCRIPTION_FAILED";
9
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["DATA_FETCH_FAILED"] = 1200] = "DATA_FETCH_FAILED";
10
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["DATA_PARSE_ERROR"] = 1201] = "DATA_PARSE_ERROR";
11
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["DATA_WRITE_FAILED"] = 1202] = "DATA_WRITE_FAILED";
12
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["DATA_DELETE_FAILED"] = 1203] = "DATA_DELETE_FAILED";
13
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["DOCUMENT_NOT_FOUND"] = 1204] = "DOCUMENT_NOT_FOUND";
14
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["INVALID_CONFIG"] = 1300] = "INVALID_CONFIG";
15
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["MISSING_API_KEY"] = 1301] = "MISSING_API_KEY";
16
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["MISSING_SERVER_URL"] = 1302] = "MISSING_SERVER_URL";
17
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["MISSING_SERVER_SECRET"] = 1303] = "MISSING_SERVER_SECRET";
18
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["NOT_INITIALIZED"] = 1500] = "NOT_INITIALIZED";
19
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["NOT_CONNECTED"] = 1501] = "NOT_CONNECTED";
20
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["INVALID_QUERY"] = 1700] = "INVALID_QUERY";
21
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["QUERY_EXECUTION_FAILED"] = 1701] = "QUERY_EXECUTION_FAILED";
22
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["BATCH_WRITE_FAILED"] = 1800] = "BATCH_WRITE_FAILED";
23
+ RiviumSyncErrorCode2[RiviumSyncErrorCode2["UNKNOWN_ERROR"] = 9999] = "UNKNOWN_ERROR";
24
+ return RiviumSyncErrorCode2;
25
+ })(RiviumSyncErrorCode || {});
26
+ var ERROR_MESSAGES = {
27
+ [1e3 /* CONNECTION_FAILED */]: "Failed to connect to server",
28
+ [1001 /* CONNECTION_TIMEOUT */]: "Connection timed out",
29
+ [1002 /* CONNECTION_LOST */]: "Connection to server was lost",
30
+ [1004 /* AUTHENTICATION_FAILED */]: "Authentication failed - invalid API key",
31
+ [1100 /* SUBSCRIPTION_FAILED */]: "Failed to subscribe to path",
32
+ [1200 /* DATA_FETCH_FAILED */]: "Failed to fetch data",
33
+ [1201 /* DATA_PARSE_ERROR */]: "Failed to parse data",
34
+ [1202 /* DATA_WRITE_FAILED */]: "Failed to write data",
35
+ [1203 /* DATA_DELETE_FAILED */]: "Failed to delete data",
36
+ [1204 /* DOCUMENT_NOT_FOUND */]: "Document not found",
37
+ [1300 /* INVALID_CONFIG */]: "Invalid configuration",
38
+ [1301 /* MISSING_API_KEY */]: "API key is missing",
39
+ [1302 /* MISSING_SERVER_URL */]: "Server URL is missing",
40
+ [1303 /* MISSING_SERVER_SECRET */]: "Server secret is missing",
41
+ [1500 /* NOT_INITIALIZED */]: "SDK is not initialized",
42
+ [1501 /* NOT_CONNECTED */]: "Not connected to server",
43
+ [1700 /* INVALID_QUERY */]: "Invalid query parameters",
44
+ [1701 /* QUERY_EXECUTION_FAILED */]: "Query execution failed",
45
+ [1800 /* BATCH_WRITE_FAILED */]: "Batch write operation failed",
46
+ [9999 /* UNKNOWN_ERROR */]: "An unknown error occurred"
47
+ };
48
+ var RiviumSyncError = class extends Error {
49
+ constructor(code, details) {
50
+ super(ERROR_MESSAGES[code] || "Unknown error");
51
+ this.name = "RiviumSyncError";
52
+ this.code = code;
53
+ this.details = details;
54
+ }
55
+ toJSON() {
56
+ return {
57
+ code: this.code,
58
+ message: this.message,
59
+ details: this.details
60
+ };
61
+ }
62
+ };
63
+ var RiviumSyncLogLevel = /* @__PURE__ */ ((RiviumSyncLogLevel2) => {
64
+ RiviumSyncLogLevel2[RiviumSyncLogLevel2["NONE"] = 0] = "NONE";
65
+ RiviumSyncLogLevel2[RiviumSyncLogLevel2["ERROR"] = 1] = "ERROR";
66
+ RiviumSyncLogLevel2[RiviumSyncLogLevel2["WARNING"] = 2] = "WARNING";
67
+ RiviumSyncLogLevel2[RiviumSyncLogLevel2["INFO"] = 3] = "INFO";
68
+ RiviumSyncLogLevel2[RiviumSyncLogLevel2["DEBUG"] = 4] = "DEBUG";
69
+ RiviumSyncLogLevel2[RiviumSyncLogLevel2["VERBOSE"] = 5] = "VERBOSE";
70
+ return RiviumSyncLogLevel2;
71
+ })(RiviumSyncLogLevel || {});
72
+ var SyncCollection = class {
73
+ constructor(admin, databaseId, collectionId) {
74
+ this.admin = admin;
75
+ this.databaseId = databaseId;
76
+ this.collectionId = collectionId;
77
+ }
78
+ /**
79
+ * Get a document reference
80
+ */
81
+ doc(documentId) {
82
+ return new SyncDocumentRef(this.admin, this.databaseId, this.collectionId, documentId);
83
+ }
84
+ /**
85
+ * Create a new document with auto-generated ID
86
+ */
87
+ async add(data) {
88
+ return this.admin.addDocument(this.databaseId, this.collectionId, data);
89
+ }
90
+ /**
91
+ * Get a single document by ID
92
+ */
93
+ async get(documentId) {
94
+ return this.admin.getDocument(this.databaseId, this.collectionId, documentId);
95
+ }
96
+ /**
97
+ * Get all documents in collection
98
+ */
99
+ async getAll(options) {
100
+ return this.admin.getDocuments(this.databaseId, this.collectionId, options);
101
+ }
102
+ /**
103
+ * Listen to collection changes (requires enableRealtime: true)
104
+ */
105
+ onSnapshot(callback, options) {
106
+ return this.admin.listenCollection(this.databaseId, this.collectionId, callback, options);
107
+ }
108
+ /**
109
+ * Start a query builder
110
+ */
111
+ query() {
112
+ return new SyncQuery(this.admin, this.databaseId, this.collectionId);
113
+ }
114
+ /**
115
+ * Add a filter condition
116
+ */
117
+ where(field, operator, value) {
118
+ return new SyncQuery(this.admin, this.databaseId, this.collectionId).where(field, operator, value);
119
+ }
120
+ /**
121
+ * Order results
122
+ */
123
+ orderBy(field, direction = "asc") {
124
+ return new SyncQuery(this.admin, this.databaseId, this.collectionId).orderBy(field, direction);
125
+ }
126
+ /**
127
+ * Limit results
128
+ */
129
+ limit(count) {
130
+ return new SyncQuery(this.admin, this.databaseId, this.collectionId).limit(count);
131
+ }
132
+ };
133
+ var SyncDocumentRef = class {
134
+ constructor(admin, databaseId, collectionId, documentId) {
135
+ this.admin = admin;
136
+ this.databaseId = databaseId;
137
+ this.collectionId = collectionId;
138
+ this.documentId = documentId;
139
+ }
140
+ get id() {
141
+ return this.documentId;
142
+ }
143
+ get path() {
144
+ return `/${this.databaseId}/${this.collectionId}/${this.documentId}`;
145
+ }
146
+ /**
147
+ * Get document data
148
+ */
149
+ async get() {
150
+ return this.admin.getDocument(this.databaseId, this.collectionId, this.documentId);
151
+ }
152
+ /**
153
+ * Check if document exists
154
+ */
155
+ async exists() {
156
+ const doc = await this.get();
157
+ return doc !== null;
158
+ }
159
+ /**
160
+ * Set document data (overwrite)
161
+ */
162
+ async set(data) {
163
+ await this.admin.setDocument(this.databaseId, this.collectionId, this.documentId, data);
164
+ }
165
+ /**
166
+ * Update document data (merge)
167
+ */
168
+ async update(data) {
169
+ await this.admin.updateDocument(this.databaseId, this.collectionId, this.documentId, data);
170
+ }
171
+ /**
172
+ * Delete document
173
+ */
174
+ async delete() {
175
+ await this.admin.deleteDocument(this.databaseId, this.collectionId, this.documentId);
176
+ }
177
+ /**
178
+ * Listen to document changes (requires enableRealtime: true)
179
+ */
180
+ onSnapshot(callback) {
181
+ return this.admin.listenDocument(this.databaseId, this.collectionId, this.documentId, callback);
182
+ }
183
+ };
184
+ var SyncQuery = class {
185
+ constructor(admin, databaseId, collectionId) {
186
+ this.options = {};
187
+ this.admin = admin;
188
+ this.databaseId = databaseId;
189
+ this.collectionId = collectionId;
190
+ }
191
+ /**
192
+ * Add a filter condition
193
+ */
194
+ where(field, operator, value) {
195
+ if (!this.options.filters) {
196
+ this.options.filters = [];
197
+ }
198
+ this.options.filters.push({ field, operator, value });
199
+ return this;
200
+ }
201
+ /**
202
+ * Order results
203
+ */
204
+ orderBy(field, direction = "asc") {
205
+ this.options.orderBy = field;
206
+ this.options.orderDirection = direction;
207
+ return this;
208
+ }
209
+ /**
210
+ * Limit results
211
+ */
212
+ limit(count) {
213
+ this.options.limit = count;
214
+ return this;
215
+ }
216
+ /**
217
+ * Skip results (for pagination)
218
+ */
219
+ offset(count) {
220
+ this.options.offset = count;
221
+ return this;
222
+ }
223
+ /**
224
+ * Alias for offset - skip first N results
225
+ */
226
+ startAfter(count) {
227
+ return this.offset(count);
228
+ }
229
+ /**
230
+ * Execute query and get results
231
+ */
232
+ async get() {
233
+ return this.admin.getDocuments(this.databaseId, this.collectionId, this.options);
234
+ }
235
+ /**
236
+ * Get first result only
237
+ */
238
+ async getFirst() {
239
+ const originalLimit = this.options.limit;
240
+ this.options.limit = 1;
241
+ const results = await this.get();
242
+ this.options.limit = originalLimit;
243
+ return results.length > 0 ? results[0] : null;
244
+ }
245
+ /**
246
+ * Count matching documents
247
+ */
248
+ async count() {
249
+ const results = await this.get();
250
+ return results.length;
251
+ }
252
+ /**
253
+ * Listen to query results (requires enableRealtime: true)
254
+ */
255
+ onSnapshot(callback) {
256
+ return this.admin.listenCollection(this.databaseId, this.collectionId, callback, this.options);
257
+ }
258
+ };
259
+ var SyncDatabase = class {
260
+ constructor(admin, databaseId) {
261
+ this.admin = admin;
262
+ this.databaseId = databaseId;
263
+ }
264
+ get id() {
265
+ return this.databaseId;
266
+ }
267
+ /**
268
+ * Get a collection reference
269
+ */
270
+ collection(collectionId) {
271
+ return new SyncCollection(this.admin, this.databaseId, collectionId);
272
+ }
273
+ };
274
+ var WriteBatch = class {
275
+ constructor(admin) {
276
+ this.operations = [];
277
+ this.admin = admin;
278
+ }
279
+ /**
280
+ * Add a set operation to the batch
281
+ */
282
+ set(docRef, data) {
283
+ this.operations.push({
284
+ type: "set",
285
+ databaseId: docRef.databaseId,
286
+ collectionId: docRef.collectionId,
287
+ documentId: docRef.id,
288
+ data
289
+ });
290
+ return this;
291
+ }
292
+ /**
293
+ * Add an update operation to the batch
294
+ */
295
+ update(docRef, data) {
296
+ this.operations.push({
297
+ type: "update",
298
+ databaseId: docRef.databaseId,
299
+ collectionId: docRef.collectionId,
300
+ documentId: docRef.id,
301
+ data
302
+ });
303
+ return this;
304
+ }
305
+ /**
306
+ * Add a delete operation to the batch
307
+ */
308
+ delete(docRef) {
309
+ this.operations.push({
310
+ type: "delete",
311
+ databaseId: docRef.databaseId,
312
+ collectionId: docRef.collectionId,
313
+ documentId: docRef.id
314
+ });
315
+ return this;
316
+ }
317
+ /**
318
+ * Commit all operations in the batch
319
+ */
320
+ async commit() {
321
+ await this.admin.executeBatch(this.operations);
322
+ this.operations = [];
323
+ }
324
+ /**
325
+ * Get the number of pending operations
326
+ */
327
+ get size() {
328
+ return this.operations.length;
329
+ }
330
+ };
331
+ var _RiviumSyncAdmin = class _RiviumSyncAdmin {
332
+ constructor(config) {
333
+ this.mqttClient = null;
334
+ this.mqttConfig = null;
335
+ // Realtime listeners
336
+ this.documentListeners = /* @__PURE__ */ new Map();
337
+ this.collectionListeners = /* @__PURE__ */ new Map();
338
+ this.cachedCollections = /* @__PURE__ */ new Map();
339
+ if (!config.apiKey) {
340
+ throw new RiviumSyncError(1301 /* MISSING_API_KEY */);
341
+ }
342
+ if (!config.serverSecret) {
343
+ throw new RiviumSyncError(1303 /* MISSING_SERVER_SECRET */);
344
+ }
345
+ this.config = {
346
+ ...config,
347
+ baseUrl: _RiviumSyncAdmin.DEFAULT_BASE_URL,
348
+ enableRealtime: config.enableRealtime ?? false,
349
+ logLevel: config.logLevel ?? 1 /* ERROR */,
350
+ timeout: config.timeout ?? 3e4
351
+ };
352
+ this.logLevel = this.config.logLevel;
353
+ this.timeout = this.config.timeout;
354
+ this.log(3 /* INFO */, "RiviumSyncAdmin SDK initialized");
355
+ if (this.config.enableRealtime) {
356
+ this.initRealtime();
357
+ }
358
+ }
359
+ // ==========================================================================
360
+ // Logging
361
+ // ==========================================================================
362
+ log(level, message, ...args) {
363
+ if (level > this.logLevel) return;
364
+ const prefix = "[RiviumSync]";
365
+ switch (level) {
366
+ case 1 /* ERROR */:
367
+ console.error(prefix, message, ...args);
368
+ break;
369
+ case 2 /* WARNING */:
370
+ console.warn(prefix, message, ...args);
371
+ break;
372
+ case 3 /* INFO */:
373
+ console.info(prefix, message, ...args);
374
+ break;
375
+ default:
376
+ console.log(prefix, message, ...args);
377
+ }
378
+ }
379
+ setLogLevel(level) {
380
+ this.logLevel = level;
381
+ }
382
+ // ==========================================================================
383
+ // HTTP Client
384
+ // ==========================================================================
385
+ async request(method, path, body) {
386
+ const url = `${this.config.baseUrl}${path}`;
387
+ const headers = {
388
+ "Content-Type": "application/json",
389
+ "x-api-key": this.config.apiKey,
390
+ "x-server-secret": this.config.serverSecret
391
+ };
392
+ if (this.config.userId) {
393
+ headers["X-User-Id"] = this.config.userId;
394
+ }
395
+ const controller = new AbortController();
396
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
397
+ try {
398
+ const response = await fetch(url, {
399
+ method,
400
+ headers,
401
+ body: body ? JSON.stringify(body) : void 0,
402
+ signal: controller.signal
403
+ });
404
+ clearTimeout(timeoutId);
405
+ if (!response.ok) {
406
+ const errorText = await response.text();
407
+ throw new RiviumSyncError(
408
+ 1200 /* DATA_FETCH_FAILED */,
409
+ `HTTP ${response.status}: ${errorText}`
410
+ );
411
+ }
412
+ const text = await response.text();
413
+ if (!text) {
414
+ return {};
415
+ }
416
+ return JSON.parse(text);
417
+ } catch (error) {
418
+ clearTimeout(timeoutId);
419
+ if (error instanceof RiviumSyncError) {
420
+ throw error;
421
+ }
422
+ if (error.name === "AbortError") {
423
+ throw new RiviumSyncError(1001 /* CONNECTION_TIMEOUT */);
424
+ }
425
+ throw new RiviumSyncError(
426
+ 1200 /* DATA_FETCH_FAILED */,
427
+ error.message
428
+ );
429
+ }
430
+ }
431
+ // ==========================================================================
432
+ // Public API - Database Access
433
+ // ==========================================================================
434
+ /**
435
+ * Get a database reference
436
+ */
437
+ database(databaseId) {
438
+ return new SyncDatabase(this, databaseId);
439
+ }
440
+ /**
441
+ * Create a new write batch
442
+ */
443
+ batch() {
444
+ return new WriteBatch(this);
445
+ }
446
+ // ==========================================================================
447
+ // Document Operations (Internal)
448
+ // ==========================================================================
449
+ async getDocument(databaseId, collectionId, documentId) {
450
+ try {
451
+ const response = await this.request(
452
+ "GET",
453
+ `/databases/${databaseId}/collections/${collectionId}/documents/sdk/${documentId}`
454
+ );
455
+ return response;
456
+ } catch (error) {
457
+ if (error.details?.includes("404")) {
458
+ return null;
459
+ }
460
+ this.log(1 /* ERROR */, "Failed to get document:", error);
461
+ throw error;
462
+ }
463
+ }
464
+ async getDocuments(databaseId, collectionId, options) {
465
+ try {
466
+ const queryParams = new URLSearchParams();
467
+ if (options?.filters) {
468
+ queryParams.set("filters", JSON.stringify(options.filters));
469
+ }
470
+ if (options?.orderBy) {
471
+ queryParams.set("orderBy", options.orderBy);
472
+ queryParams.set("orderDirection", options.orderDirection || "asc");
473
+ }
474
+ if (options?.limit) {
475
+ queryParams.set("limit", options.limit.toString());
476
+ }
477
+ if (options?.offset) {
478
+ queryParams.set("offset", options.offset.toString());
479
+ }
480
+ const queryString = queryParams.toString();
481
+ const path = `/databases/${databaseId}/collections/${collectionId}/documents/sdk${queryString ? `?${queryString}` : ""}`;
482
+ const response = await this.request(
483
+ "GET",
484
+ path
485
+ );
486
+ return Array.isArray(response) ? response : response.documents || [];
487
+ } catch (error) {
488
+ this.log(1 /* ERROR */, "Failed to get documents:", error);
489
+ throw error;
490
+ }
491
+ }
492
+ async addDocument(databaseId, collectionId, data) {
493
+ try {
494
+ const response = await this.request(
495
+ "POST",
496
+ `/databases/${databaseId}/collections/${collectionId}/documents/sdk`,
497
+ { data }
498
+ );
499
+ return response;
500
+ } catch (error) {
501
+ this.log(1 /* ERROR */, "Failed to add document:", error);
502
+ throw error;
503
+ }
504
+ }
505
+ async setDocument(databaseId, collectionId, documentId, data) {
506
+ try {
507
+ await this.request(
508
+ "PUT",
509
+ `/databases/${databaseId}/collections/${collectionId}/documents/sdk/${documentId}`,
510
+ { data }
511
+ );
512
+ } catch (error) {
513
+ this.log(1 /* ERROR */, "Failed to set document:", error);
514
+ throw error;
515
+ }
516
+ }
517
+ async updateDocument(databaseId, collectionId, documentId, data) {
518
+ try {
519
+ await this.request(
520
+ "PATCH",
521
+ `/databases/${databaseId}/collections/${collectionId}/documents/sdk/${documentId}`,
522
+ { data }
523
+ );
524
+ } catch (error) {
525
+ this.log(1 /* ERROR */, "Failed to update document:", error);
526
+ throw error;
527
+ }
528
+ }
529
+ async deleteDocument(databaseId, collectionId, documentId) {
530
+ try {
531
+ await this.request(
532
+ "DELETE",
533
+ `/databases/${databaseId}/collections/${collectionId}/documents/sdk/${documentId}`
534
+ );
535
+ } catch (error) {
536
+ this.log(1 /* ERROR */, "Failed to delete document:", error);
537
+ throw error;
538
+ }
539
+ }
540
+ // ==========================================================================
541
+ // Batch Operations
542
+ // ==========================================================================
543
+ async executeBatch(operations) {
544
+ for (const op of operations) {
545
+ switch (op.type) {
546
+ case "set":
547
+ await this.setDocument(op.databaseId, op.collectionId, op.documentId, op.data);
548
+ break;
549
+ case "update":
550
+ await this.updateDocument(op.databaseId, op.collectionId, op.documentId, op.data);
551
+ break;
552
+ case "delete":
553
+ await this.deleteDocument(op.databaseId, op.collectionId, op.documentId);
554
+ break;
555
+ }
556
+ }
557
+ }
558
+ // ==========================================================================
559
+ // Realtime (Optional)
560
+ // ==========================================================================
561
+ async initRealtime() {
562
+ try {
563
+ this.log(4 /* DEBUG */, "Fetching MQTT token...");
564
+ const tokenData = await this.request("POST", "/connections/token");
565
+ this.mqttConfig = {
566
+ host: tokenData.mqtt.host,
567
+ port: tokenData.mqtt.port,
568
+ wsHost: tokenData.mqtt.host,
569
+ wsPort: tokenData.mqtt.port,
570
+ password: tokenData.token
571
+ };
572
+ this.connectMqtt();
573
+ } catch (error) {
574
+ this.log(1 /* ERROR */, "Failed to fetch MQTT token:", error);
575
+ }
576
+ }
577
+ connectMqtt() {
578
+ if (!this.mqttConfig) {
579
+ return;
580
+ }
581
+ const url = `mqtt://${this.mqttConfig.host}:${this.mqttConfig.port}`;
582
+ const clientId = `rivium_sync_node_${this.generateUUID()}`;
583
+ const options = {
584
+ clientId,
585
+ clean: false,
586
+ connectTimeout: 1e4,
587
+ username: "jwt",
588
+ password: this.mqttConfig.password
589
+ };
590
+ this.log(4 /* DEBUG */, "Connecting to MQTT:", url);
591
+ this.mqttClient = mqtt.connect(url, options);
592
+ this.mqttClient.on("connect", () => {
593
+ this.log(3 /* INFO */, "MQTT connected");
594
+ this.resubscribeAll();
595
+ });
596
+ this.mqttClient.on("message", (topic, payload) => {
597
+ try {
598
+ const data = JSON.parse(payload.toString());
599
+ this.handleMqttMessage(topic, data);
600
+ } catch (error) {
601
+ this.log(1 /* ERROR */, "MQTT message parse error:", error);
602
+ }
603
+ });
604
+ this.mqttClient.on("error", (error) => {
605
+ this.log(1 /* ERROR */, "MQTT error:", error);
606
+ });
607
+ this.mqttClient.on("close", () => {
608
+ this.log(3 /* INFO */, "MQTT disconnected");
609
+ });
610
+ }
611
+ listenDocument(databaseId, collectionId, documentId, callback) {
612
+ if (!this.config.enableRealtime) {
613
+ this.log(2 /* WARNING */, "Realtime not enabled. Set enableRealtime: true in config.");
614
+ this.getDocument(databaseId, collectionId, documentId).then((doc) => {
615
+ callback(doc);
616
+ });
617
+ return () => {
618
+ };
619
+ }
620
+ const path = `/${databaseId}/${collectionId}/${documentId}`;
621
+ const mqttTopic = `rivium_sync/${this.config.apiKey.substring(0, 16)}/db/${databaseId}/${collectionId}/${documentId}`;
622
+ if (!this.documentListeners.has(path)) {
623
+ this.documentListeners.set(path, /* @__PURE__ */ new Set());
624
+ }
625
+ this.documentListeners.get(path).add(callback);
626
+ if (this.mqttClient?.connected) {
627
+ this.mqttClient.subscribe(mqttTopic, { qos: 1 });
628
+ }
629
+ this.getDocument(databaseId, collectionId, documentId).then((doc) => {
630
+ callback(doc);
631
+ });
632
+ return () => {
633
+ const listeners = this.documentListeners.get(path);
634
+ if (listeners) {
635
+ listeners.delete(callback);
636
+ if (listeners.size === 0) {
637
+ this.documentListeners.delete(path);
638
+ this.mqttClient?.unsubscribe(mqttTopic);
639
+ }
640
+ }
641
+ };
642
+ }
643
+ listenCollection(databaseId, collectionId, callback, options) {
644
+ if (!this.config.enableRealtime) {
645
+ this.log(2 /* WARNING */, "Realtime not enabled. Set enableRealtime: true in config.");
646
+ this.getDocuments(databaseId, collectionId, options).then((docs) => {
647
+ callback(docs);
648
+ });
649
+ return () => {
650
+ };
651
+ }
652
+ const path = `/${databaseId}/${collectionId}`;
653
+ const mqttTopic = `rivium_sync/${this.config.apiKey.substring(0, 16)}/db/${databaseId}/${collectionId}/+`;
654
+ if (!this.collectionListeners.has(path)) {
655
+ this.collectionListeners.set(path, /* @__PURE__ */ new Set());
656
+ }
657
+ this.collectionListeners.get(path).add({
658
+ callback,
659
+ options
660
+ });
661
+ if (this.mqttClient?.connected) {
662
+ this.mqttClient.subscribe(mqttTopic, { qos: 1 });
663
+ }
664
+ this.getDocuments(databaseId, collectionId, options).then((docs) => {
665
+ this.cachedCollections.set(path, docs);
666
+ callback(docs);
667
+ });
668
+ return () => {
669
+ const listeners = this.collectionListeners.get(path);
670
+ if (listeners) {
671
+ const listenerObj = Array.from(listeners).find((l) => l.callback === callback);
672
+ if (listenerObj) {
673
+ listeners.delete(listenerObj);
674
+ }
675
+ if (listeners.size === 0) {
676
+ this.collectionListeners.delete(path);
677
+ this.cachedCollections.delete(path);
678
+ this.mqttClient?.unsubscribe(mqttTopic);
679
+ }
680
+ }
681
+ };
682
+ }
683
+ resubscribeAll() {
684
+ if (!this.mqttClient?.connected) return;
685
+ const appId = this.config.apiKey.substring(0, 16);
686
+ this.documentListeners.forEach((_, path) => {
687
+ const parts = path.split("/").filter((p) => p);
688
+ if (parts.length === 3) {
689
+ const [databaseId, collectionId, documentId] = parts;
690
+ const topic = `rivium_sync/${appId}/db/${databaseId}/${collectionId}/${documentId}`;
691
+ this.mqttClient.subscribe(topic, { qos: 1 });
692
+ }
693
+ });
694
+ this.collectionListeners.forEach((_, path) => {
695
+ const parts = path.split("/").filter((p) => p);
696
+ if (parts.length === 2) {
697
+ const [databaseId, collectionId] = parts;
698
+ const topic = `rivium_sync/${appId}/db/${databaseId}/${collectionId}/+`;
699
+ this.mqttClient.subscribe(topic, { qos: 1 });
700
+ }
701
+ });
702
+ }
703
+ handleMqttMessage(topic, data) {
704
+ const parts = topic.split("/");
705
+ if (parts.length < 6) return;
706
+ const databaseId = parts[3];
707
+ const collectionId = parts[4];
708
+ const documentId = parts[5];
709
+ const documentPath = `/${databaseId}/${collectionId}/${documentId}`;
710
+ const collectionPath = `/${databaseId}/${collectionId}`;
711
+ const typedData = data;
712
+ const docListeners = this.documentListeners.get(documentPath);
713
+ if (docListeners) {
714
+ const document = {
715
+ id: documentId,
716
+ data: typedData.data || typedData,
717
+ createdAt: typedData.createdAt,
718
+ updatedAt: typedData.updatedAt,
719
+ version: typedData.version
720
+ };
721
+ docListeners.forEach((callback) => {
722
+ if (typedData.deleted) {
723
+ callback(null);
724
+ } else {
725
+ callback(document);
726
+ }
727
+ });
728
+ }
729
+ const colListeners = this.collectionListeners.get(collectionPath);
730
+ if (colListeners) {
731
+ let docs = this.cachedCollections.get(collectionPath) || [];
732
+ if (typedData.deleted) {
733
+ docs = docs.filter((d) => d.id !== documentId);
734
+ } else {
735
+ const existingIndex = docs.findIndex((d) => d.id === documentId);
736
+ const newDoc = {
737
+ id: documentId,
738
+ data: typedData.data || typedData,
739
+ createdAt: typedData.createdAt,
740
+ updatedAt: typedData.updatedAt,
741
+ version: typedData.version
742
+ };
743
+ if (existingIndex >= 0) {
744
+ docs[existingIndex] = newDoc;
745
+ } else {
746
+ docs.push(newDoc);
747
+ }
748
+ }
749
+ this.cachedCollections.set(collectionPath, docs);
750
+ colListeners.forEach(({ callback, options }) => {
751
+ let filteredDocs = [...docs];
752
+ if (options?.filters) {
753
+ filteredDocs = this.applyFilters(filteredDocs, options.filters);
754
+ }
755
+ if (options?.orderBy) {
756
+ filteredDocs = this.applyOrdering(filteredDocs, options.orderBy, options.orderDirection);
757
+ }
758
+ if (options?.limit) {
759
+ filteredDocs = filteredDocs.slice(options.offset || 0, (options.offset || 0) + options.limit);
760
+ }
761
+ callback(filteredDocs);
762
+ });
763
+ }
764
+ }
765
+ applyFilters(docs, filters) {
766
+ return docs.filter((doc) => {
767
+ const data = doc.data;
768
+ return filters.every((filter) => {
769
+ const value = data[filter.field];
770
+ switch (filter.operator) {
771
+ case "==":
772
+ return value === filter.value;
773
+ case "!=":
774
+ return value !== filter.value;
775
+ case "<":
776
+ return value < filter.value;
777
+ case "<=":
778
+ return value <= filter.value;
779
+ case ">":
780
+ return value > filter.value;
781
+ case ">=":
782
+ return value >= filter.value;
783
+ case "in":
784
+ return Array.isArray(filter.value) && filter.value.includes(value);
785
+ case "not-in":
786
+ return Array.isArray(filter.value) && !filter.value.includes(value);
787
+ case "array-contains":
788
+ return Array.isArray(value) && value.includes(filter.value);
789
+ default:
790
+ return true;
791
+ }
792
+ });
793
+ });
794
+ }
795
+ applyOrdering(docs, orderBy, direction) {
796
+ return [...docs].sort((a, b) => {
797
+ const aData = a.data;
798
+ const bData = b.data;
799
+ const aVal = aData[orderBy];
800
+ const bVal = bData[orderBy];
801
+ let comparison = 0;
802
+ if (aVal == null && bVal != null) comparison = -1;
803
+ else if (aVal != null && bVal == null) comparison = 1;
804
+ else if (aVal != null && bVal != null) {
805
+ if (aVal < bVal) comparison = -1;
806
+ else if (aVal > bVal) comparison = 1;
807
+ }
808
+ return direction === "desc" ? -comparison : comparison;
809
+ });
810
+ }
811
+ // ==========================================================================
812
+ // Utilities
813
+ // ==========================================================================
814
+ generateUUID() {
815
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
816
+ const r = Math.random() * 16 | 0;
817
+ const v = c === "x" ? r : r & 3 | 8;
818
+ return v.toString(16);
819
+ });
820
+ }
821
+ /**
822
+ * Disconnect from realtime updates
823
+ */
824
+ disconnect() {
825
+ if (this.mqttClient) {
826
+ this.mqttClient.end(true);
827
+ this.mqttClient = null;
828
+ }
829
+ }
830
+ };
831
+ _RiviumSyncAdmin.DEFAULT_BASE_URL = "https://sync.rivium.co";
832
+ var RiviumSyncAdmin = _RiviumSyncAdmin;
833
+ var index_default = RiviumSyncAdmin;
834
+ export {
835
+ RiviumSyncAdmin,
836
+ RiviumSyncError,
837
+ RiviumSyncErrorCode,
838
+ RiviumSyncLogLevel,
839
+ SyncCollection,
840
+ SyncDatabase,
841
+ SyncDocumentRef,
842
+ SyncQuery,
843
+ WriteBatch,
844
+ index_default as default
845
+ };