@lark-sh/client 0.1.1 → 0.1.3

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 CHANGED
@@ -17,7 +17,7 @@ var eventTypeToShort = {
17
17
  child_changed: "cc",
18
18
  child_removed: "cr"
19
19
  };
20
- var DEFAULT_COORDINATOR_URL = "https://db.lark.dev";
20
+ var DEFAULT_COORDINATOR_URL = "https://db.lark.sh";
21
21
 
22
22
  // src/connection/Coordinator.ts
23
23
  var Coordinator = class {
@@ -206,6 +206,234 @@ var MessageQueue = class {
206
206
  }
207
207
  };
208
208
 
209
+ // src/utils/path.ts
210
+ function normalizePath(path) {
211
+ if (!path || path === "/") return "/";
212
+ const segments = path.split("/").filter((segment) => segment.length > 0);
213
+ if (segments.length === 0) return "/";
214
+ return "/" + segments.join("/");
215
+ }
216
+ function joinPath(...segments) {
217
+ const allParts = [];
218
+ for (const segment of segments) {
219
+ if (!segment || segment === "/") continue;
220
+ const parts = segment.split("/").filter((p) => p.length > 0);
221
+ allParts.push(...parts);
222
+ }
223
+ if (allParts.length === 0) return "/";
224
+ return "/" + allParts.join("/");
225
+ }
226
+ function getParentPath(path) {
227
+ const normalized = normalizePath(path);
228
+ if (normalized === "/") return "/";
229
+ const lastSlash = normalized.lastIndexOf("/");
230
+ if (lastSlash <= 0) return "/";
231
+ return normalized.substring(0, lastSlash);
232
+ }
233
+ function getKey(path) {
234
+ const normalized = normalizePath(path);
235
+ if (normalized === "/") return null;
236
+ const lastSlash = normalized.lastIndexOf("/");
237
+ return normalized.substring(lastSlash + 1);
238
+ }
239
+ function getValueAtPath(obj, path) {
240
+ const normalized = normalizePath(path);
241
+ if (normalized === "/") return obj;
242
+ const segments = normalized.split("/").filter((s) => s.length > 0);
243
+ let current = obj;
244
+ for (const segment of segments) {
245
+ if (current === null || current === void 0) {
246
+ return void 0;
247
+ }
248
+ if (typeof current !== "object") {
249
+ return void 0;
250
+ }
251
+ current = current[segment];
252
+ }
253
+ return current;
254
+ }
255
+ function setValueAtPath(obj, path, value) {
256
+ const normalized = normalizePath(path);
257
+ if (normalized === "/") {
258
+ return;
259
+ }
260
+ const segments = normalized.split("/").filter((s) => s.length > 0);
261
+ let current = obj;
262
+ for (let i = 0; i < segments.length - 1; i++) {
263
+ const segment = segments[i];
264
+ if (!(segment in current) || typeof current[segment] !== "object" || current[segment] === null) {
265
+ current[segment] = {};
266
+ }
267
+ current = current[segment];
268
+ }
269
+ const lastSegment = segments[segments.length - 1];
270
+ current[lastSegment] = value;
271
+ }
272
+
273
+ // src/cache/DataCache.ts
274
+ var DataCache = class {
275
+ constructor() {
276
+ // path -> cached value
277
+ this.cache = /* @__PURE__ */ new Map();
278
+ }
279
+ /**
280
+ * Store a value at a path.
281
+ * Called when we receive data from a subscription event.
282
+ */
283
+ set(path, value) {
284
+ const normalized = normalizePath(path);
285
+ this.cache.set(normalized, value);
286
+ }
287
+ /**
288
+ * Get the cached value at a path.
289
+ * Returns undefined if not in cache.
290
+ *
291
+ * This method handles nested lookups:
292
+ * - If /boxes is cached with {0: true, 1: false}
293
+ * - get('/boxes/0') returns true (extracted from parent)
294
+ */
295
+ get(path) {
296
+ const normalized = normalizePath(path);
297
+ if (this.cache.has(normalized)) {
298
+ return { value: this.cache.get(normalized), found: true };
299
+ }
300
+ const ancestorResult = this.getFromAncestor(normalized);
301
+ if (ancestorResult.found) {
302
+ return ancestorResult;
303
+ }
304
+ return { value: void 0, found: false };
305
+ }
306
+ /**
307
+ * Check if we have cached data that covers the given path.
308
+ * This includes exact matches and ancestor paths.
309
+ */
310
+ has(path) {
311
+ return this.get(path).found;
312
+ }
313
+ /**
314
+ * Try to get data from an ancestor path.
315
+ * E.g., if /boxes is cached and we want /boxes/5, extract it.
316
+ */
317
+ getFromAncestor(path) {
318
+ const segments = path.split("/").filter((s) => s.length > 0);
319
+ for (let i = segments.length - 1; i >= 0; i--) {
320
+ const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
321
+ if (this.cache.has(ancestorPath)) {
322
+ const ancestorValue = this.cache.get(ancestorPath);
323
+ const relativePath = "/" + segments.slice(i).join("/");
324
+ const extractedValue = getValueAtPath(ancestorValue, relativePath);
325
+ return { value: extractedValue, found: true };
326
+ }
327
+ }
328
+ if (this.cache.has("/")) {
329
+ const rootValue = this.cache.get("/");
330
+ const extractedValue = getValueAtPath(rootValue, path);
331
+ return { value: extractedValue, found: true };
332
+ }
333
+ return { value: void 0, found: false };
334
+ }
335
+ /**
336
+ * Remove cached data at a path.
337
+ * Called when unsubscribing from a path that's no longer covered.
338
+ */
339
+ delete(path) {
340
+ const normalized = normalizePath(path);
341
+ this.cache.delete(normalized);
342
+ }
343
+ /**
344
+ * Remove cached data at a path and all descendant paths.
345
+ * Called when a subscription is removed and no other subscription covers it.
346
+ */
347
+ deleteTree(path) {
348
+ const normalized = normalizePath(path);
349
+ this.cache.delete(normalized);
350
+ const prefix = normalized === "/" ? "/" : normalized + "/";
351
+ for (const cachedPath of this.cache.keys()) {
352
+ if (cachedPath.startsWith(prefix) || normalized === "/" && cachedPath !== "/") {
353
+ this.cache.delete(cachedPath);
354
+ }
355
+ }
356
+ }
357
+ /**
358
+ * Update cache when a child value changes.
359
+ * This updates both the child path and any cached ancestor that contains it.
360
+ *
361
+ * E.g., if /boxes is cached and /boxes/5 changes:
362
+ * - Update the /boxes cache to reflect the new /boxes/5 value
363
+ */
364
+ updateChild(path, value) {
365
+ const normalized = normalizePath(path);
366
+ this.cache.set(normalized, value);
367
+ const segments = normalized.split("/").filter((s) => s.length > 0);
368
+ for (let i = segments.length - 1; i >= 1; i--) {
369
+ const ancestorPath = "/" + segments.slice(0, i).join("/");
370
+ if (this.cache.has(ancestorPath)) {
371
+ const ancestorValue = this.cache.get(ancestorPath);
372
+ if (ancestorValue !== null && typeof ancestorValue === "object") {
373
+ const childKey = segments[i];
374
+ const remainingPath = "/" + segments.slice(i).join("/");
375
+ const updatedAncestor = this.deepClone(ancestorValue);
376
+ setValueAtPath(updatedAncestor, remainingPath, value);
377
+ this.cache.set(ancestorPath, updatedAncestor);
378
+ }
379
+ }
380
+ }
381
+ if (this.cache.has("/") && normalized !== "/") {
382
+ const rootValue = this.cache.get("/");
383
+ if (rootValue !== null && typeof rootValue === "object") {
384
+ const updatedRoot = this.deepClone(rootValue);
385
+ setValueAtPath(updatedRoot, normalized, value);
386
+ this.cache.set("/", updatedRoot);
387
+ }
388
+ }
389
+ }
390
+ /**
391
+ * Remove a child from the cache (e.g., on child_removed event).
392
+ */
393
+ removeChild(path) {
394
+ const normalized = normalizePath(path);
395
+ this.deleteTree(normalized);
396
+ const segments = normalized.split("/").filter((s) => s.length > 0);
397
+ for (let i = segments.length - 1; i >= 1; i--) {
398
+ const ancestorPath = "/" + segments.slice(0, i).join("/");
399
+ if (this.cache.has(ancestorPath)) {
400
+ const ancestorValue = this.cache.get(ancestorPath);
401
+ if (ancestorValue !== null && typeof ancestorValue === "object") {
402
+ const updatedAncestor = this.deepClone(ancestorValue);
403
+ const parentPath = "/" + segments.slice(i, -1).join("/");
404
+ const childKey = segments[segments.length - 1];
405
+ const parent = parentPath === "/" ? updatedAncestor : getValueAtPath(updatedAncestor, parentPath);
406
+ if (parent && typeof parent === "object") {
407
+ delete parent[childKey];
408
+ }
409
+ this.cache.set(ancestorPath, updatedAncestor);
410
+ }
411
+ }
412
+ }
413
+ }
414
+ /**
415
+ * Clear all cached data.
416
+ */
417
+ clear() {
418
+ this.cache.clear();
419
+ }
420
+ /**
421
+ * Get the number of cached paths (for testing/debugging).
422
+ */
423
+ get size() {
424
+ return this.cache.size;
425
+ }
426
+ /**
427
+ * Deep clone a value to avoid mutation issues.
428
+ */
429
+ deepClone(value) {
430
+ if (value === null || typeof value !== "object") {
431
+ return value;
432
+ }
433
+ return JSON.parse(JSON.stringify(value));
434
+ }
435
+ };
436
+
209
437
  // src/connection/SubscriptionManager.ts
210
438
  var SubscriptionManager = class {
211
439
  constructor() {
@@ -217,6 +445,7 @@ var SubscriptionManager = class {
217
445
  this.sendUnsubscribe = null;
218
446
  // Callback to create DataSnapshot from event data
219
447
  this.createSnapshot = null;
448
+ this.cache = new DataCache();
220
449
  }
221
450
  /**
222
451
  * Initialize the manager with server communication callbacks.
@@ -329,6 +558,15 @@ var SubscriptionManager = class {
329
558
  if (eventType === "value") {
330
559
  snapshotPath = path;
331
560
  snapshotValue = message.v;
561
+ this.cache.set(path, snapshotValue);
562
+ } else if (eventType === "child_added" || eventType === "child_changed") {
563
+ snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
564
+ snapshotValue = message.v;
565
+ this.cache.updateChild(snapshotPath, snapshotValue);
566
+ } else if (eventType === "child_removed") {
567
+ snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
568
+ snapshotValue = message.v;
569
+ this.cache.removeChild(snapshotPath);
332
570
  } else {
333
571
  snapshotPath = path === "/" ? `/${message.k}` : `${path}/${message.k}`;
334
572
  snapshotValue = message.v;
@@ -356,6 +594,7 @@ var SubscriptionManager = class {
356
594
  */
357
595
  clear() {
358
596
  this.subscriptions.clear();
597
+ this.cache.clear();
359
598
  }
360
599
  /**
361
600
  * Check if there are any subscriptions at a path.
@@ -363,6 +602,65 @@ var SubscriptionManager = class {
363
602
  hasSubscriptions(path) {
364
603
  return this.subscriptions.has(path);
365
604
  }
605
+ /**
606
+ * Check if a path is "covered" by an active subscription.
607
+ *
608
+ * A path is covered if:
609
+ * - There's an active 'value' subscription at that exact path, OR
610
+ * - There's an active 'value' subscription at an ancestor path
611
+ *
612
+ * Child event subscriptions (child_added, etc.) don't provide full coverage
613
+ * because they only notify of changes, not the complete value.
614
+ */
615
+ isPathCovered(path) {
616
+ const normalized = normalizePath(path);
617
+ if (this.hasValueSubscription(normalized)) {
618
+ return true;
619
+ }
620
+ const segments = normalized.split("/").filter((s) => s.length > 0);
621
+ for (let i = segments.length - 1; i >= 0; i--) {
622
+ const ancestorPath = i === 0 ? "/" : "/" + segments.slice(0, i).join("/");
623
+ if (this.hasValueSubscription(ancestorPath)) {
624
+ return true;
625
+ }
626
+ }
627
+ if (normalized !== "/" && this.hasValueSubscription("/")) {
628
+ return true;
629
+ }
630
+ return false;
631
+ }
632
+ /**
633
+ * Check if there's a 'value' subscription at a path.
634
+ */
635
+ hasValueSubscription(path) {
636
+ const pathSubs = this.subscriptions.get(path);
637
+ if (!pathSubs) return false;
638
+ const valueSubs = pathSubs.get("value");
639
+ return valueSubs !== void 0 && valueSubs.length > 0;
640
+ }
641
+ /**
642
+ * Get a cached value if the path is covered by an active subscription.
643
+ *
644
+ * Returns { value, found: true } if we have cached data for this path
645
+ * (either exact match or extractable from a cached ancestor).
646
+ *
647
+ * Returns { value: undefined, found: false } if:
648
+ * - The path is not covered by any subscription, OR
649
+ * - We don't have cached data yet
650
+ */
651
+ getCachedValue(path) {
652
+ const normalized = normalizePath(path);
653
+ if (!this.isPathCovered(normalized)) {
654
+ return { value: void 0, found: false };
655
+ }
656
+ return this.cache.get(normalized);
657
+ }
658
+ /**
659
+ * Get the cache size (for testing/debugging).
660
+ */
661
+ get cacheSize() {
662
+ return this.cache.size;
663
+ }
366
664
  };
367
665
 
368
666
  // src/connection/WebSocketClient.ts
@@ -503,53 +801,6 @@ var OnDisconnect = class {
503
801
  }
504
802
  };
505
803
 
506
- // src/utils/path.ts
507
- function normalizePath(path) {
508
- if (!path || path === "/") return "/";
509
- const segments = path.split("/").filter((segment) => segment.length > 0);
510
- if (segments.length === 0) return "/";
511
- return "/" + segments.join("/");
512
- }
513
- function joinPath(...segments) {
514
- const allParts = [];
515
- for (const segment of segments) {
516
- if (!segment || segment === "/") continue;
517
- const parts = segment.split("/").filter((p) => p.length > 0);
518
- allParts.push(...parts);
519
- }
520
- if (allParts.length === 0) return "/";
521
- return "/" + allParts.join("/");
522
- }
523
- function getParentPath(path) {
524
- const normalized = normalizePath(path);
525
- if (normalized === "/") return "/";
526
- const lastSlash = normalized.lastIndexOf("/");
527
- if (lastSlash <= 0) return "/";
528
- return normalized.substring(0, lastSlash);
529
- }
530
- function getKey(path) {
531
- const normalized = normalizePath(path);
532
- if (normalized === "/") return null;
533
- const lastSlash = normalized.lastIndexOf("/");
534
- return normalized.substring(lastSlash + 1);
535
- }
536
- function getValueAtPath(obj, path) {
537
- const normalized = normalizePath(path);
538
- if (normalized === "/") return obj;
539
- const segments = normalized.split("/").filter((s) => s.length > 0);
540
- let current = obj;
541
- for (const segment of segments) {
542
- if (current === null || current === void 0) {
543
- return void 0;
544
- }
545
- if (typeof current !== "object") {
546
- return void 0;
547
- }
548
- current = current[segment];
549
- }
550
- return current;
551
- }
552
-
553
804
  // src/utils/pushid.ts
554
805
  var PUSH_CHARS = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
555
806
  var lastPushTime = 0;
@@ -1255,12 +1506,28 @@ var LarkDatabase = class {
1255
1506
  }
1256
1507
  /**
1257
1508
  * @internal Send a once (read) operation.
1509
+ *
1510
+ * This method first checks if the data is available in the local cache
1511
+ * (from an active subscription). If so, it returns the cached value
1512
+ * immediately without a server round-trip.
1513
+ *
1514
+ * Cache is only used when:
1515
+ * - No query parameters are specified (queries may filter/order differently)
1516
+ * - The path is covered by an active 'value' subscription
1517
+ * - We have cached data available
1258
1518
  */
1259
1519
  async _sendOnce(path, query) {
1520
+ const normalizedPath = normalizePath(path) || "/";
1521
+ if (!query) {
1522
+ const cached = this.subscriptionManager.getCachedValue(normalizedPath);
1523
+ if (cached.found) {
1524
+ return new DataSnapshot(cached.value, path, this);
1525
+ }
1526
+ }
1260
1527
  const requestId = this.messageQueue.nextRequestId();
1261
1528
  const message = {
1262
1529
  o: "o",
1263
- p: normalizePath(path) || "/",
1530
+ p: normalizedPath,
1264
1531
  r: requestId
1265
1532
  };
1266
1533
  if (query) {