@lark-sh/client 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.js ADDED
@@ -0,0 +1,1391 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DataSnapshot: () => DataSnapshot,
34
+ DatabaseReference: () => DatabaseReference,
35
+ LarkDatabase: () => LarkDatabase,
36
+ LarkError: () => LarkError,
37
+ OnDisconnect: () => OnDisconnect,
38
+ generatePushId: () => generatePushId
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/protocol/constants.ts
43
+ var OnDisconnectAction = {
44
+ SET: "s",
45
+ UPDATE: "u",
46
+ DELETE: "d",
47
+ CANCEL: "c"
48
+ };
49
+ var eventTypeFromShort = {
50
+ v: "value",
51
+ ca: "child_added",
52
+ cc: "child_changed",
53
+ cr: "child_removed"
54
+ };
55
+ var eventTypeToShort = {
56
+ value: "v",
57
+ child_added: "ca",
58
+ child_changed: "cc",
59
+ child_removed: "cr"
60
+ };
61
+ var DEFAULT_COORDINATOR_URL = "https://db.lark.dev";
62
+
63
+ // src/connection/Coordinator.ts
64
+ var Coordinator = class {
65
+ constructor(baseUrl = DEFAULT_COORDINATOR_URL) {
66
+ this.baseUrl = baseUrl.replace(/\/$/, "");
67
+ }
68
+ /**
69
+ * Request connection details from the coordinator.
70
+ *
71
+ * @param database - Database ID in format "project/database"
72
+ * @param options - Either a user token or anonymous flag
73
+ * @returns Connection details including host, port, and connection token
74
+ */
75
+ async connect(database, options = {}) {
76
+ const body = { database };
77
+ if (options.token) {
78
+ body.token = options.token;
79
+ } else if (options.anonymous) {
80
+ body.anonymous = true;
81
+ } else {
82
+ body.anonymous = true;
83
+ }
84
+ const response = await fetch(`${this.baseUrl}/connect`, {
85
+ method: "POST",
86
+ headers: {
87
+ "Content-Type": "application/json"
88
+ },
89
+ body: JSON.stringify(body)
90
+ });
91
+ if (!response.ok) {
92
+ let errorMessage = `Coordinator request failed: ${response.status}`;
93
+ try {
94
+ const errorBody = await response.json();
95
+ if (errorBody.message) {
96
+ errorMessage = errorBody.message;
97
+ }
98
+ } catch {
99
+ }
100
+ throw new Error(errorMessage);
101
+ }
102
+ const data = await response.json();
103
+ return data;
104
+ }
105
+ };
106
+
107
+ // src/LarkError.ts
108
+ var LarkError = class _LarkError extends Error {
109
+ constructor(code, message) {
110
+ super(message || code);
111
+ this.code = code;
112
+ this.name = "LarkError";
113
+ const ErrorWithCapture = Error;
114
+ if (ErrorWithCapture.captureStackTrace) {
115
+ ErrorWithCapture.captureStackTrace(this, _LarkError);
116
+ }
117
+ }
118
+ };
119
+
120
+ // src/protocol/messages.ts
121
+ function isAckMessage(msg) {
122
+ return "a" in msg && !("uid" in msg);
123
+ }
124
+ function isJoinAckMessage(msg) {
125
+ return "a" in msg && "uid" in msg;
126
+ }
127
+ function isNackMessage(msg) {
128
+ return "n" in msg;
129
+ }
130
+ function isEventMessage(msg) {
131
+ return "ev" in msg;
132
+ }
133
+ function isOnceResponseMessage(msg) {
134
+ return "oc" in msg;
135
+ }
136
+ function isPushAckMessage(msg) {
137
+ return "pa" in msg;
138
+ }
139
+
140
+ // src/connection/MessageQueue.ts
141
+ var MessageQueue = class {
142
+ constructor(defaultTimeout = 3e4) {
143
+ this.nextId = 1;
144
+ this.pending = /* @__PURE__ */ new Map();
145
+ this.defaultTimeout = defaultTimeout;
146
+ }
147
+ /**
148
+ * Generate a new unique request ID.
149
+ */
150
+ nextRequestId() {
151
+ return String(this.nextId++);
152
+ }
153
+ /**
154
+ * Register a pending request that expects a response.
155
+ * Returns a promise that resolves when ack is received, or rejects on nack/timeout.
156
+ */
157
+ registerRequest(requestId, timeout) {
158
+ return new Promise((resolve, reject) => {
159
+ const timeoutMs = timeout ?? this.defaultTimeout;
160
+ const timeoutHandle = setTimeout(() => {
161
+ this.pending.delete(requestId);
162
+ reject(new LarkError("timeout", `Request ${requestId} timed out after ${timeoutMs}ms`));
163
+ }, timeoutMs);
164
+ this.pending.set(requestId, {
165
+ resolve,
166
+ reject,
167
+ timeout: timeoutHandle
168
+ });
169
+ });
170
+ }
171
+ /**
172
+ * Handle an incoming server message. If it's a response to a pending request,
173
+ * resolve or reject the corresponding promise.
174
+ *
175
+ * @returns true if the message was handled (was a response), false otherwise
176
+ */
177
+ handleMessage(message) {
178
+ if (isJoinAckMessage(message)) {
179
+ const pending = this.pending.get(message.a);
180
+ if (pending) {
181
+ clearTimeout(pending.timeout);
182
+ this.pending.delete(message.a);
183
+ pending.resolve({
184
+ uid: message.uid,
185
+ provider: message.provider,
186
+ token: message.token
187
+ });
188
+ return true;
189
+ }
190
+ }
191
+ if (isAckMessage(message)) {
192
+ const pending = this.pending.get(message.a);
193
+ if (pending) {
194
+ clearTimeout(pending.timeout);
195
+ this.pending.delete(message.a);
196
+ pending.resolve(void 0);
197
+ return true;
198
+ }
199
+ }
200
+ if (isNackMessage(message)) {
201
+ const pending = this.pending.get(message.n);
202
+ if (pending) {
203
+ clearTimeout(pending.timeout);
204
+ this.pending.delete(message.n);
205
+ pending.reject(new LarkError(message.e, message.m));
206
+ return true;
207
+ }
208
+ }
209
+ if (isOnceResponseMessage(message)) {
210
+ const pending = this.pending.get(message.oc);
211
+ if (pending) {
212
+ clearTimeout(pending.timeout);
213
+ this.pending.delete(message.oc);
214
+ pending.resolve(message.ov);
215
+ return true;
216
+ }
217
+ }
218
+ if (isPushAckMessage(message)) {
219
+ const pending = this.pending.get(message.pa);
220
+ if (pending) {
221
+ clearTimeout(pending.timeout);
222
+ this.pending.delete(message.pa);
223
+ pending.resolve(message.pk);
224
+ return true;
225
+ }
226
+ }
227
+ return false;
228
+ }
229
+ /**
230
+ * Reject all pending requests (e.g., on disconnect).
231
+ */
232
+ rejectAll(error) {
233
+ for (const [, pending] of this.pending) {
234
+ clearTimeout(pending.timeout);
235
+ pending.reject(error);
236
+ }
237
+ this.pending.clear();
238
+ }
239
+ /**
240
+ * Get the number of pending requests.
241
+ */
242
+ get pendingCount() {
243
+ return this.pending.size;
244
+ }
245
+ };
246
+
247
+ // src/connection/SubscriptionManager.ts
248
+ var SubscriptionManager = class {
249
+ constructor() {
250
+ // path -> eventType -> array of subscriptions
251
+ this.subscriptions = /* @__PURE__ */ new Map();
252
+ // Callback to send subscribe message to server
253
+ this.sendSubscribe = null;
254
+ // Callback to send unsubscribe message to server
255
+ this.sendUnsubscribe = null;
256
+ // Callback to create DataSnapshot from event data
257
+ this.createSnapshot = null;
258
+ }
259
+ /**
260
+ * Initialize the manager with server communication callbacks.
261
+ */
262
+ initialize(options) {
263
+ this.sendSubscribe = options.sendSubscribe;
264
+ this.sendUnsubscribe = options.sendUnsubscribe;
265
+ this.createSnapshot = options.createSnapshot;
266
+ }
267
+ /**
268
+ * Subscribe to events at a path.
269
+ * Returns an unsubscribe function.
270
+ */
271
+ subscribe(path, eventType, callback) {
272
+ const normalizedPath = path;
273
+ const isFirstForPath = !this.subscriptions.has(normalizedPath);
274
+ const existingEventTypes = this.getEventTypesForPath(normalizedPath);
275
+ const isFirstForEventType = !existingEventTypes.includes(eventType);
276
+ if (!this.subscriptions.has(normalizedPath)) {
277
+ this.subscriptions.set(normalizedPath, /* @__PURE__ */ new Map());
278
+ }
279
+ const pathSubs = this.subscriptions.get(normalizedPath);
280
+ if (!pathSubs.has(eventType)) {
281
+ pathSubs.set(eventType, []);
282
+ }
283
+ const eventSubs = pathSubs.get(eventType);
284
+ const unsubscribe = () => {
285
+ this.unsubscribeCallback(normalizedPath, eventType, callback);
286
+ };
287
+ eventSubs.push({ callback, unsubscribe });
288
+ if (isFirstForPath || isFirstForEventType) {
289
+ const allEventTypes = this.getEventTypesForPath(normalizedPath);
290
+ const shortEventTypes = allEventTypes.map((et) => eventTypeToShort[et]);
291
+ this.sendSubscribe?.(normalizedPath, shortEventTypes).catch((err) => {
292
+ console.error("Failed to subscribe:", err);
293
+ });
294
+ }
295
+ return unsubscribe;
296
+ }
297
+ /**
298
+ * Remove a specific callback from a subscription.
299
+ */
300
+ unsubscribeCallback(path, eventType, callback) {
301
+ const pathSubs = this.subscriptions.get(path);
302
+ if (!pathSubs) return;
303
+ const eventSubs = pathSubs.get(eventType);
304
+ if (!eventSubs) return;
305
+ const index = eventSubs.findIndex((entry) => entry.callback === callback);
306
+ if (index !== -1) {
307
+ eventSubs.splice(index, 1);
308
+ }
309
+ if (eventSubs.length === 0) {
310
+ pathSubs.delete(eventType);
311
+ }
312
+ if (pathSubs.size === 0) {
313
+ this.subscriptions.delete(path);
314
+ this.sendUnsubscribe?.(path).catch((err) => {
315
+ console.error("Failed to unsubscribe:", err);
316
+ });
317
+ }
318
+ }
319
+ /**
320
+ * Remove all subscriptions of a specific event type at a path.
321
+ */
322
+ unsubscribeEventType(path, eventType) {
323
+ const normalizedPath = path;
324
+ const pathSubs = this.subscriptions.get(normalizedPath);
325
+ if (!pathSubs) return;
326
+ pathSubs.delete(eventType);
327
+ if (pathSubs.size === 0) {
328
+ this.subscriptions.delete(normalizedPath);
329
+ this.sendUnsubscribe?.(normalizedPath).catch((err) => {
330
+ console.error("Failed to unsubscribe:", err);
331
+ });
332
+ } else {
333
+ const remainingEventTypes = this.getEventTypesForPath(normalizedPath);
334
+ const shortEventTypes = remainingEventTypes.map((et) => eventTypeToShort[et]);
335
+ this.sendSubscribe?.(normalizedPath, shortEventTypes).catch((err) => {
336
+ console.error("Failed to update subscription:", err);
337
+ });
338
+ }
339
+ }
340
+ /**
341
+ * Remove ALL subscriptions at a path.
342
+ */
343
+ unsubscribeAll(path) {
344
+ const normalizedPath = path;
345
+ if (!this.subscriptions.has(normalizedPath)) return;
346
+ this.subscriptions.delete(normalizedPath);
347
+ this.sendUnsubscribe?.(normalizedPath).catch((err) => {
348
+ console.error("Failed to unsubscribe:", err);
349
+ });
350
+ }
351
+ /**
352
+ * Handle an incoming event message from the server.
353
+ */
354
+ handleEvent(message) {
355
+ const eventType = eventTypeFromShort[message.ev];
356
+ if (!eventType) {
357
+ console.warn("Unknown event type:", message.ev);
358
+ return;
359
+ }
360
+ const path = message.p;
361
+ const pathSubs = this.subscriptions.get(path);
362
+ if (!pathSubs) return;
363
+ const eventSubs = pathSubs.get(eventType);
364
+ if (!eventSubs || eventSubs.length === 0) return;
365
+ let snapshotPath;
366
+ let snapshotValue;
367
+ if (eventType === "value") {
368
+ snapshotPath = path;
369
+ snapshotValue = message.v;
370
+ } else {
371
+ snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
372
+ snapshotValue = message.v;
373
+ }
374
+ const snapshot = this.createSnapshot?.(snapshotPath, snapshotValue, message.x ?? false);
375
+ if (!snapshot) return;
376
+ for (const entry of eventSubs) {
377
+ try {
378
+ entry.callback(snapshot, void 0);
379
+ } catch (err) {
380
+ console.error("Error in subscription callback:", err);
381
+ }
382
+ }
383
+ }
384
+ /**
385
+ * Get all event types currently subscribed at a path.
386
+ */
387
+ getEventTypesForPath(path) {
388
+ const pathSubs = this.subscriptions.get(path);
389
+ if (!pathSubs) return [];
390
+ return Array.from(pathSubs.keys());
391
+ }
392
+ /**
393
+ * Clear all subscriptions (e.g., on disconnect).
394
+ */
395
+ clear() {
396
+ this.subscriptions.clear();
397
+ }
398
+ /**
399
+ * Check if there are any subscriptions at a path.
400
+ */
401
+ hasSubscriptions(path) {
402
+ return this.subscriptions.has(path);
403
+ }
404
+ };
405
+
406
+ // src/connection/WebSocketClient.ts
407
+ var import_ws = __toESM(require("ws"));
408
+ var WebSocketImpl = typeof WebSocket !== "undefined" ? WebSocket : import_ws.default;
409
+ var WebSocketClient = class {
410
+ constructor(options) {
411
+ this.ws = null;
412
+ this._state = "disconnected";
413
+ this.options = options;
414
+ }
415
+ get state() {
416
+ return this._state;
417
+ }
418
+ get connected() {
419
+ return this._state === "connected";
420
+ }
421
+ /**
422
+ * Connect to a WebSocket server.
423
+ */
424
+ connect(url) {
425
+ return new Promise((resolve, reject) => {
426
+ if (this._state !== "disconnected") {
427
+ reject(new Error("Already connected or connecting"));
428
+ return;
429
+ }
430
+ this._state = "connecting";
431
+ try {
432
+ this.ws = new WebSocketImpl(url);
433
+ } catch (err) {
434
+ this._state = "disconnected";
435
+ reject(err);
436
+ return;
437
+ }
438
+ const onOpen = () => {
439
+ cleanup();
440
+ this._state = "connected";
441
+ this.setupEventHandlers();
442
+ resolve();
443
+ this.options.onOpen();
444
+ };
445
+ const onError = (event) => {
446
+ cleanup();
447
+ this._state = "disconnected";
448
+ this.ws = null;
449
+ reject(new Error("WebSocket connection failed"));
450
+ this.options.onError(event);
451
+ };
452
+ const onClose = (event) => {
453
+ cleanup();
454
+ this._state = "disconnected";
455
+ this.ws = null;
456
+ reject(new Error(`WebSocket closed: ${event.code} ${event.reason}`));
457
+ };
458
+ const cleanup = () => {
459
+ this.ws?.removeEventListener("open", onOpen);
460
+ this.ws?.removeEventListener("error", onError);
461
+ this.ws?.removeEventListener("close", onClose);
462
+ };
463
+ this.ws.addEventListener("open", onOpen);
464
+ this.ws.addEventListener("error", onError);
465
+ this.ws.addEventListener("close", onClose);
466
+ });
467
+ }
468
+ /**
469
+ * Set up persistent event handlers after connection is established.
470
+ */
471
+ setupEventHandlers() {
472
+ if (!this.ws) return;
473
+ this.ws.addEventListener("message", (event) => {
474
+ this.options.onMessage(event.data);
475
+ });
476
+ this.ws.addEventListener("close", (event) => {
477
+ this._state = "disconnected";
478
+ this.ws = null;
479
+ this.options.onClose(event.code, event.reason);
480
+ });
481
+ this.ws.addEventListener("error", (event) => {
482
+ this.options.onError(event);
483
+ });
484
+ }
485
+ /**
486
+ * Send a message over the WebSocket.
487
+ */
488
+ send(data) {
489
+ if (!this.ws || this._state !== "connected") {
490
+ throw new Error("WebSocket not connected");
491
+ }
492
+ this.ws.send(data);
493
+ }
494
+ /**
495
+ * Close the WebSocket connection.
496
+ */
497
+ close(code = 1e3, reason = "Client disconnect") {
498
+ if (this.ws) {
499
+ this.ws.close(code, reason);
500
+ this.ws = null;
501
+ }
502
+ this._state = "disconnected";
503
+ }
504
+ };
505
+
506
+ // src/OnDisconnect.ts
507
+ var OnDisconnect = class {
508
+ constructor(db, path) {
509
+ this._db = db;
510
+ this._path = path;
511
+ }
512
+ /**
513
+ * Set a value when disconnected.
514
+ */
515
+ async set(value) {
516
+ await this._db._sendOnDisconnect(this._path, OnDisconnectAction.SET, value);
517
+ }
518
+ /**
519
+ * Update values when disconnected.
520
+ */
521
+ async update(values) {
522
+ await this._db._sendOnDisconnect(this._path, OnDisconnectAction.UPDATE, values);
523
+ }
524
+ /**
525
+ * Remove data when disconnected.
526
+ */
527
+ async remove() {
528
+ await this._db._sendOnDisconnect(this._path, OnDisconnectAction.DELETE);
529
+ }
530
+ /**
531
+ * Cancel any pending onDisconnect handlers at this location.
532
+ */
533
+ async cancel() {
534
+ await this._db._sendOnDisconnect(this._path, OnDisconnectAction.CANCEL);
535
+ }
536
+ /**
537
+ * Set a value with priority when disconnected.
538
+ */
539
+ async setWithPriority(value, priority) {
540
+ await this._db._sendOnDisconnect(this._path, OnDisconnectAction.SET, value, priority);
541
+ }
542
+ };
543
+
544
+ // src/utils/path.ts
545
+ function normalizePath(path) {
546
+ if (!path || path === "/") return "/";
547
+ const segments = path.split("/").filter((segment) => segment.length > 0);
548
+ if (segments.length === 0) return "/";
549
+ return "/" + segments.join("/");
550
+ }
551
+ function joinPath(...segments) {
552
+ const allParts = [];
553
+ for (const segment of segments) {
554
+ if (!segment || segment === "/") continue;
555
+ const parts = segment.split("/").filter((p) => p.length > 0);
556
+ allParts.push(...parts);
557
+ }
558
+ if (allParts.length === 0) return "/";
559
+ return "/" + allParts.join("/");
560
+ }
561
+ function getParentPath(path) {
562
+ const normalized = normalizePath(path);
563
+ if (normalized === "/") return "/";
564
+ const lastSlash = normalized.lastIndexOf("/");
565
+ if (lastSlash <= 0) return "/";
566
+ return normalized.substring(0, lastSlash);
567
+ }
568
+ function getKey(path) {
569
+ const normalized = normalizePath(path);
570
+ if (normalized === "/") return null;
571
+ const lastSlash = normalized.lastIndexOf("/");
572
+ return normalized.substring(lastSlash + 1);
573
+ }
574
+ function getValueAtPath(obj, path) {
575
+ const normalized = normalizePath(path);
576
+ if (normalized === "/") return obj;
577
+ const segments = normalized.split("/").filter((s) => s.length > 0);
578
+ let current = obj;
579
+ for (const segment of segments) {
580
+ if (current === null || current === void 0) {
581
+ return void 0;
582
+ }
583
+ if (typeof current !== "object") {
584
+ return void 0;
585
+ }
586
+ current = current[segment];
587
+ }
588
+ return current;
589
+ }
590
+
591
+ // src/utils/pushid.ts
592
+ var PUSH_CHARS = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
593
+ var lastPushTime = 0;
594
+ var lastRandChars = [];
595
+ function generatePushId() {
596
+ let now = Date.now();
597
+ const duplicateTime = now === lastPushTime;
598
+ lastPushTime = now;
599
+ const timeChars = new Array(8);
600
+ for (let i = 7; i >= 0; i--) {
601
+ timeChars[i] = PUSH_CHARS.charAt(now % 64);
602
+ now = Math.floor(now / 64);
603
+ }
604
+ let id = timeChars.join("");
605
+ if (!duplicateTime) {
606
+ lastRandChars = [];
607
+ for (let i = 0; i < 12; i++) {
608
+ lastRandChars.push(Math.floor(Math.random() * 64));
609
+ }
610
+ } else {
611
+ let i = 11;
612
+ while (i >= 0 && lastRandChars[i] === 63) {
613
+ lastRandChars[i] = 0;
614
+ i--;
615
+ }
616
+ if (i >= 0) {
617
+ lastRandChars[i]++;
618
+ }
619
+ }
620
+ for (let i = 0; i < 12; i++) {
621
+ id += PUSH_CHARS.charAt(lastRandChars[i]);
622
+ }
623
+ return id;
624
+ }
625
+
626
+ // src/DatabaseReference.ts
627
+ var DatabaseReference = class _DatabaseReference {
628
+ constructor(db, path, query = {}) {
629
+ this._db = db;
630
+ this._path = normalizePath(path);
631
+ this._query = query;
632
+ }
633
+ /**
634
+ * The path of this reference.
635
+ */
636
+ get path() {
637
+ return this._path;
638
+ }
639
+ /**
640
+ * The last segment of the path (the "key"), or null for root.
641
+ */
642
+ get key() {
643
+ return getKey(this._path);
644
+ }
645
+ /**
646
+ * Get a reference to the parent node, or null if this is root.
647
+ */
648
+ get parent() {
649
+ if (this._path === "/") return null;
650
+ const parentPath = getParentPath(this._path);
651
+ return new _DatabaseReference(this._db, parentPath);
652
+ }
653
+ /**
654
+ * Get a reference to the root of the database.
655
+ */
656
+ get root() {
657
+ return new _DatabaseReference(this._db, "");
658
+ }
659
+ // ============================================
660
+ // Navigation
661
+ // ============================================
662
+ /**
663
+ * Get a reference to a child path.
664
+ */
665
+ child(path) {
666
+ const childPath = joinPath(this._path, path);
667
+ return new _DatabaseReference(this._db, childPath);
668
+ }
669
+ // ============================================
670
+ // Write Operations
671
+ // ============================================
672
+ /**
673
+ * Set the data at this location, overwriting any existing data.
674
+ */
675
+ async set(value) {
676
+ await this._db._sendSet(this._path, value);
677
+ }
678
+ /**
679
+ * Update specific children at this location without overwriting other children.
680
+ */
681
+ async update(values) {
682
+ await this._db._sendUpdate(this._path, values);
683
+ }
684
+ /**
685
+ * Remove the data at this location.
686
+ */
687
+ async remove() {
688
+ await this._db._sendDelete(this._path);
689
+ }
690
+ /**
691
+ * Generate a new child location with a unique key and optionally set its value.
692
+ *
693
+ * If value is provided, sets the value and returns a Promise that resolves
694
+ * to the new reference.
695
+ *
696
+ * If no value is provided, returns a reference immediately with a client-generated
697
+ * push key (you can then call set() on it).
698
+ */
699
+ push(value) {
700
+ if (value === void 0) {
701
+ const key = generatePushId();
702
+ return this.child(key);
703
+ }
704
+ return this._db._sendPush(this._path, value).then((key) => {
705
+ return this.child(key);
706
+ });
707
+ }
708
+ /**
709
+ * Set the data with a priority value for ordering.
710
+ */
711
+ async setWithPriority(value, priority) {
712
+ await this._db._sendSet(this._path, value, priority);
713
+ }
714
+ /**
715
+ * Set the priority of the data at this location.
716
+ */
717
+ async setPriority(priority) {
718
+ console.warn("setPriority: fetching current value to preserve it");
719
+ const snapshot = await this.once();
720
+ await this.setWithPriority(snapshot.val(), priority);
721
+ }
722
+ // ============================================
723
+ // Read Operations
724
+ // ============================================
725
+ /**
726
+ * Read the data at this location once.
727
+ */
728
+ async once(eventType = "value") {
729
+ if (eventType !== "value") {
730
+ throw new Error('once() only supports "value" event type');
731
+ }
732
+ return this._db._sendOnce(this._path, this._buildQueryParams());
733
+ }
734
+ // ============================================
735
+ // Subscriptions
736
+ // ============================================
737
+ /**
738
+ * Subscribe to events at this location.
739
+ * Returns an unsubscribe function.
740
+ */
741
+ on(eventType, callback) {
742
+ return this._db._subscribe(this._path, eventType, callback);
743
+ }
744
+ /**
745
+ * Unsubscribe from events.
746
+ * If eventType is specified, removes all listeners of that type.
747
+ * If no eventType, removes ALL listeners at this path.
748
+ */
749
+ off(eventType) {
750
+ if (eventType) {
751
+ this._db._unsubscribeEventType(this._path, eventType);
752
+ } else {
753
+ this._db._unsubscribeAll(this._path);
754
+ }
755
+ }
756
+ // ============================================
757
+ // OnDisconnect
758
+ // ============================================
759
+ /**
760
+ * Get an OnDisconnect handler for this location.
761
+ */
762
+ onDisconnect() {
763
+ return new OnDisconnect(this._db, this._path);
764
+ }
765
+ // ============================================
766
+ // Query Modifiers
767
+ // ============================================
768
+ /**
769
+ * Order results by key.
770
+ */
771
+ orderByKey() {
772
+ return new _DatabaseReference(this._db, this._path, {
773
+ ...this._query,
774
+ orderBy: "key"
775
+ });
776
+ }
777
+ /**
778
+ * Order results by priority.
779
+ */
780
+ orderByPriority() {
781
+ return new _DatabaseReference(this._db, this._path, {
782
+ ...this._query,
783
+ orderBy: "priority"
784
+ });
785
+ }
786
+ /**
787
+ * Order results by a child key.
788
+ * NOTE: Phase 2 - not yet implemented on server.
789
+ */
790
+ orderByChild(path) {
791
+ console.warn("orderByChild() is not yet implemented");
792
+ return new _DatabaseReference(this._db, this._path, {
793
+ ...this._query,
794
+ orderBy: "child",
795
+ orderByChildPath: path
796
+ });
797
+ }
798
+ /**
799
+ * Order results by value.
800
+ * NOTE: Phase 2 - not yet implemented on server.
801
+ */
802
+ orderByValue() {
803
+ console.warn("orderByValue() is not yet implemented");
804
+ return new _DatabaseReference(this._db, this._path, {
805
+ ...this._query,
806
+ orderBy: "value"
807
+ });
808
+ }
809
+ /**
810
+ * Limit to the first N results.
811
+ */
812
+ limitToFirst(limit) {
813
+ return new _DatabaseReference(this._db, this._path, {
814
+ ...this._query,
815
+ limitToFirst: limit
816
+ });
817
+ }
818
+ /**
819
+ * Limit to the last N results.
820
+ */
821
+ limitToLast(limit) {
822
+ return new _DatabaseReference(this._db, this._path, {
823
+ ...this._query,
824
+ limitToLast: limit
825
+ });
826
+ }
827
+ /**
828
+ * Start at a specific value/key.
829
+ * NOTE: Phase 2 - not yet implemented on server.
830
+ */
831
+ startAt(value, key) {
832
+ console.warn("startAt() is not yet implemented");
833
+ return new _DatabaseReference(this._db, this._path, {
834
+ ...this._query,
835
+ startAt: { value, key }
836
+ });
837
+ }
838
+ /**
839
+ * End at a specific value/key.
840
+ * NOTE: Phase 2 - not yet implemented on server.
841
+ */
842
+ endAt(value, key) {
843
+ console.warn("endAt() is not yet implemented");
844
+ return new _DatabaseReference(this._db, this._path, {
845
+ ...this._query,
846
+ endAt: { value, key }
847
+ });
848
+ }
849
+ /**
850
+ * Filter to items equal to a specific value.
851
+ * NOTE: Phase 2 - not yet implemented on server.
852
+ */
853
+ equalTo(value, key) {
854
+ console.warn("equalTo() is not yet implemented");
855
+ return new _DatabaseReference(this._db, this._path, {
856
+ ...this._query,
857
+ equalTo: { value, key }
858
+ });
859
+ }
860
+ // ============================================
861
+ // Internal Helpers
862
+ // ============================================
863
+ /**
864
+ * Build query parameters for wire protocol.
865
+ */
866
+ _buildQueryParams() {
867
+ const params = {};
868
+ let hasParams = false;
869
+ if (this._query.orderBy === "key") {
870
+ params.ob = "k";
871
+ hasParams = true;
872
+ } else if (this._query.orderBy === "priority") {
873
+ params.ob = "p";
874
+ hasParams = true;
875
+ }
876
+ if (this._query.limitToFirst !== void 0) {
877
+ params.lf = this._query.limitToFirst;
878
+ hasParams = true;
879
+ }
880
+ if (this._query.limitToLast !== void 0) {
881
+ params.ll = this._query.limitToLast;
882
+ hasParams = true;
883
+ }
884
+ return hasParams ? params : void 0;
885
+ }
886
+ /**
887
+ * Returns the absolute URL for this database location.
888
+ * Format: https://db.lark.sh/project/database/path/to/data
889
+ */
890
+ toString() {
891
+ const baseUrl = this._db._getBaseUrl();
892
+ if (this._path === "/") {
893
+ return baseUrl;
894
+ }
895
+ return `${baseUrl}${this._path}`;
896
+ }
897
+ };
898
+
899
+ // src/DataSnapshot.ts
900
+ var DataSnapshot = class _DataSnapshot {
901
+ constructor(data, path, db, options = {}) {
902
+ this._data = data;
903
+ this._path = normalizePath(path);
904
+ this._db = db;
905
+ this._volatile = options.volatile ?? false;
906
+ this._priority = options.priority ?? null;
907
+ }
908
+ /**
909
+ * Get a DatabaseReference for the location of this snapshot.
910
+ */
911
+ get ref() {
912
+ return this._db.ref(this._path);
913
+ }
914
+ /**
915
+ * Get the last segment of the path (the "key"), or null for root.
916
+ */
917
+ get key() {
918
+ return getKey(this._path);
919
+ }
920
+ /**
921
+ * Get the raw data value.
922
+ */
923
+ val() {
924
+ return this._data;
925
+ }
926
+ /**
927
+ * Check if data exists at this location (is not null/undefined).
928
+ */
929
+ exists() {
930
+ return this._data !== null && this._data !== void 0;
931
+ }
932
+ /**
933
+ * Get a child snapshot at the specified path.
934
+ */
935
+ child(path) {
936
+ const childPath = joinPath(this._path, path);
937
+ const childData = getValueAtPath(this._data, path);
938
+ return new _DataSnapshot(childData, childPath, this._db, {
939
+ volatile: this._volatile
940
+ });
941
+ }
942
+ /**
943
+ * Check if this snapshot has any children.
944
+ */
945
+ hasChildren() {
946
+ if (typeof this._data !== "object" || this._data === null) {
947
+ return false;
948
+ }
949
+ return Object.keys(this._data).length > 0;
950
+ }
951
+ /**
952
+ * Check if this snapshot has a specific child.
953
+ */
954
+ hasChild(path) {
955
+ const childData = getValueAtPath(this._data, path);
956
+ return childData !== void 0 && childData !== null;
957
+ }
958
+ /**
959
+ * Get the number of children.
960
+ */
961
+ numChildren() {
962
+ if (typeof this._data !== "object" || this._data === null) {
963
+ return 0;
964
+ }
965
+ return Object.keys(this._data).length;
966
+ }
967
+ /**
968
+ * Iterate over children. Return true from callback to stop iteration.
969
+ */
970
+ forEach(callback) {
971
+ if (typeof this._data !== "object" || this._data === null) {
972
+ return;
973
+ }
974
+ const keys = Object.keys(this._data);
975
+ for (const key of keys) {
976
+ const childSnap = this.child(key);
977
+ if (callback(childSnap) === true) {
978
+ break;
979
+ }
980
+ }
981
+ }
982
+ /**
983
+ * Get the priority of the data at this location.
984
+ */
985
+ getPriority() {
986
+ return this._priority;
987
+ }
988
+ /**
989
+ * Check if this snapshot was from a volatile (high-frequency) update.
990
+ * This is a Lark extension not present in Firebase.
991
+ */
992
+ isVolatile() {
993
+ return this._volatile;
994
+ }
995
+ /**
996
+ * Export the snapshot data as JSON (alias for val()).
997
+ */
998
+ toJSON() {
999
+ return this._data;
1000
+ }
1001
+ };
1002
+
1003
+ // src/utils/jwt.ts
1004
+ function decodeJwtPayload(token) {
1005
+ const parts = token.split(".");
1006
+ if (parts.length !== 3) {
1007
+ throw new Error("Invalid JWT format");
1008
+ }
1009
+ const payload = parts[1];
1010
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
1011
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
1012
+ let decoded;
1013
+ if (typeof atob === "function") {
1014
+ decoded = atob(padded);
1015
+ } else {
1016
+ decoded = Buffer.from(padded, "base64").toString("utf-8");
1017
+ }
1018
+ return JSON.parse(decoded);
1019
+ }
1020
+
1021
+ // src/LarkDatabase.ts
1022
+ var LarkDatabase = class {
1023
+ constructor() {
1024
+ this._state = "disconnected";
1025
+ this._auth = null;
1026
+ this._databaseId = null;
1027
+ this._coordinatorUrl = null;
1028
+ this.ws = null;
1029
+ // Event callbacks
1030
+ this.connectCallbacks = /* @__PURE__ */ new Set();
1031
+ this.disconnectCallbacks = /* @__PURE__ */ new Set();
1032
+ this.errorCallbacks = /* @__PURE__ */ new Set();
1033
+ this.messageQueue = new MessageQueue();
1034
+ this.subscriptionManager = new SubscriptionManager();
1035
+ }
1036
+ // ============================================
1037
+ // Connection State
1038
+ // ============================================
1039
+ /**
1040
+ * Whether the database is currently connected.
1041
+ */
1042
+ get connected() {
1043
+ return this._state === "connected";
1044
+ }
1045
+ /**
1046
+ * Current auth info, or null if not connected.
1047
+ */
1048
+ get auth() {
1049
+ return this._auth;
1050
+ }
1051
+ /**
1052
+ * @internal Get the base URL for reference toString().
1053
+ */
1054
+ _getBaseUrl() {
1055
+ if (this._coordinatorUrl && this._databaseId) {
1056
+ return `${this._coordinatorUrl}/${this._databaseId}`;
1057
+ }
1058
+ return "lark://";
1059
+ }
1060
+ // ============================================
1061
+ // Connection Management
1062
+ // ============================================
1063
+ /**
1064
+ * Connect to a database.
1065
+ *
1066
+ * @param databaseId - Database ID in format "project/database"
1067
+ * @param options - Connection options (token, anonymous, coordinator URL)
1068
+ */
1069
+ async connect(databaseId, options = {}) {
1070
+ if (this._state !== "disconnected") {
1071
+ throw new Error("Already connected or connecting");
1072
+ }
1073
+ this._state = "connecting";
1074
+ this._databaseId = databaseId;
1075
+ this._coordinatorUrl = options.coordinator || DEFAULT_COORDINATOR_URL;
1076
+ try {
1077
+ const coordinatorUrl = this._coordinatorUrl;
1078
+ const coordinator = new Coordinator(coordinatorUrl);
1079
+ const connectResponse = await coordinator.connect(databaseId, {
1080
+ token: options.token,
1081
+ anonymous: options.anonymous
1082
+ });
1083
+ const wsUrl = connectResponse.ws_url;
1084
+ this.ws = new WebSocketClient({
1085
+ onMessage: this.handleMessage.bind(this),
1086
+ onOpen: () => {
1087
+ },
1088
+ // Handled in connect flow
1089
+ onClose: this.handleClose.bind(this),
1090
+ onError: this.handleError.bind(this)
1091
+ });
1092
+ await this.ws.connect(wsUrl);
1093
+ const requestId = this.messageQueue.nextRequestId();
1094
+ const joinMessage = {
1095
+ o: "j",
1096
+ t: connectResponse.token,
1097
+ r: requestId
1098
+ };
1099
+ this.send(joinMessage);
1100
+ await this.messageQueue.registerRequest(requestId);
1101
+ const jwtPayload = decodeJwtPayload(connectResponse.token);
1102
+ this._auth = {
1103
+ uid: jwtPayload.sub,
1104
+ provider: jwtPayload.provider,
1105
+ token: jwtPayload.claims || {}
1106
+ };
1107
+ this._state = "connected";
1108
+ this.subscriptionManager.initialize({
1109
+ sendSubscribe: this.sendSubscribeMessage.bind(this),
1110
+ sendUnsubscribe: this.sendUnsubscribeMessage.bind(this),
1111
+ createSnapshot: this.createSnapshot.bind(this)
1112
+ });
1113
+ this.connectCallbacks.forEach((cb) => cb());
1114
+ } catch (error) {
1115
+ this._state = "disconnected";
1116
+ this._auth = null;
1117
+ this._databaseId = null;
1118
+ this.ws?.close();
1119
+ this.ws = null;
1120
+ throw error;
1121
+ }
1122
+ }
1123
+ /**
1124
+ * Disconnect from the database.
1125
+ * This triggers onDisconnect hooks on the server.
1126
+ */
1127
+ async disconnect() {
1128
+ if (this._state === "disconnected") {
1129
+ return;
1130
+ }
1131
+ if (this._state === "connected" && this.ws) {
1132
+ try {
1133
+ const requestId = this.messageQueue.nextRequestId();
1134
+ this.send({ o: "l", r: requestId });
1135
+ await Promise.race([
1136
+ this.messageQueue.registerRequest(requestId),
1137
+ new Promise((resolve) => setTimeout(resolve, 1e3))
1138
+ ]);
1139
+ } catch {
1140
+ }
1141
+ }
1142
+ this.cleanup();
1143
+ }
1144
+ /**
1145
+ * Clean up connection state.
1146
+ */
1147
+ cleanup() {
1148
+ this.ws?.close();
1149
+ this.ws = null;
1150
+ this._state = "disconnected";
1151
+ this._auth = null;
1152
+ this._databaseId = null;
1153
+ this._coordinatorUrl = null;
1154
+ this.subscriptionManager.clear();
1155
+ this.messageQueue.rejectAll(new Error("Connection closed"));
1156
+ }
1157
+ // ============================================
1158
+ // Reference Access
1159
+ // ============================================
1160
+ /**
1161
+ * Get a reference to a path in the database.
1162
+ */
1163
+ ref(path = "") {
1164
+ return new DatabaseReference(this, path);
1165
+ }
1166
+ // ============================================
1167
+ // Connection Events
1168
+ // ============================================
1169
+ /**
1170
+ * Register a callback for when connection is established.
1171
+ * Returns an unsubscribe function.
1172
+ */
1173
+ onConnect(callback) {
1174
+ this.connectCallbacks.add(callback);
1175
+ return () => this.connectCallbacks.delete(callback);
1176
+ }
1177
+ /**
1178
+ * Register a callback for when connection is lost.
1179
+ * Returns an unsubscribe function.
1180
+ */
1181
+ onDisconnect(callback) {
1182
+ this.disconnectCallbacks.add(callback);
1183
+ return () => this.disconnectCallbacks.delete(callback);
1184
+ }
1185
+ /**
1186
+ * Register a callback for connection errors.
1187
+ * Returns an unsubscribe function.
1188
+ */
1189
+ onError(callback) {
1190
+ this.errorCallbacks.add(callback);
1191
+ return () => this.errorCallbacks.delete(callback);
1192
+ }
1193
+ // ============================================
1194
+ // Internal: Message Handling
1195
+ // ============================================
1196
+ handleMessage(data) {
1197
+ let message;
1198
+ try {
1199
+ message = JSON.parse(data);
1200
+ } catch {
1201
+ console.error("Failed to parse message:", data);
1202
+ return;
1203
+ }
1204
+ if (this.messageQueue.handleMessage(message)) {
1205
+ return;
1206
+ }
1207
+ if (isEventMessage(message)) {
1208
+ this.subscriptionManager.handleEvent(message);
1209
+ }
1210
+ }
1211
+ handleClose(code, reason) {
1212
+ const wasConnected = this._state === "connected";
1213
+ this.cleanup();
1214
+ if (wasConnected) {
1215
+ this.disconnectCallbacks.forEach((cb) => cb());
1216
+ }
1217
+ }
1218
+ handleError(event) {
1219
+ const error = new Error("WebSocket error");
1220
+ this.errorCallbacks.forEach((cb) => cb(error));
1221
+ }
1222
+ // ============================================
1223
+ // Internal: Sending Messages
1224
+ // ============================================
1225
+ send(message) {
1226
+ if (!this.ws || !this.ws.connected) {
1227
+ throw new LarkError("not_connected", "Not connected to database");
1228
+ }
1229
+ this.ws.send(JSON.stringify(message));
1230
+ }
1231
+ /**
1232
+ * @internal Send a set operation.
1233
+ */
1234
+ async _sendSet(path, value, priority) {
1235
+ const requestId = this.messageQueue.nextRequestId();
1236
+ const message = {
1237
+ o: "s",
1238
+ p: normalizePath(path) || "/",
1239
+ v: value,
1240
+ r: requestId
1241
+ };
1242
+ if (priority !== void 0) {
1243
+ message.y = priority;
1244
+ }
1245
+ this.send(message);
1246
+ await this.messageQueue.registerRequest(requestId);
1247
+ }
1248
+ /**
1249
+ * @internal Send an update operation.
1250
+ */
1251
+ async _sendUpdate(path, values) {
1252
+ const requestId = this.messageQueue.nextRequestId();
1253
+ const message = {
1254
+ o: "u",
1255
+ p: normalizePath(path) || "/",
1256
+ v: values,
1257
+ r: requestId
1258
+ };
1259
+ this.send(message);
1260
+ await this.messageQueue.registerRequest(requestId);
1261
+ }
1262
+ /**
1263
+ * @internal Send a delete operation.
1264
+ */
1265
+ async _sendDelete(path) {
1266
+ const requestId = this.messageQueue.nextRequestId();
1267
+ const message = {
1268
+ o: "d",
1269
+ p: normalizePath(path) || "/",
1270
+ r: requestId
1271
+ };
1272
+ this.send(message);
1273
+ await this.messageQueue.registerRequest(requestId);
1274
+ }
1275
+ /**
1276
+ * @internal Send a push operation. Returns the generated key.
1277
+ */
1278
+ async _sendPush(path, value) {
1279
+ const requestId = this.messageQueue.nextRequestId();
1280
+ const message = {
1281
+ o: "p",
1282
+ p: normalizePath(path) || "/",
1283
+ v: value,
1284
+ r: requestId
1285
+ };
1286
+ this.send(message);
1287
+ const key = await this.messageQueue.registerRequest(requestId);
1288
+ return key;
1289
+ }
1290
+ /**
1291
+ * @internal Send a once (read) operation.
1292
+ */
1293
+ async _sendOnce(path, query) {
1294
+ const requestId = this.messageQueue.nextRequestId();
1295
+ const message = {
1296
+ o: "o",
1297
+ p: normalizePath(path) || "/",
1298
+ r: requestId
1299
+ };
1300
+ if (query) {
1301
+ message.q = query;
1302
+ }
1303
+ this.send(message);
1304
+ const value = await this.messageQueue.registerRequest(requestId);
1305
+ return new DataSnapshot(value, path, this);
1306
+ }
1307
+ /**
1308
+ * @internal Send an onDisconnect operation.
1309
+ */
1310
+ async _sendOnDisconnect(path, action, value, priority) {
1311
+ const requestId = this.messageQueue.nextRequestId();
1312
+ const message = {
1313
+ o: "od",
1314
+ p: normalizePath(path) || "/",
1315
+ a: action,
1316
+ r: requestId
1317
+ };
1318
+ if (value !== void 0) {
1319
+ message.v = value;
1320
+ }
1321
+ if (priority !== void 0) {
1322
+ message.y = priority;
1323
+ }
1324
+ this.send(message);
1325
+ await this.messageQueue.registerRequest(requestId);
1326
+ }
1327
+ /**
1328
+ * @internal Send a subscribe message to server.
1329
+ */
1330
+ async sendSubscribeMessage(path, eventTypes) {
1331
+ const requestId = this.messageQueue.nextRequestId();
1332
+ const message = {
1333
+ o: "sb",
1334
+ p: normalizePath(path) || "/",
1335
+ e: eventTypes,
1336
+ r: requestId
1337
+ };
1338
+ this.send(message);
1339
+ await this.messageQueue.registerRequest(requestId);
1340
+ }
1341
+ /**
1342
+ * @internal Send an unsubscribe message to server.
1343
+ */
1344
+ async sendUnsubscribeMessage(path) {
1345
+ const requestId = this.messageQueue.nextRequestId();
1346
+ const message = {
1347
+ o: "us",
1348
+ p: normalizePath(path) || "/",
1349
+ r: requestId
1350
+ };
1351
+ this.send(message);
1352
+ await this.messageQueue.registerRequest(requestId);
1353
+ }
1354
+ /**
1355
+ * @internal Create a DataSnapshot from event data.
1356
+ */
1357
+ createSnapshot(path, value, volatile) {
1358
+ return new DataSnapshot(value, path, this, { volatile });
1359
+ }
1360
+ // ============================================
1361
+ // Internal: Subscription Management
1362
+ // ============================================
1363
+ /**
1364
+ * @internal Subscribe to events at a path.
1365
+ */
1366
+ _subscribe(path, eventType, callback) {
1367
+ return this.subscriptionManager.subscribe(path, eventType, callback);
1368
+ }
1369
+ /**
1370
+ * @internal Unsubscribe from a specific event type at a path.
1371
+ */
1372
+ _unsubscribeEventType(path, eventType) {
1373
+ this.subscriptionManager.unsubscribeEventType(path, eventType);
1374
+ }
1375
+ /**
1376
+ * @internal Unsubscribe from all events at a path.
1377
+ */
1378
+ _unsubscribeAll(path) {
1379
+ this.subscriptionManager.unsubscribeAll(path);
1380
+ }
1381
+ };
1382
+ // Annotate the CommonJS export names for ESM import in node:
1383
+ 0 && (module.exports = {
1384
+ DataSnapshot,
1385
+ DatabaseReference,
1386
+ LarkDatabase,
1387
+ LarkError,
1388
+ OnDisconnect,
1389
+ generatePushId
1390
+ });
1391
+ //# sourceMappingURL=index.js.map