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.d.cts CHANGED
@@ -1,40 +1,9 @@
1
- import { B as BotWorkerPool, L as Lease, G as GramoBaseEvent, a as GramoBaseConfig, C as CollectionConfig, A as AuthConfig, U as UploadOptions, F as FileRecord, M as Migration, W as WorkerStats } from './BotWorkerPool-9ndHQt2g.cjs';
2
- export { b as ComparisonOperator, c as Filter, d as FindOptions, e as GramoBaseDocument, S as Session, f as UpdateOperators, g as User, h as WalEntry, i as WalOpType, j as WithId } from './BotWorkerPool-9ndHQt2g.cjs';
3
- import { z } from 'zod';
4
- import { C as Collection, G as GramoBaseAuth } from './GramoBaseAuth-CHNn2_e5.cjs';
1
+ import { B as BotWorkerPool, G as GramoBaseEvent, a as GramoBaseConfig, S as SchemaLike, C as CollectionConfig, A as AuthConfig, U as UploadOptions, F as FileRecord, M as Migration, W as WorkerStats } from './BotWorkerPool-h_8a20dt.cjs';
2
+ export { b as ComparisonOperator, c as Filter, d as FindOptions, e as GramoBaseDocument, I as InferSchema, L as Lease, f as Session, g as UpdateOperators, h as User, i as WalEntry, j as WalOpType, k as WithId } from './BotWorkerPool-h_8a20dt.cjs';
3
+ import { R as Registry, C as Collection, G as GramoBaseAuth } from './GramoBaseAuth-ObeOxqKj.cjs';
5
4
  import EventEmitter from 'eventemitter3';
6
5
  import 'node-telegram-bot-api';
7
-
8
- /**
9
- * Registry uses a pinned Telegram message as a distributed lock.
10
- *
11
- * When a gramobase instance starts up, it reads the registry message.
12
- * If no lease exists or the existing lease is expired, it writes a new
13
- * lease with its own instanceId and begins sending heartbeats.
14
- *
15
- * This prevents multiple writer processes from corrupting the index
16
- * (last-write-wins races on the pinned index message).
17
- *
18
- * Read-only operations are always permitted. Only index mutations
19
- * require holding the write lease.
20
- */
21
- declare class Registry {
22
- private pool;
23
- private channelId;
24
- private debug;
25
- private state;
26
- private readonly instanceId;
27
- constructor(pool: BotWorkerPool, channelId: string, debug?: boolean);
28
- acquireWriteLease(): Promise<Lease>;
29
- releaseWriteLease(): Promise<void>;
30
- forceRelease(): Promise<void>;
31
- isWriteLeaseHeld(): Promise<boolean>;
32
- private heartbeat;
33
- private readRegistryMessage;
34
- private writeRegistryMessage;
35
- getInstanceId(): string;
36
- getCurrentLease(): Lease | null;
37
- }
6
+ import 'zod';
38
7
 
39
8
  type Unsubscribe = () => void;
40
9
  /**
@@ -78,7 +47,7 @@ declare class GramoBase {
78
47
  constructor(config: GramoBaseConfig);
79
48
  connect(): Promise<this>;
80
49
  disconnect(): Promise<void>;
81
- collection<T extends z.ZodType>(name: string, config: CollectionConfig<T>): Collection<T>;
50
+ collection<T extends SchemaLike>(name: string, config: CollectionConfig<T>): Collection<T>;
82
51
  createAuth(config: AuthConfig): GramoBaseAuth;
83
52
  uploadFile(data: Buffer, options?: UploadOptions): Promise<FileRecord>;
84
53
  getFileUrl(fileId: string): Promise<string>;
@@ -101,4 +70,4 @@ declare class GramoBase {
101
70
  }
102
71
  declare function createClient(config: GramoBaseConfig): GramoBase;
103
72
 
104
- export { AuthConfig, Collection, CollectionConfig, FileRecord, GramoBase, GramoBaseAuth, GramoBaseConfig, GramoBaseEvent, Lease, Migration, RealtimeManager, UploadOptions, createClient };
73
+ export { AuthConfig, Collection, CollectionConfig, FileRecord, GramoBase, GramoBaseAuth, GramoBaseConfig, GramoBaseEvent, Migration, RealtimeManager, SchemaLike, UploadOptions, createClient };
package/dist/index.d.ts CHANGED
@@ -1,40 +1,9 @@
1
- import { B as BotWorkerPool, L as Lease, G as GramoBaseEvent, a as GramoBaseConfig, C as CollectionConfig, A as AuthConfig, U as UploadOptions, F as FileRecord, M as Migration, W as WorkerStats } from './BotWorkerPool-9ndHQt2g.js';
2
- export { b as ComparisonOperator, c as Filter, d as FindOptions, e as GramoBaseDocument, S as Session, f as UpdateOperators, g as User, h as WalEntry, i as WalOpType, j as WithId } from './BotWorkerPool-9ndHQt2g.js';
3
- import { z } from 'zod';
4
- import { C as Collection, G as GramoBaseAuth } from './GramoBaseAuth-00fg0u_b.js';
1
+ import { B as BotWorkerPool, G as GramoBaseEvent, a as GramoBaseConfig, S as SchemaLike, C as CollectionConfig, A as AuthConfig, U as UploadOptions, F as FileRecord, M as Migration, W as WorkerStats } from './BotWorkerPool-h_8a20dt.js';
2
+ export { b as ComparisonOperator, c as Filter, d as FindOptions, e as GramoBaseDocument, I as InferSchema, L as Lease, f as Session, g as UpdateOperators, h as User, i as WalEntry, j as WalOpType, k as WithId } from './BotWorkerPool-h_8a20dt.js';
3
+ import { R as Registry, C as Collection, G as GramoBaseAuth } from './GramoBaseAuth-DfRKq2yW.js';
5
4
  import EventEmitter from 'eventemitter3';
6
5
  import 'node-telegram-bot-api';
7
-
8
- /**
9
- * Registry uses a pinned Telegram message as a distributed lock.
10
- *
11
- * When a gramobase instance starts up, it reads the registry message.
12
- * If no lease exists or the existing lease is expired, it writes a new
13
- * lease with its own instanceId and begins sending heartbeats.
14
- *
15
- * This prevents multiple writer processes from corrupting the index
16
- * (last-write-wins races on the pinned index message).
17
- *
18
- * Read-only operations are always permitted. Only index mutations
19
- * require holding the write lease.
20
- */
21
- declare class Registry {
22
- private pool;
23
- private channelId;
24
- private debug;
25
- private state;
26
- private readonly instanceId;
27
- constructor(pool: BotWorkerPool, channelId: string, debug?: boolean);
28
- acquireWriteLease(): Promise<Lease>;
29
- releaseWriteLease(): Promise<void>;
30
- forceRelease(): Promise<void>;
31
- isWriteLeaseHeld(): Promise<boolean>;
32
- private heartbeat;
33
- private readRegistryMessage;
34
- private writeRegistryMessage;
35
- getInstanceId(): string;
36
- getCurrentLease(): Lease | null;
37
- }
6
+ import 'zod';
38
7
 
39
8
  type Unsubscribe = () => void;
40
9
  /**
@@ -78,7 +47,7 @@ declare class GramoBase {
78
47
  constructor(config: GramoBaseConfig);
79
48
  connect(): Promise<this>;
80
49
  disconnect(): Promise<void>;
81
- collection<T extends z.ZodType>(name: string, config: CollectionConfig<T>): Collection<T>;
50
+ collection<T extends SchemaLike>(name: string, config: CollectionConfig<T>): Collection<T>;
82
51
  createAuth(config: AuthConfig): GramoBaseAuth;
83
52
  uploadFile(data: Buffer, options?: UploadOptions): Promise<FileRecord>;
84
53
  getFileUrl(fileId: string): Promise<string>;
@@ -101,4 +70,4 @@ declare class GramoBase {
101
70
  }
102
71
  declare function createClient(config: GramoBaseConfig): GramoBase;
103
72
 
104
- export { AuthConfig, Collection, CollectionConfig, FileRecord, GramoBase, GramoBaseAuth, GramoBaseConfig, GramoBaseEvent, Lease, Migration, RealtimeManager, UploadOptions, createClient };
73
+ export { AuthConfig, Collection, CollectionConfig, FileRecord, GramoBase, GramoBaseAuth, GramoBaseConfig, GramoBaseEvent, Migration, RealtimeManager, SchemaLike, UploadOptions, createClient };
package/dist/index.js CHANGED
@@ -270,9 +270,10 @@ var INDEX_TAG = "__GRAMOBASE_INDEX__";
270
270
  var DOC_TAG = "__GRAMOBASE_DOC__";
271
271
  var MAX_MSG_BYTES = 4e3;
272
272
  var TelegramStorage = class {
273
- constructor(pool, defaultChannelId, encryptionKey, debug = false) {
273
+ constructor(pool, defaultChannelId, registry, encryptionKey, debug = false) {
274
274
  this.pool = pool;
275
275
  this.defaultChannelId = defaultChannelId;
276
+ this.registry = registry;
276
277
  this.debug = debug;
277
278
  if (encryptionKey) {
278
279
  this.encryptionKey = createHash("sha256").update(encryptionKey).digest();
@@ -280,20 +281,52 @@ var TelegramStorage = class {
280
281
  }
281
282
  pool;
282
283
  defaultChannelId;
284
+ registry;
283
285
  debug;
284
286
  encryptionKey = null;
285
287
  // collection → pinned index message ID
286
288
  indexMsgIds = /* @__PURE__ */ new Map();
287
289
  // ─── Index management ─────────────────────────────────────────────────────
290
+ async readRawMessageText(msgId, channel) {
291
+ try {
292
+ const msg = await this.pool.execute(
293
+ (bot) => bot.forwardMessage(channel, channel, msgId)
294
+ );
295
+ if (!msg?.text) return null;
296
+ let text = msg.text;
297
+ if (this.encryptionKey && text.startsWith("ENC:")) {
298
+ text = this.decrypt(text);
299
+ }
300
+ return text;
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
288
305
  async loadIndex(collection, channelId) {
289
306
  const channel = channelId ?? this.defaultChannelId;
307
+ try {
308
+ const msgId = await this.registry.getCollectionIndexMsgId(collection);
309
+ if (msgId) {
310
+ const text = await this.readRawMessageText(msgId, channel);
311
+ if (text && text.startsWith(INDEX_TAG)) {
312
+ const json = text.replace(INDEX_TAG + "\n", "");
313
+ const parsed = JSON.parse(json);
314
+ this.indexMsgIds.set(collection, msgId);
315
+ return parsed;
316
+ }
317
+ }
318
+ } catch {
319
+ }
290
320
  try {
291
321
  const chat = await this.pool.execute((bot) => bot.getChat(channel));
292
322
  if (chat.pinned_message?.text?.startsWith(INDEX_TAG)) {
293
323
  const json = chat.pinned_message.text.replace(INDEX_TAG + "\n", "");
294
324
  const parsed = JSON.parse(json);
295
- this.indexMsgIds.set(collection, chat.pinned_message.message_id);
296
- return parsed;
325
+ if (parsed.collection === collection) {
326
+ this.indexMsgIds.set(collection, chat.pinned_message.message_id);
327
+ await this.registry.setCollectionIndexMsgId(collection, chat.pinned_message.message_id);
328
+ return parsed;
329
+ }
297
330
  }
298
331
  } catch {
299
332
  }
@@ -308,7 +341,7 @@ var TelegramStorage = class {
308
341
  const channel = channelId ?? this.defaultChannelId;
309
342
  const text = `${INDEX_TAG}
310
343
  ${JSON.stringify(index)}`;
311
- const existingMsgId = this.indexMsgIds.get(index.collection);
344
+ const existingMsgId = this.indexMsgIds.get(index.collection) || await this.registry.getCollectionIndexMsgId(index.collection);
312
345
  if (existingMsgId) {
313
346
  try {
314
347
  await this.pool.execute(
@@ -324,10 +357,9 @@ ${JSON.stringify(index)}`;
324
357
  const msg = await this.pool.execute(
325
358
  (bot) => bot.sendMessage(channel, text, { disable_notification: true })
326
359
  );
327
- this.indexMsgIds.set(index.collection, msg.message_id);
328
- await this.pool.execute(
329
- (bot) => bot.pinChatMessage(channel, msg.message_id, { disable_notification: true })
330
- );
360
+ const newMsgId = msg.message_id;
361
+ this.indexMsgIds.set(index.collection, newMsgId);
362
+ await this.registry.setCollectionIndexMsgId(index.collection, newMsgId);
331
363
  }
332
364
  // ─── Document CRUD ────────────────────────────────────────────────────────
333
365
  async writeDocument(doc, channelId) {
@@ -347,10 +379,9 @@ ${JSON.stringify(index)}`;
347
379
  async readDocument(msgId, channelId) {
348
380
  const channel = channelId ?? this.defaultChannelId;
349
381
  try {
350
- const msgs = await this.pool.execute(
351
- (bot) => bot.forwardMessages(channel, channel, [msgId])
382
+ const msg = await this.pool.execute(
383
+ (bot) => bot.forwardMessage(channel, channel, msgId)
352
384
  );
353
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
354
385
  if (!msg?.text) return null;
355
386
  let text = msg.text;
356
387
  if (this.encryptionKey && text.startsWith("ENC:")) {
@@ -361,6 +392,7 @@ ${JSON.stringify(index)}`;
361
392
  }
362
393
  const parsed = JSON.parse(text);
363
394
  delete parsed[DOC_TAG];
395
+ parsed._msgId = msgId;
364
396
  return parsed;
365
397
  } catch {
366
398
  return null;
@@ -399,10 +431,9 @@ ${JSON.stringify(index)}`;
399
431
  const msgIds = JSON.parse(headerText.replace("CHUNK:", ""));
400
432
  const parts = [];
401
433
  for (const id of msgIds) {
402
- const msgs = await this.pool.execute(
403
- (bot) => bot.forwardMessages(channel, channel, [id])
434
+ const msg = await this.pool.execute(
435
+ (bot) => bot.forwardMessage(channel, channel, id)
404
436
  );
405
- const msg = Array.isArray(msgs) ? msgs[0] : msgs;
406
437
  if (msg?.text) parts.push(msg.text);
407
438
  }
408
439
  return parts.join("");
@@ -603,7 +634,8 @@ var Registry = class {
603
634
  this.state = {
604
635
  activeLease: null,
605
636
  instanceId: this.instanceId,
606
- registryMsgId: null
637
+ registryMsgId: null,
638
+ indexes: {}
607
639
  };
608
640
  }
609
641
  pool;
@@ -611,14 +643,33 @@ var Registry = class {
611
643
  debug;
612
644
  state;
613
645
  instanceId;
614
- async acquireWriteLease() {
646
+ async acquireWriteLease(options = { wait: true }) {
615
647
  const existing = await this.readRegistryMessage();
616
648
  if (existing?.activeLease) {
617
649
  const lease2 = existing.activeLease;
618
650
  if (lease2.instanceId !== this.instanceId && Date.now() < lease2.expiresAt) {
619
- throw new Error(
620
- `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(lease2.expiresAt).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
621
- );
651
+ if (!options.wait) {
652
+ throw new Error(
653
+ `[gramobase Registry] Another instance (${lease2.instanceId}) holds the write lease until ${new Date(
654
+ lease2.expiresAt
655
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
656
+ );
657
+ }
658
+ const waitMs = lease2.expiresAt - Date.now() + 250;
659
+ if (this.debug) {
660
+ console.log(
661
+ `[Registry] Lease held by ${lease2.instanceId}, waiting ${waitMs}ms for it to expire...`
662
+ );
663
+ }
664
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
665
+ const recheckState = await this.readRegistryMessage();
666
+ if (recheckState?.activeLease && recheckState.activeLease.instanceId !== this.instanceId && Date.now() < recheckState.activeLease.expiresAt) {
667
+ throw new Error(
668
+ `[gramobase Registry] Another instance (${recheckState.activeLease.instanceId}) holds the write lease until ${new Date(
669
+ recheckState.activeLease.expiresAt
670
+ ).toISOString()}. Use Registry.forceRelease() to break a stale lease.`
671
+ );
672
+ }
622
673
  }
623
674
  }
624
675
  const lease = {
@@ -670,15 +721,26 @@ var Registry = class {
670
721
  if (chat.pinned_message?.text?.startsWith(REGISTRY_TAG)) {
671
722
  this.state.registryMsgId = chat.pinned_message.message_id;
672
723
  const json = chat.pinned_message.text.replace(REGISTRY_TAG + "\n", "");
673
- return JSON.parse(json);
724
+ const parsed = JSON.parse(json);
725
+ this.state.indexes = parsed.indexes || {};
726
+ return parsed;
674
727
  }
675
728
  } catch {
676
729
  }
677
730
  return null;
678
731
  }
679
732
  async writeRegistryMessage(data) {
733
+ let leasePayload = null;
734
+ if (data.activeLease) {
735
+ const { heartbeatInterval, ...rest } = data.activeLease;
736
+ leasePayload = rest;
737
+ }
738
+ const payload = {
739
+ activeLease: leasePayload,
740
+ indexes: data.indexes || this.state.indexes || {}
741
+ };
680
742
  const text = `${REGISTRY_TAG}
681
- ${JSON.stringify(data, null, 0)}`;
743
+ ${JSON.stringify(payload, null, 0)}`;
682
744
  if (this.state.registryMsgId) {
683
745
  try {
684
746
  await this.pool.execute(
@@ -701,6 +763,22 @@ ${JSON.stringify(data, null, 0)}`;
701
763
  })
702
764
  );
703
765
  }
766
+ async getCollectionIndexMsgId(collection) {
767
+ if (!this.state.registryMsgId) {
768
+ await this.readRegistryMessage();
769
+ }
770
+ return this.state.indexes[collection] || null;
771
+ }
772
+ async setCollectionIndexMsgId(collection, msgId) {
773
+ if (!this.state.registryMsgId) {
774
+ await this.readRegistryMessage();
775
+ }
776
+ this.state.indexes[collection] = msgId;
777
+ await this.writeRegistryMessage({
778
+ activeLease: this.state.activeLease,
779
+ indexes: this.state.indexes
780
+ });
781
+ }
704
782
  getInstanceId() {
705
783
  return this.instanceId;
706
784
  }
@@ -1456,9 +1534,15 @@ var GramoBase = class {
1456
1534
  const tokens = Array.isArray(config.botToken) ? config.botToken : [config.botToken];
1457
1535
  this.pool = new BotWorkerPool(tokens, config.concurrency ?? 25, config.debug ?? false);
1458
1536
  this.cache = new HotCache(config.cacheMaxBytes, config.cacheTtlMs);
1537
+ this.registry = new Registry(
1538
+ this.pool,
1539
+ config.indexChannelId ?? config.channelId,
1540
+ config.debug ?? false
1541
+ );
1459
1542
  this.storage = new TelegramStorage(
1460
1543
  this.pool,
1461
1544
  config.channelId,
1545
+ this.registry,
1462
1546
  config.encryptionKey,
1463
1547
  config.debug ?? false
1464
1548
  );
@@ -1467,11 +1551,6 @@ var GramoBase = class {
1467
1551
  config.walChannelId ?? config.channelId,
1468
1552
  config.debug ?? false
1469
1553
  );
1470
- this.registry = new Registry(
1471
- this.pool,
1472
- config.indexChannelId ?? config.channelId,
1473
- config.debug ?? false
1474
- );
1475
1554
  this.realtime = new RealtimeManager(
1476
1555
  this.pool,
1477
1556
  config.webhookUrl,
@@ -1602,7 +1681,20 @@ var GramoBase = class {
1602
1681
  }
1603
1682
  }
1604
1683
  };
1684
+ var globalForGramo = globalThis;
1605
1685
  function createClient(config) {
1686
+ if (config.global) {
1687
+ if (!globalForGramo.__gramobase_clients__) {
1688
+ globalForGramo.__gramobase_clients__ = /* @__PURE__ */ new Map();
1689
+ }
1690
+ const cacheKey = Array.isArray(config.botToken) ? `${config.channelId}:${config.botToken.join(",")}` : `${config.channelId}:${config.botToken}`;
1691
+ if (globalForGramo.__gramobase_clients__.has(cacheKey)) {
1692
+ return globalForGramo.__gramobase_clients__.get(cacheKey);
1693
+ }
1694
+ const client = new GramoBase(config);
1695
+ globalForGramo.__gramobase_clients__.set(cacheKey, client);
1696
+ return client;
1697
+ }
1606
1698
  return new GramoBase(config);
1607
1699
  }
1608
1700