gramobase 1.0.11 → 1.0.13

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.cjs CHANGED
@@ -299,9 +299,10 @@ var INDEX_TAG = "__GRAMOBASE_INDEX__";
299
299
  var DOC_TAG = "__GRAMOBASE_DOC__";
300
300
  var MAX_MSG_BYTES = 4e3;
301
301
  var TelegramStorage = class {
302
- constructor(pool, defaultChannelId, encryptionKey, debug = false) {
302
+ constructor(pool, defaultChannelId, registry, encryptionKey, debug = false) {
303
303
  this.pool = pool;
304
304
  this.defaultChannelId = defaultChannelId;
305
+ this.registry = registry;
305
306
  this.debug = debug;
306
307
  if (encryptionKey) {
307
308
  this.encryptionKey = crypto.createHash("sha256").update(encryptionKey).digest();
@@ -309,20 +310,52 @@ var TelegramStorage = class {
309
310
  }
310
311
  pool;
311
312
  defaultChannelId;
313
+ registry;
312
314
  debug;
313
315
  encryptionKey = null;
314
316
  // collection → pinned index message ID
315
317
  indexMsgIds = /* @__PURE__ */ new Map();
316
318
  // ─── Index management ─────────────────────────────────────────────────────
319
+ async readRawMessageText(msgId, channel) {
320
+ try {
321
+ const msg = await this.pool.execute(
322
+ (bot) => bot.forwardMessage(channel, channel, msgId)
323
+ );
324
+ if (!msg?.text) return null;
325
+ let text = msg.text;
326
+ if (this.encryptionKey && text.startsWith("ENC:")) {
327
+ text = this.decrypt(text);
328
+ }
329
+ return text;
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
317
334
  async loadIndex(collection, channelId) {
318
335
  const channel = channelId ?? this.defaultChannelId;
336
+ try {
337
+ const msgId = await this.registry.getCollectionIndexMsgId(collection);
338
+ if (msgId) {
339
+ const text = await this.readRawMessageText(msgId, channel);
340
+ if (text && text.startsWith(INDEX_TAG)) {
341
+ const json = text.replace(INDEX_TAG + "\n", "");
342
+ const parsed = JSON.parse(json);
343
+ this.indexMsgIds.set(collection, msgId);
344
+ return parsed;
345
+ }
346
+ }
347
+ } catch {
348
+ }
319
349
  try {
320
350
  const chat = await this.pool.execute((bot) => bot.getChat(channel));
321
351
  if (chat.pinned_message?.text?.startsWith(INDEX_TAG)) {
322
352
  const json = chat.pinned_message.text.replace(INDEX_TAG + "\n", "");
323
353
  const parsed = JSON.parse(json);
324
- this.indexMsgIds.set(collection, chat.pinned_message.message_id);
325
- return parsed;
354
+ if (parsed.collection === collection) {
355
+ this.indexMsgIds.set(collection, chat.pinned_message.message_id);
356
+ await this.registry.setCollectionIndexMsgId(collection, chat.pinned_message.message_id);
357
+ return parsed;
358
+ }
326
359
  }
327
360
  } catch {
328
361
  }
@@ -337,7 +370,7 @@ var TelegramStorage = class {
337
370
  const channel = channelId ?? this.defaultChannelId;
338
371
  const text = `${INDEX_TAG}
339
372
  ${JSON.stringify(index)}`;
340
- const existingMsgId = this.indexMsgIds.get(index.collection);
373
+ const existingMsgId = this.indexMsgIds.get(index.collection) || await this.registry.getCollectionIndexMsgId(index.collection);
341
374
  if (existingMsgId) {
342
375
  try {
343
376
  await this.pool.execute(
@@ -353,10 +386,9 @@ ${JSON.stringify(index)}`;
353
386
  const msg = await this.pool.execute(
354
387
  (bot) => bot.sendMessage(channel, text, { disable_notification: true })
355
388
  );
356
- this.indexMsgIds.set(index.collection, msg.message_id);
357
- await this.pool.execute(
358
- (bot) => bot.pinChatMessage(channel, msg.message_id, { disable_notification: true })
359
- );
389
+ const newMsgId = msg.message_id;
390
+ this.indexMsgIds.set(index.collection, newMsgId);
391
+ await this.registry.setCollectionIndexMsgId(index.collection, newMsgId);
360
392
  }
361
393
  // ─── Document CRUD ────────────────────────────────────────────────────────
362
394
  async writeDocument(doc, channelId) {
@@ -376,10 +408,9 @@ ${JSON.stringify(index)}`;
376
408
  async readDocument(msgId, channelId) {
377
409
  const channel = channelId ?? this.defaultChannelId;
378
410
  try {
379
- const msgs = await this.pool.execute(
380
- (bot) => bot.forwardMessages(channel, channel, [msgId])
411
+ const msg = await this.pool.execute(
412
+ (bot) => bot.forwardMessage(channel, channel, msgId)
381
413
  );
382
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
383
414
  if (!msg?.text) return null;
384
415
  let text = msg.text;
385
416
  if (this.encryptionKey && text.startsWith("ENC:")) {
@@ -390,6 +421,7 @@ ${JSON.stringify(index)}`;
390
421
  }
391
422
  const parsed = JSON.parse(text);
392
423
  delete parsed[DOC_TAG];
424
+ parsed._msgId = msgId;
393
425
  return parsed;
394
426
  } catch {
395
427
  return null;
@@ -428,10 +460,9 @@ ${JSON.stringify(index)}`;
428
460
  const msgIds = JSON.parse(headerText.replace("CHUNK:", ""));
429
461
  const parts = [];
430
462
  for (const id of msgIds) {
431
- const msgs = await this.pool.execute(
432
- (bot) => bot.forwardMessages(channel, channel, [id])
463
+ const msg = await this.pool.execute(
464
+ (bot) => bot.forwardMessage(channel, channel, id)
433
465
  );
434
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
435
466
  if (msg?.text) parts.push(msg.text);
436
467
  }
437
468
  return parts.join("");
@@ -632,7 +663,8 @@ var Registry = class {
632
663
  this.state = {
633
664
  activeLease: null,
634
665
  instanceId: this.instanceId,
635
- registryMsgId: null
666
+ registryMsgId: null,
667
+ indexes: {}
636
668
  };
637
669
  }
638
670
  pool;
@@ -640,14 +672,33 @@ var Registry = class {
640
672
  debug;
641
673
  state;
642
674
  instanceId;
643
- async acquireWriteLease() {
675
+ async acquireWriteLease(options = { wait: true }) {
644
676
  const existing = await this.readRegistryMessage();
645
677
  if (existing?.activeLease) {
646
678
  const lease2 = existing.activeLease;
647
679
  if (lease2.instanceId !== this.instanceId && Date.now() < lease2.expiresAt) {
648
- throw new Error(
649
- `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(lease2.expiresAt).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
650
- );
680
+ if (!options.wait) {
681
+ throw new Error(
682
+ `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(
683
+ lease2.expiresAt
684
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
685
+ );
686
+ }
687
+ const waitMs = lease2.expiresAt - Date.now() + 250;
688
+ if (this.debug) {
689
+ console.log(
690
+ `[Registry] Lease held by ${lease2.instanceId}, waiting ${waitMs}ms for it to expire...`
691
+ );
692
+ }
693
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
694
+ const recheckState = await this.readRegistryMessage();
695
+ if (recheckState?.activeLease && recheckState.activeLease.instanceId !== this.instanceId && Date.now() < recheckState.activeLease.expiresAt) {
696
+ throw new Error(
697
+ `[gramobase Registry] Another instance (${recheckState.activeLease.instanceId}) holds the write lease until ${new Date(
698
+ recheckState.activeLease.expiresAt
699
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
700
+ );
701
+ }
651
702
  }
652
703
  }
653
704
  const lease = {
@@ -699,15 +750,26 @@ var Registry = class {
699
750
  if (chat.pinned_message?.text?.startsWith(REGISTRY_TAG)) {
700
751
  this.state.registryMsgId = chat.pinned_message.message_id;
701
752
  const json = chat.pinned_message.text.replace(REGISTRY_TAG + "\n", "");
702
- return JSON.parse(json);
753
+ const parsed = JSON.parse(json);
754
+ this.state.indexes = parsed.indexes || {};
755
+ return parsed;
703
756
  }
704
757
  } catch {
705
758
  }
706
759
  return null;
707
760
  }
708
761
  async writeRegistryMessage(data) {
762
+ let leasePayload = null;
763
+ if (data.activeLease) {
764
+ const { heartbeatInterval, ...rest } = data.activeLease;
765
+ leasePayload = rest;
766
+ }
767
+ const payload = {
768
+ activeLease: leasePayload,
769
+ indexes: data.indexes || this.state.indexes || {}
770
+ };
709
771
  const text = `${REGISTRY_TAG}
710
- ${JSON.stringify(data, null, 0)}`;
772
+ ${JSON.stringify(payload, null, 0)}`;
711
773
  if (this.state.registryMsgId) {
712
774
  try {
713
775
  await this.pool.execute(
@@ -730,6 +792,22 @@ ${JSON.stringify(data, null, 0)}`;
730
792
  })
731
793
  );
732
794
  }
795
+ async getCollectionIndexMsgId(collection) {
796
+ if (!this.state.registryMsgId) {
797
+ await this.readRegistryMessage();
798
+ }
799
+ return this.state.indexes[collection] || null;
800
+ }
801
+ async setCollectionIndexMsgId(collection, msgId) {
802
+ if (!this.state.registryMsgId) {
803
+ await this.readRegistryMessage();
804
+ }
805
+ this.state.indexes[collection] = msgId;
806
+ await this.writeRegistryMessage({
807
+ activeLease: this.state.activeLease,
808
+ indexes: this.state.indexes
809
+ });
810
+ }
733
811
  getInstanceId() {
734
812
  return this.instanceId;
735
813
  }
@@ -1485,9 +1563,15 @@ var GramoBase = class {
1485
1563
  const tokens = Array.isArray(config.botToken) ? config.botToken : [config.botToken];
1486
1564
  this.pool = new BotWorkerPool(tokens, config.concurrency ?? 25, config.debug ?? false);
1487
1565
  this.cache = new HotCache(config.cacheMaxBytes, config.cacheTtlMs);
1566
+ this.registry = new Registry(
1567
+ this.pool,
1568
+ config.indexChannelId ?? config.channelId,
1569
+ config.debug ?? false
1570
+ );
1488
1571
  this.storage = new TelegramStorage(
1489
1572
  this.pool,
1490
1573
  config.channelId,
1574
+ this.registry,
1491
1575
  config.encryptionKey,
1492
1576
  config.debug ?? false
1493
1577
  );
@@ -1496,11 +1580,6 @@ var GramoBase = class {
1496
1580
  config.walChannelId ?? config.channelId,
1497
1581
  config.debug ?? false
1498
1582
  );
1499
- this.registry = new Registry(
1500
- this.pool,
1501
- config.indexChannelId ?? config.channelId,
1502
- config.debug ?? false
1503
- );
1504
1583
  this.realtime = new RealtimeManager(
1505
1584
  this.pool,
1506
1585
  config.webhookUrl,
@@ -1631,7 +1710,20 @@ var GramoBase = class {
1631
1710
  }
1632
1711
  }
1633
1712
  };
1713
+ var globalForGramo = globalThis;
1634
1714
  function createClient(config) {
1715
+ if (config.global) {
1716
+ if (!globalForGramo.__gramobase_clients__) {
1717
+ globalForGramo.__gramobase_clients__ = /* @__PURE__ */ new Map();
1718
+ }
1719
+ const cacheKey = Array.isArray(config.botToken) ? `${config.channelId}:${config.botToken.join(",")}` : `${config.channelId}:${config.botToken}`;
1720
+ if (globalForGramo.__gramobase_clients__.has(cacheKey)) {
1721
+ return globalForGramo.__gramobase_clients__.get(cacheKey);
1722
+ }
1723
+ const client = new GramoBase(config);
1724
+ globalForGramo.__gramobase_clients__.set(cacheKey, client);
1725
+ return client;
1726
+ }
1635
1727
  return new GramoBase(config);
1636
1728
  }
1637
1729