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.
@@ -1,5 +1,4 @@
1
- import { B as BotWorkerPool, M as Migration } from '../BotWorkerPool-9ndHQt2g.cjs';
2
- import 'zod';
1
+ import { B as BotWorkerPool, M as Migration } from '../BotWorkerPool-h_8a20dt.cjs';
3
2
  import 'node-telegram-bot-api';
4
3
  import 'eventemitter3';
5
4
 
@@ -1,5 +1,4 @@
1
- import { B as BotWorkerPool, M as Migration } from '../BotWorkerPool-9ndHQt2g.js';
2
- import 'zod';
1
+ import { B as BotWorkerPool, M as Migration } from '../BotWorkerPool-h_8a20dt.js';
3
2
  import 'node-telegram-bot-api';
4
3
  import 'eventemitter3';
5
4
 
@@ -1244,9 +1244,10 @@ var INDEX_TAG = "__GRAMOBASE_INDEX__";
1244
1244
  var DOC_TAG = "__GRAMOBASE_DOC__";
1245
1245
  var MAX_MSG_BYTES = 4e3;
1246
1246
  var TelegramStorage = class {
1247
- constructor(pool, defaultChannelId, encryptionKey, debug = false) {
1247
+ constructor(pool, defaultChannelId, registry, encryptionKey, debug = false) {
1248
1248
  this.pool = pool;
1249
1249
  this.defaultChannelId = defaultChannelId;
1250
+ this.registry = registry;
1250
1251
  this.debug = debug;
1251
1252
  if (encryptionKey) {
1252
1253
  this.encryptionKey = crypto.createHash("sha256").update(encryptionKey).digest();
@@ -1254,20 +1255,52 @@ var TelegramStorage = class {
1254
1255
  }
1255
1256
  pool;
1256
1257
  defaultChannelId;
1258
+ registry;
1257
1259
  debug;
1258
1260
  encryptionKey = null;
1259
1261
  // collection → pinned index message ID
1260
1262
  indexMsgIds = /* @__PURE__ */ new Map();
1261
1263
  // ─── Index management ─────────────────────────────────────────────────────
1264
+ async readRawMessageText(msgId, channel) {
1265
+ try {
1266
+ const msg = await this.pool.execute(
1267
+ (bot) => bot.forwardMessage(channel, channel, msgId)
1268
+ );
1269
+ if (!msg?.text) return null;
1270
+ let text = msg.text;
1271
+ if (this.encryptionKey && text.startsWith("ENC:")) {
1272
+ text = this.decrypt(text);
1273
+ }
1274
+ return text;
1275
+ } catch {
1276
+ return null;
1277
+ }
1278
+ }
1262
1279
  async loadIndex(collection, channelId) {
1263
1280
  const channel = channelId ?? this.defaultChannelId;
1281
+ try {
1282
+ const msgId = await this.registry.getCollectionIndexMsgId(collection);
1283
+ if (msgId) {
1284
+ const text = await this.readRawMessageText(msgId, channel);
1285
+ if (text && text.startsWith(INDEX_TAG)) {
1286
+ const json = text.replace(INDEX_TAG + "\n", "");
1287
+ const parsed = JSON.parse(json);
1288
+ this.indexMsgIds.set(collection, msgId);
1289
+ return parsed;
1290
+ }
1291
+ }
1292
+ } catch {
1293
+ }
1264
1294
  try {
1265
1295
  const chat = await this.pool.execute((bot) => bot.getChat(channel));
1266
1296
  if (chat.pinned_message?.text?.startsWith(INDEX_TAG)) {
1267
1297
  const json = chat.pinned_message.text.replace(INDEX_TAG + "\n", "");
1268
1298
  const parsed = JSON.parse(json);
1269
- this.indexMsgIds.set(collection, chat.pinned_message.message_id);
1270
- return parsed;
1299
+ if (parsed.collection === collection) {
1300
+ this.indexMsgIds.set(collection, chat.pinned_message.message_id);
1301
+ await this.registry.setCollectionIndexMsgId(collection, chat.pinned_message.message_id);
1302
+ return parsed;
1303
+ }
1271
1304
  }
1272
1305
  } catch {
1273
1306
  }
@@ -1282,7 +1315,7 @@ var TelegramStorage = class {
1282
1315
  const channel = channelId ?? this.defaultChannelId;
1283
1316
  const text = `${INDEX_TAG}
1284
1317
  ${JSON.stringify(index)}`;
1285
- const existingMsgId = this.indexMsgIds.get(index.collection);
1318
+ const existingMsgId = this.indexMsgIds.get(index.collection) || await this.registry.getCollectionIndexMsgId(index.collection);
1286
1319
  if (existingMsgId) {
1287
1320
  try {
1288
1321
  await this.pool.execute(
@@ -1298,10 +1331,9 @@ ${JSON.stringify(index)}`;
1298
1331
  const msg = await this.pool.execute(
1299
1332
  (bot) => bot.sendMessage(channel, text, { disable_notification: true })
1300
1333
  );
1301
- this.indexMsgIds.set(index.collection, msg.message_id);
1302
- await this.pool.execute(
1303
- (bot) => bot.pinChatMessage(channel, msg.message_id, { disable_notification: true })
1304
- );
1334
+ const newMsgId = msg.message_id;
1335
+ this.indexMsgIds.set(index.collection, newMsgId);
1336
+ await this.registry.setCollectionIndexMsgId(index.collection, newMsgId);
1305
1337
  }
1306
1338
  // ─── Document CRUD ────────────────────────────────────────────────────────
1307
1339
  async writeDocument(doc, channelId) {
@@ -1321,10 +1353,9 @@ ${JSON.stringify(index)}`;
1321
1353
  async readDocument(msgId, channelId) {
1322
1354
  const channel = channelId ?? this.defaultChannelId;
1323
1355
  try {
1324
- const msgs = await this.pool.execute(
1325
- (bot) => bot.forwardMessages(channel, channel, [msgId])
1356
+ const msg = await this.pool.execute(
1357
+ (bot) => bot.forwardMessage(channel, channel, msgId)
1326
1358
  );
1327
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
1328
1359
  if (!msg?.text) return null;
1329
1360
  let text = msg.text;
1330
1361
  if (this.encryptionKey && text.startsWith("ENC:")) {
@@ -1335,6 +1366,7 @@ ${JSON.stringify(index)}`;
1335
1366
  }
1336
1367
  const parsed = JSON.parse(text);
1337
1368
  delete parsed[DOC_TAG];
1369
+ parsed._msgId = msgId;
1338
1370
  return parsed;
1339
1371
  } catch {
1340
1372
  return null;
@@ -1373,10 +1405,9 @@ ${JSON.stringify(index)}`;
1373
1405
  const msgIds = JSON.parse(headerText.replace("CHUNK:", ""));
1374
1406
  const parts = [];
1375
1407
  for (const id of msgIds) {
1376
- const msgs = await this.pool.execute(
1377
- (bot) => bot.forwardMessages(channel, channel, [id])
1408
+ const msg = await this.pool.execute(
1409
+ (bot) => bot.forwardMessage(channel, channel, id)
1378
1410
  );
1379
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
1380
1411
  if (msg?.text) parts.push(msg.text);
1381
1412
  }
1382
1413
  return parts.join("");
@@ -1577,7 +1608,8 @@ var Registry = class {
1577
1608
  this.state = {
1578
1609
  activeLease: null,
1579
1610
  instanceId: this.instanceId,
1580
- registryMsgId: null
1611
+ registryMsgId: null,
1612
+ indexes: {}
1581
1613
  };
1582
1614
  }
1583
1615
  pool;
@@ -1585,14 +1617,33 @@ var Registry = class {
1585
1617
  debug;
1586
1618
  state;
1587
1619
  instanceId;
1588
- async acquireWriteLease() {
1620
+ async acquireWriteLease(options = { wait: true }) {
1589
1621
  const existing = await this.readRegistryMessage();
1590
1622
  if (existing?.activeLease) {
1591
1623
  const lease2 = existing.activeLease;
1592
1624
  if (lease2.instanceId !== this.instanceId && Date.now() < lease2.expiresAt) {
1593
- throw new Error(
1594
- `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(lease2.expiresAt).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
1595
- );
1625
+ if (!options.wait) {
1626
+ throw new Error(
1627
+ `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(
1628
+ lease2.expiresAt
1629
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
1630
+ );
1631
+ }
1632
+ const waitMs = lease2.expiresAt - Date.now() + 250;
1633
+ if (this.debug) {
1634
+ console.log(
1635
+ `[Registry] Lease held by ${lease2.instanceId}, waiting ${waitMs}ms for it to expire...`
1636
+ );
1637
+ }
1638
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
1639
+ const recheckState = await this.readRegistryMessage();
1640
+ if (recheckState?.activeLease && recheckState.activeLease.instanceId !== this.instanceId && Date.now() < recheckState.activeLease.expiresAt) {
1641
+ throw new Error(
1642
+ `[gramobase Registry] Another instance (${recheckState.activeLease.instanceId}) holds the write lease until ${new Date(
1643
+ recheckState.activeLease.expiresAt
1644
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
1645
+ );
1646
+ }
1596
1647
  }
1597
1648
  }
1598
1649
  const lease = {
@@ -1644,15 +1695,26 @@ var Registry = class {
1644
1695
  if (chat.pinned_message?.text?.startsWith(REGISTRY_TAG)) {
1645
1696
  this.state.registryMsgId = chat.pinned_message.message_id;
1646
1697
  const json = chat.pinned_message.text.replace(REGISTRY_TAG + "\n", "");
1647
- return JSON.parse(json);
1698
+ const parsed = JSON.parse(json);
1699
+ this.state.indexes = parsed.indexes || {};
1700
+ return parsed;
1648
1701
  }
1649
1702
  } catch {
1650
1703
  }
1651
1704
  return null;
1652
1705
  }
1653
1706
  async writeRegistryMessage(data) {
1707
+ let leasePayload = null;
1708
+ if (data.activeLease) {
1709
+ const { heartbeatInterval, ...rest } = data.activeLease;
1710
+ leasePayload = rest;
1711
+ }
1712
+ const payload = {
1713
+ activeLease: leasePayload,
1714
+ indexes: data.indexes || this.state.indexes || {}
1715
+ };
1654
1716
  const text = `${REGISTRY_TAG}
1655
- ${JSON.stringify(data, null, 0)}`;
1717
+ ${JSON.stringify(payload, null, 0)}`;
1656
1718
  if (this.state.registryMsgId) {
1657
1719
  try {
1658
1720
  await this.pool.execute(
@@ -1675,6 +1737,22 @@ ${JSON.stringify(data, null, 0)}`;
1675
1737
  })
1676
1738
  );
1677
1739
  }
1740
+ async getCollectionIndexMsgId(collection) {
1741
+ if (!this.state.registryMsgId) {
1742
+ await this.readRegistryMessage();
1743
+ }
1744
+ return this.state.indexes[collection] || null;
1745
+ }
1746
+ async setCollectionIndexMsgId(collection, msgId) {
1747
+ if (!this.state.registryMsgId) {
1748
+ await this.readRegistryMessage();
1749
+ }
1750
+ this.state.indexes[collection] = msgId;
1751
+ await this.writeRegistryMessage({
1752
+ activeLease: this.state.activeLease,
1753
+ indexes: this.state.indexes
1754
+ });
1755
+ }
1678
1756
  getInstanceId() {
1679
1757
  return this.instanceId;
1680
1758
  }
@@ -2430,9 +2508,15 @@ var GramoBase = class {
2430
2508
  const tokens = Array.isArray(config.botToken) ? config.botToken : [config.botToken];
2431
2509
  this.pool = new BotWorkerPool(tokens, config.concurrency ?? 25, config.debug ?? false);
2432
2510
  this.cache = new HotCache(config.cacheMaxBytes, config.cacheTtlMs);
2511
+ this.registry = new Registry(
2512
+ this.pool,
2513
+ config.indexChannelId ?? config.channelId,
2514
+ config.debug ?? false
2515
+ );
2433
2516
  this.storage = new TelegramStorage(
2434
2517
  this.pool,
2435
2518
  config.channelId,
2519
+ this.registry,
2436
2520
  config.encryptionKey,
2437
2521
  config.debug ?? false
2438
2522
  );
@@ -2441,11 +2525,6 @@ var GramoBase = class {
2441
2525
  config.walChannelId ?? config.channelId,
2442
2526
  config.debug ?? false
2443
2527
  );
2444
- this.registry = new Registry(
2445
- this.pool,
2446
- config.indexChannelId ?? config.channelId,
2447
- config.debug ?? false
2448
- );
2449
2528
  this.realtime = new RealtimeManager(
2450
2529
  this.pool,
2451
2530
  config.webhookUrl,
@@ -2576,7 +2655,20 @@ var GramoBase = class {
2576
2655
  }
2577
2656
  }
2578
2657
  };
2658
+ var globalForGramo = globalThis;
2579
2659
  function createClient(config) {
2660
+ if (config.global) {
2661
+ if (!globalForGramo.__gramobase_clients__) {
2662
+ globalForGramo.__gramobase_clients__ = /* @__PURE__ */ new Map();
2663
+ }
2664
+ const cacheKey = Array.isArray(config.botToken) ? `${config.channelId}:${config.botToken.join(",")}` : `${config.channelId}:${config.botToken}`;
2665
+ if (globalForGramo.__gramobase_clients__.has(cacheKey)) {
2666
+ return globalForGramo.__gramobase_clients__.get(cacheKey);
2667
+ }
2668
+ const client = new GramoBase(config);
2669
+ globalForGramo.__gramobase_clients__.set(cacheKey, client);
2670
+ return client;
2671
+ }
2580
2672
  return new GramoBase(config);
2581
2673
  }
2582
2674
 
@@ -2655,6 +2747,21 @@ async function startStudio(port, cwd = process.cwd()) {
2655
2747
  db = null;
2656
2748
  }
2657
2749
  }
2750
+ const cleanShutdown = async () => {
2751
+ if (db) {
2752
+ try {
2753
+ await db.disconnect();
2754
+ } catch (_) {
2755
+ }
2756
+ db = null;
2757
+ }
2758
+ server.close(() => {
2759
+ process.exit(0);
2760
+ });
2761
+ setTimeout(() => process.exit(0), 1e3);
2762
+ };
2763
+ process.once("SIGINT", cleanShutdown);
2764
+ process.once("SIGTERM", cleanShutdown);
2658
2765
  const sseClients = /* @__PURE__ */ new Set();
2659
2766
  if (db) {
2660
2767
  const forward = (ev) => {