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.
@@ -1213,9 +1213,10 @@ var INDEX_TAG = "__GRAMOBASE_INDEX__";
1213
1213
  var DOC_TAG = "__GRAMOBASE_DOC__";
1214
1214
  var MAX_MSG_BYTES = 4e3;
1215
1215
  var TelegramStorage = class {
1216
- constructor(pool, defaultChannelId, encryptionKey, debug = false) {
1216
+ constructor(pool, defaultChannelId, registry, encryptionKey, debug = false) {
1217
1217
  this.pool = pool;
1218
1218
  this.defaultChannelId = defaultChannelId;
1219
+ this.registry = registry;
1219
1220
  this.debug = debug;
1220
1221
  if (encryptionKey) {
1221
1222
  this.encryptionKey = createHash("sha256").update(encryptionKey).digest();
@@ -1223,20 +1224,52 @@ var TelegramStorage = class {
1223
1224
  }
1224
1225
  pool;
1225
1226
  defaultChannelId;
1227
+ registry;
1226
1228
  debug;
1227
1229
  encryptionKey = null;
1228
1230
  // collection → pinned index message ID
1229
1231
  indexMsgIds = /* @__PURE__ */ new Map();
1230
1232
  // ─── Index management ─────────────────────────────────────────────────────
1233
+ async readRawMessageText(msgId, channel) {
1234
+ try {
1235
+ const msg = await this.pool.execute(
1236
+ (bot) => bot.forwardMessage(channel, channel, msgId)
1237
+ );
1238
+ if (!msg?.text) return null;
1239
+ let text = msg.text;
1240
+ if (this.encryptionKey && text.startsWith("ENC:")) {
1241
+ text = this.decrypt(text);
1242
+ }
1243
+ return text;
1244
+ } catch {
1245
+ return null;
1246
+ }
1247
+ }
1231
1248
  async loadIndex(collection, channelId) {
1232
1249
  const channel = channelId ?? this.defaultChannelId;
1250
+ try {
1251
+ const msgId = await this.registry.getCollectionIndexMsgId(collection);
1252
+ if (msgId) {
1253
+ const text = await this.readRawMessageText(msgId, channel);
1254
+ if (text && text.startsWith(INDEX_TAG)) {
1255
+ const json = text.replace(INDEX_TAG + "\n", "");
1256
+ const parsed = JSON.parse(json);
1257
+ this.indexMsgIds.set(collection, msgId);
1258
+ return parsed;
1259
+ }
1260
+ }
1261
+ } catch {
1262
+ }
1233
1263
  try {
1234
1264
  const chat = await this.pool.execute((bot) => bot.getChat(channel));
1235
1265
  if (chat.pinned_message?.text?.startsWith(INDEX_TAG)) {
1236
1266
  const json = chat.pinned_message.text.replace(INDEX_TAG + "\n", "");
1237
1267
  const parsed = JSON.parse(json);
1238
- this.indexMsgIds.set(collection, chat.pinned_message.message_id);
1239
- return parsed;
1268
+ if (parsed.collection === collection) {
1269
+ this.indexMsgIds.set(collection, chat.pinned_message.message_id);
1270
+ await this.registry.setCollectionIndexMsgId(collection, chat.pinned_message.message_id);
1271
+ return parsed;
1272
+ }
1240
1273
  }
1241
1274
  } catch {
1242
1275
  }
@@ -1251,7 +1284,7 @@ var TelegramStorage = class {
1251
1284
  const channel = channelId ?? this.defaultChannelId;
1252
1285
  const text = `${INDEX_TAG}
1253
1286
  ${JSON.stringify(index)}`;
1254
- const existingMsgId = this.indexMsgIds.get(index.collection);
1287
+ const existingMsgId = this.indexMsgIds.get(index.collection) || await this.registry.getCollectionIndexMsgId(index.collection);
1255
1288
  if (existingMsgId) {
1256
1289
  try {
1257
1290
  await this.pool.execute(
@@ -1267,10 +1300,9 @@ ${JSON.stringify(index)}`;
1267
1300
  const msg = await this.pool.execute(
1268
1301
  (bot) => bot.sendMessage(channel, text, { disable_notification: true })
1269
1302
  );
1270
- this.indexMsgIds.set(index.collection, msg.message_id);
1271
- await this.pool.execute(
1272
- (bot) => bot.pinChatMessage(channel, msg.message_id, { disable_notification: true })
1273
- );
1303
+ const newMsgId = msg.message_id;
1304
+ this.indexMsgIds.set(index.collection, newMsgId);
1305
+ await this.registry.setCollectionIndexMsgId(index.collection, newMsgId);
1274
1306
  }
1275
1307
  // ─── Document CRUD ────────────────────────────────────────────────────────
1276
1308
  async writeDocument(doc, channelId) {
@@ -1290,10 +1322,9 @@ ${JSON.stringify(index)}`;
1290
1322
  async readDocument(msgId, channelId) {
1291
1323
  const channel = channelId ?? this.defaultChannelId;
1292
1324
  try {
1293
- const msgs = await this.pool.execute(
1294
- (bot) => bot.forwardMessages(channel, channel, [msgId])
1325
+ const msg = await this.pool.execute(
1326
+ (bot) => bot.forwardMessage(channel, channel, msgId)
1295
1327
  );
1296
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
1297
1328
  if (!msg?.text) return null;
1298
1329
  let text = msg.text;
1299
1330
  if (this.encryptionKey && text.startsWith("ENC:")) {
@@ -1304,6 +1335,7 @@ ${JSON.stringify(index)}`;
1304
1335
  }
1305
1336
  const parsed = JSON.parse(text);
1306
1337
  delete parsed[DOC_TAG];
1338
+ parsed._msgId = msgId;
1307
1339
  return parsed;
1308
1340
  } catch {
1309
1341
  return null;
@@ -1342,10 +1374,9 @@ ${JSON.stringify(index)}`;
1342
1374
  const msgIds = JSON.parse(headerText.replace("CHUNK:", ""));
1343
1375
  const parts = [];
1344
1376
  for (const id of msgIds) {
1345
- const msgs = await this.pool.execute(
1346
- (bot) => bot.forwardMessages(channel, channel, [id])
1377
+ const msg = await this.pool.execute(
1378
+ (bot) => bot.forwardMessage(channel, channel, id)
1347
1379
  );
1348
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
1349
1380
  if (msg?.text) parts.push(msg.text);
1350
1381
  }
1351
1382
  return parts.join("");
@@ -1546,7 +1577,8 @@ var Registry = class {
1546
1577
  this.state = {
1547
1578
  activeLease: null,
1548
1579
  instanceId: this.instanceId,
1549
- registryMsgId: null
1580
+ registryMsgId: null,
1581
+ indexes: {}
1550
1582
  };
1551
1583
  }
1552
1584
  pool;
@@ -1554,14 +1586,33 @@ var Registry = class {
1554
1586
  debug;
1555
1587
  state;
1556
1588
  instanceId;
1557
- async acquireWriteLease() {
1589
+ async acquireWriteLease(options = { wait: true }) {
1558
1590
  const existing = await this.readRegistryMessage();
1559
1591
  if (existing?.activeLease) {
1560
1592
  const lease2 = existing.activeLease;
1561
1593
  if (lease2.instanceId !== this.instanceId && Date.now() < lease2.expiresAt) {
1562
- throw new Error(
1563
- `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(lease2.expiresAt).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
1564
- );
1594
+ if (!options.wait) {
1595
+ throw new Error(
1596
+ `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(
1597
+ lease2.expiresAt
1598
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
1599
+ );
1600
+ }
1601
+ const waitMs = lease2.expiresAt - Date.now() + 250;
1602
+ if (this.debug) {
1603
+ console.log(
1604
+ `[Registry] Lease held by ${lease2.instanceId}, waiting ${waitMs}ms for it to expire...`
1605
+ );
1606
+ }
1607
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
1608
+ const recheckState = await this.readRegistryMessage();
1609
+ if (recheckState?.activeLease && recheckState.activeLease.instanceId !== this.instanceId && Date.now() < recheckState.activeLease.expiresAt) {
1610
+ throw new Error(
1611
+ `[gramobase Registry] Another instance (${recheckState.activeLease.instanceId}) holds the write lease until ${new Date(
1612
+ recheckState.activeLease.expiresAt
1613
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
1614
+ );
1615
+ }
1565
1616
  }
1566
1617
  }
1567
1618
  const lease = {
@@ -1613,15 +1664,26 @@ var Registry = class {
1613
1664
  if (chat.pinned_message?.text?.startsWith(REGISTRY_TAG)) {
1614
1665
  this.state.registryMsgId = chat.pinned_message.message_id;
1615
1666
  const json = chat.pinned_message.text.replace(REGISTRY_TAG + "\n", "");
1616
- return JSON.parse(json);
1667
+ const parsed = JSON.parse(json);
1668
+ this.state.indexes = parsed.indexes || {};
1669
+ return parsed;
1617
1670
  }
1618
1671
  } catch {
1619
1672
  }
1620
1673
  return null;
1621
1674
  }
1622
1675
  async writeRegistryMessage(data) {
1676
+ let leasePayload = null;
1677
+ if (data.activeLease) {
1678
+ const { heartbeatInterval, ...rest } = data.activeLease;
1679
+ leasePayload = rest;
1680
+ }
1681
+ const payload = {
1682
+ activeLease: leasePayload,
1683
+ indexes: data.indexes || this.state.indexes || {}
1684
+ };
1623
1685
  const text = `${REGISTRY_TAG}
1624
- ${JSON.stringify(data, null, 0)}`;
1686
+ ${JSON.stringify(payload, null, 0)}`;
1625
1687
  if (this.state.registryMsgId) {
1626
1688
  try {
1627
1689
  await this.pool.execute(
@@ -1644,6 +1706,22 @@ ${JSON.stringify(data, null, 0)}`;
1644
1706
  })
1645
1707
  );
1646
1708
  }
1709
+ async getCollectionIndexMsgId(collection) {
1710
+ if (!this.state.registryMsgId) {
1711
+ await this.readRegistryMessage();
1712
+ }
1713
+ return this.state.indexes[collection] || null;
1714
+ }
1715
+ async setCollectionIndexMsgId(collection, msgId) {
1716
+ if (!this.state.registryMsgId) {
1717
+ await this.readRegistryMessage();
1718
+ }
1719
+ this.state.indexes[collection] = msgId;
1720
+ await this.writeRegistryMessage({
1721
+ activeLease: this.state.activeLease,
1722
+ indexes: this.state.indexes
1723
+ });
1724
+ }
1647
1725
  getInstanceId() {
1648
1726
  return this.instanceId;
1649
1727
  }
@@ -2399,9 +2477,15 @@ var GramoBase = class {
2399
2477
  const tokens = Array.isArray(config.botToken) ? config.botToken : [config.botToken];
2400
2478
  this.pool = new BotWorkerPool(tokens, config.concurrency ?? 25, config.debug ?? false);
2401
2479
  this.cache = new HotCache(config.cacheMaxBytes, config.cacheTtlMs);
2480
+ this.registry = new Registry(
2481
+ this.pool,
2482
+ config.indexChannelId ?? config.channelId,
2483
+ config.debug ?? false
2484
+ );
2402
2485
  this.storage = new TelegramStorage(
2403
2486
  this.pool,
2404
2487
  config.channelId,
2488
+ this.registry,
2405
2489
  config.encryptionKey,
2406
2490
  config.debug ?? false
2407
2491
  );
@@ -2410,11 +2494,6 @@ var GramoBase = class {
2410
2494
  config.walChannelId ?? config.channelId,
2411
2495
  config.debug ?? false
2412
2496
  );
2413
- this.registry = new Registry(
2414
- this.pool,
2415
- config.indexChannelId ?? config.channelId,
2416
- config.debug ?? false
2417
- );
2418
2497
  this.realtime = new RealtimeManager(
2419
2498
  this.pool,
2420
2499
  config.webhookUrl,
@@ -2545,7 +2624,20 @@ var GramoBase = class {
2545
2624
  }
2546
2625
  }
2547
2626
  };
2627
+ var globalForGramo = globalThis;
2548
2628
  function createClient(config) {
2629
+ if (config.global) {
2630
+ if (!globalForGramo.__gramobase_clients__) {
2631
+ globalForGramo.__gramobase_clients__ = /* @__PURE__ */ new Map();
2632
+ }
2633
+ const cacheKey = Array.isArray(config.botToken) ? `${config.channelId}:${config.botToken.join(",")}` : `${config.channelId}:${config.botToken}`;
2634
+ if (globalForGramo.__gramobase_clients__.has(cacheKey)) {
2635
+ return globalForGramo.__gramobase_clients__.get(cacheKey);
2636
+ }
2637
+ const client = new GramoBase(config);
2638
+ globalForGramo.__gramobase_clients__.set(cacheKey, client);
2639
+ return client;
2640
+ }
2549
2641
  return new GramoBase(config);
2550
2642
  }
2551
2643
 
@@ -2624,6 +2716,21 @@ async function startStudio(port, cwd = process.cwd()) {
2624
2716
  db = null;
2625
2717
  }
2626
2718
  }
2719
+ const cleanShutdown = async () => {
2720
+ if (db) {
2721
+ try {
2722
+ await db.disconnect();
2723
+ } catch (_) {
2724
+ }
2725
+ db = null;
2726
+ }
2727
+ server.close(() => {
2728
+ process.exit(0);
2729
+ });
2730
+ setTimeout(() => process.exit(0), 1e3);
2731
+ };
2732
+ process.once("SIGINT", cleanShutdown);
2733
+ process.once("SIGTERM", cleanShutdown);
2627
2734
  const sseClients = /* @__PURE__ */ new Set();
2628
2735
  if (db) {
2629
2736
  const forward = (ev) => {