@routstr/sdk 0.3.8 → 0.3.9

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.
@@ -32,6 +32,9 @@ var NoProvidersAvailableError = class extends Error {
32
32
  this.name = "NoProvidersAvailableError";
33
33
  }
34
34
  };
35
+ function isBunRuntime() {
36
+ return typeof Bun !== "undefined";
37
+ }
35
38
  var ModelManager = class _ModelManager {
36
39
  constructor(adapter, config = {}) {
37
40
  this.adapter = adapter;
@@ -41,6 +44,7 @@ var ModelManager = class _ModelManager {
41
44
  this.excludeProviderUrls = config.excludeProviderUrls || [];
42
45
  this.routstrPubkey = config.routstrPubkey || "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8";
43
46
  this.logger = (config.logger ?? consoleLogger).child("ModelManager");
47
+ this.eventStoreDbPath = config.eventStoreDbPath;
44
48
  }
45
49
  adapter;
46
50
  cacheTTL;
@@ -50,6 +54,11 @@ var ModelManager = class _ModelManager {
50
54
  routstrPubkey;
51
55
  logger;
52
56
  providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
57
+ /** Persistent event store for relay-fetched events (null if not configured/initialized) */
58
+ eventStore = null;
59
+ eventStoreDb = null;
60
+ eventStoreInitPromise = null;
61
+ eventStoreDbPath;
53
62
  /**
54
63
  * Get the list of bootstrapped provider base URLs
55
64
  * @returns Array of provider base URLs
@@ -57,6 +66,104 @@ var ModelManager = class _ModelManager {
57
66
  getBaseUrls() {
58
67
  return this.adapter.getBaseUrlsList();
59
68
  }
69
+ /**
70
+ * Lazily initialize the persistent event store.
71
+ * Returns null if no eventStoreDbPath was provided.
72
+ */
73
+ async ensureEventStore() {
74
+ if (!this.eventStoreDbPath) return null;
75
+ if (this.eventStore) return this.eventStore;
76
+ if (!this.eventStoreInitPromise) {
77
+ this.eventStoreInitPromise = (async () => {
78
+ try {
79
+ const db = await this.createPersistentEventDatabase();
80
+ this.eventStoreDb = db;
81
+ this.eventStore = new EventStore({ database: db });
82
+ this.initializeEventStoreMetadata();
83
+ this.logger.log(
84
+ `Persistent event store initialized at ${this.eventStoreDbPath}`
85
+ );
86
+ return this.eventStore;
87
+ } catch (error) {
88
+ this.eventStoreInitPromise = null;
89
+ throw new Error(
90
+ `applesauce-sqlite with a supported SQLite driver is required for persistent Nostr event storage. Bun uses bun:sqlite; Node.js uses better-sqlite3. Install optional dependencies or omit eventStoreDbPath. (${error})`
91
+ );
92
+ }
93
+ })();
94
+ }
95
+ return this.eventStoreInitPromise;
96
+ }
97
+ /**
98
+ * Get the persistent event store, initializing it if configured.
99
+ * Returns null if no eventStoreDbPath was provided.
100
+ */
101
+ async getEventStore() {
102
+ return this.ensureEventStore();
103
+ }
104
+ async createPersistentEventDatabase() {
105
+ if (isBunRuntime()) {
106
+ const { BunSqliteEventDatabase } = await import('applesauce-sqlite/bun');
107
+ return new BunSqliteEventDatabase(
108
+ this.eventStoreDbPath
109
+ );
110
+ }
111
+ const { BetterSqlite3EventDatabase } = await import('applesauce-sqlite/better-sqlite3');
112
+ return new BetterSqlite3EventDatabase(
113
+ this.eventStoreDbPath
114
+ );
115
+ }
116
+ /** Close the persistent event store database handle, if configured. */
117
+ closeEventStore() {
118
+ this.eventStoreDb?.close?.();
119
+ this.eventStore = null;
120
+ this.eventStoreDb = null;
121
+ this.eventStoreInitPromise = null;
122
+ }
123
+ initializeEventStoreMetadata() {
124
+ this.eventStoreDb?.db?.exec(
125
+ `CREATE TABLE IF NOT EXISTS routstr_event_cache_metadata (
126
+ event_id TEXT PRIMARY KEY,
127
+ fetched_at INTEGER NOT NULL
128
+ )`
129
+ );
130
+ }
131
+ markEventFetched(event, fetchedAt = Date.now()) {
132
+ const db = this.eventStoreDb?.db;
133
+ if (!db) return;
134
+ db.prepare(
135
+ `INSERT INTO routstr_event_cache_metadata (event_id, fetched_at)
136
+ VALUES (?, ?)
137
+ ON CONFLICT(event_id) DO UPDATE SET fetched_at = excluded.fetched_at`
138
+ ).run?.(event.id, fetchedAt);
139
+ }
140
+ getEventFetchedAt(event) {
141
+ const db = this.eventStoreDb?.db;
142
+ if (!db) return void 0;
143
+ const row = db.prepare(
144
+ `SELECT fetched_at FROM routstr_event_cache_metadata WHERE event_id = ?`
145
+ ).get?.(event.id);
146
+ return typeof row?.fetched_at === "number" ? row.fetched_at : void 0;
147
+ }
148
+ /**
149
+ * Check the persistent event store for fresh cached events.
150
+ * Returns events from SQLite if they were fetched within `maxAge`, otherwise
151
+ * returns empty array (caller should hit relays). Events without local fetch
152
+ * metadata fall back to Nostr created_at for backwards compatibility.
153
+ */
154
+ async getCachedNostrEvents(filter, maxAge, forceRefresh = false) {
155
+ const eventStore = await this.ensureEventStore();
156
+ if (forceRefresh) return [];
157
+ if (!eventStore) return [];
158
+ const timeline = eventStore.getTimeline(filter);
159
+ if (timeline.length === 0) return [];
160
+ const cutoff = Date.now() - maxAge;
161
+ const freshest = Math.max(
162
+ ...timeline.map((e) => this.getEventFetchedAt(e) ?? e.created_at * 1e3)
163
+ );
164
+ if (freshest < cutoff) return [];
165
+ return timeline;
166
+ }
60
167
  static async init(adapter, config = {}, options = {}) {
61
168
  const manager = new _ModelManager(adapter, config);
62
169
  const torMode = options.torMode ?? false;
@@ -85,19 +192,31 @@ var ModelManager = class _ModelManager {
85
192
  torMode
86
193
  );
87
194
  await this.fetchRoutstr21Models(forceRefresh);
88
- await this.syncReviewedProvidersFromNostr(filteredCachedUrls);
195
+ await this.syncReviewedProvidersFromNostr(
196
+ filteredCachedUrls,
197
+ this.providerNodePubkeysByUrl,
198
+ forceRefresh
199
+ );
89
200
  return filteredCachedUrls;
90
201
  }
91
202
  }
92
203
  }
93
204
  try {
94
- const nostrProviders = await this.bootstrapFromNostr(38421, torMode);
205
+ const nostrProviders = await this.bootstrapFromNostr(
206
+ 38421,
207
+ torMode,
208
+ forceRefresh
209
+ );
95
210
  if (nostrProviders.length > 0) {
96
211
  const filtered = this.filterBaseUrlsForTor(nostrProviders, torMode);
97
212
  this.adapter.setBaseUrlsList(filtered);
98
213
  this.adapter.setBaseUrlsLastUpdate(Date.now());
99
214
  await this.fetchRoutstr21Models(forceRefresh);
100
- await this.syncReviewedProvidersFromNostr(filtered);
215
+ await this.syncReviewedProvidersFromNostr(
216
+ filtered,
217
+ this.providerNodePubkeysByUrl,
218
+ forceRefresh
219
+ );
101
220
  return filtered;
102
221
  }
103
222
  } catch (e) {
@@ -106,42 +225,52 @@ var ModelManager = class _ModelManager {
106
225
  return this.bootstrapFromHttp(torMode, forceRefresh);
107
226
  }
108
227
  /**
109
- * Bootstrap providers from Nostr network (kind 30421)
228
+ * Bootstrap providers from Nostr network (kind 38421)
110
229
  * @param kind The Nostr kind to fetch
111
230
  * @param torMode Whether running in Tor context
112
231
  * @returns Array of provider base URLs
113
232
  */
114
- async bootstrapFromNostr(kind, torMode) {
233
+ async bootstrapFromNostr(kind, torMode, forceRefresh = false) {
115
234
  const DEFAULT_RELAYS = [
116
235
  "wss://relay.primal.net",
117
236
  "wss://nos.lol",
118
237
  "wss://relay.damus.io"
119
238
  ];
120
- const pool = new RelayPool();
121
- const localEventStore = new EventStore();
122
- const timeoutMs = 5e3;
123
- await new Promise((resolve) => {
124
- pool.req(DEFAULT_RELAYS, {
125
- kinds: [kind],
126
- limit: 100
127
- }).pipe(
128
- onlyEvents(),
129
- tap((event) => {
130
- localEventStore.add(event);
131
- })
132
- ).subscribe({
133
- complete: () => {
239
+ const cached = await this.getCachedNostrEvents(
240
+ { kinds: [kind] },
241
+ this.cacheTTL,
242
+ forceRefresh
243
+ );
244
+ let sessionEvents = cached;
245
+ if (cached.length === 0) {
246
+ const pool = new RelayPool();
247
+ const timeoutMs = 5e3;
248
+ await new Promise((resolve) => {
249
+ pool.req(DEFAULT_RELAYS, {
250
+ kinds: [kind],
251
+ limit: 100
252
+ }).pipe(
253
+ onlyEvents(),
254
+ tap((event) => {
255
+ sessionEvents.push(event);
256
+ this.eventStore?.add(event);
257
+ this.markEventFetched(event);
258
+ })
259
+ ).subscribe({
260
+ complete: () => {
261
+ resolve();
262
+ }
263
+ });
264
+ setTimeout(() => {
134
265
  resolve();
135
- }
266
+ }, timeoutMs);
136
267
  });
137
- setTimeout(() => {
138
- resolve();
139
- }, timeoutMs);
140
- });
141
- const timeline = localEventStore.getTimeline({ kinds: [kind] });
268
+ } else {
269
+ this.logger.log(`Using ${cached.length} cached kind ${kind} events from persistent store`);
270
+ }
142
271
  const bases = /* @__PURE__ */ new Set();
143
272
  this.providerNodePubkeysByUrl = /* @__PURE__ */ new Map();
144
- for (const event of timeline) {
273
+ for (const event of sessionEvents) {
145
274
  const eventUrls = [];
146
275
  for (const tag of event.tags) {
147
276
  if (tag[0] === "u" && typeof tag[1] === "string") {
@@ -249,7 +378,11 @@ var ModelManager = class _ModelManager {
249
378
  this.adapter.setBaseUrlsList(list);
250
379
  this.adapter.setBaseUrlsLastUpdate(Date.now());
251
380
  await this.fetchRoutstr21Models(forceRefresh);
252
- await this.syncReviewedProvidersFromNostr(list);
381
+ await this.syncReviewedProvidersFromNostr(
382
+ list,
383
+ this.providerNodePubkeysByUrl,
384
+ forceRefresh
385
+ );
253
386
  }
254
387
  return list;
255
388
  } catch (e) {
@@ -268,7 +401,7 @@ var ModelManager = class _ModelManager {
268
401
  * @param baseUrls Current provider base URLs to evaluate
269
402
  * @returns Array of provider base URLs disabled by the review set
270
403
  */
271
- async syncReviewedProvidersFromNostr(baseUrls = this.adapter.getBaseUrlsList(), providerNodes = this.providerNodePubkeysByUrl) {
404
+ async syncReviewedProvidersFromNostr(baseUrls = this.adapter.getBaseUrlsList(), providerNodes = this.providerNodePubkeysByUrl, forceRefresh = false) {
272
405
  if (baseUrls.length === 0) return [];
273
406
  if (!this.adapter.setDisabledProviders) {
274
407
  this.logger.warn(
@@ -276,30 +409,43 @@ var ModelManager = class _ModelManager {
276
409
  );
277
410
  return [];
278
411
  }
279
- const LGTM_RELAYS = [
280
- "wss://relay.primal.net",
281
- "wss://nos.lol",
282
- "wss://relay.damus.io",
283
- "wss://relay.routstr.com"
284
- ];
285
412
  const reviewedNodePubkeys = /* @__PURE__ */ new Set();
286
413
  {
287
- const pool = new RelayPool();
288
- const store = new EventStore();
289
- const timeoutMs = 5e3;
290
- await new Promise((resolve) => {
291
- pool.req(LGTM_RELAYS, {
292
- kinds: [38425],
293
- "#t": ["lgtm"],
294
- limit: 500,
295
- authors: [this.routstrPubkey]
296
- }).pipe(
297
- onlyEvents(),
298
- tap((event) => store.add(event))
299
- ).subscribe({ complete: () => resolve() });
300
- setTimeout(() => resolve(), timeoutMs);
301
- });
302
- for (const event of store.getTimeline({ kinds: [38425] })) {
414
+ const cached = await this.getCachedNostrEvents(
415
+ { kinds: [38425], "#t": ["lgtm"], authors: [this.routstrPubkey] },
416
+ this.cacheTTL,
417
+ forceRefresh
418
+ );
419
+ let sessionEvents = cached;
420
+ if (cached.length === 0) {
421
+ const LGTM_RELAYS = [
422
+ "wss://relay.primal.net",
423
+ "wss://nos.lol",
424
+ "wss://relay.damus.io",
425
+ "wss://relay.routstr.com"
426
+ ];
427
+ const pool = new RelayPool();
428
+ const timeoutMs = 5e3;
429
+ await new Promise((resolve) => {
430
+ pool.req(LGTM_RELAYS, {
431
+ kinds: [38425],
432
+ "#t": ["lgtm"],
433
+ limit: 500,
434
+ authors: [this.routstrPubkey]
435
+ }).pipe(
436
+ onlyEvents(),
437
+ tap((event) => {
438
+ sessionEvents.push(event);
439
+ this.eventStore?.add(event);
440
+ this.markEventFetched(event);
441
+ })
442
+ ).subscribe({ complete: () => resolve() });
443
+ setTimeout(() => resolve(), timeoutMs);
444
+ });
445
+ } else {
446
+ this.logger.log(`Using ${cached.length} cached kind 38425 events from persistent store`);
447
+ }
448
+ for (const event of sessionEvents) {
303
449
  const hasLgtmTag = event.tags.some(
304
450
  (tag) => tag[0] === "t" && tag[1]?.toLowerCase() === "lgtm"
305
451
  );
@@ -408,7 +554,7 @@ var ModelManager = class _ModelManager {
408
554
  if (this.isProviderDownError(error)) {
409
555
  this.logger.warn(`Provider ${base} is down right now.`);
410
556
  } else {
411
- this.logger.warn(`Failed to fetch models from ${base}:`, error);
557
+ this.logger.warn(`Provider ${base} unreachable: ${error.message}`);
412
558
  }
413
559
  this.adapter.setProviderLastUpdate(base, Date.now());
414
560
  return { success: false, base };
@@ -528,34 +674,44 @@ var ModelManager = class _ModelManager {
528
674
  "wss://nos.lol",
529
675
  "wss://relay.routstr.com"
530
676
  ];
531
- const pool = new RelayPool();
532
- const localEventStore = new EventStore();
533
- const timeoutMs = 5e3;
534
- await new Promise((resolve) => {
535
- pool.req(DEFAULT_RELAYS, {
536
- kinds: [38423],
537
- "#d": ["routstr-21-models"],
538
- limit: 1,
539
- authors: [this.routstrPubkey]
540
- }).pipe(
541
- onlyEvents(),
542
- tap((event2) => {
543
- localEventStore.add(event2);
544
- })
545
- ).subscribe({
546
- complete: () => {
677
+ const cached = await this.getCachedNostrEvents(
678
+ { kinds: [38423], "#d": ["routstr-21-models"], authors: [this.routstrPubkey] },
679
+ this.cacheTTL,
680
+ forceRefresh
681
+ );
682
+ let sessionEvents = cached;
683
+ if (cached.length === 0) {
684
+ const pool = new RelayPool();
685
+ const timeoutMs = 5e3;
686
+ await new Promise((resolve) => {
687
+ pool.req(DEFAULT_RELAYS, {
688
+ kinds: [38423],
689
+ "#d": ["routstr-21-models"],
690
+ limit: 1,
691
+ authors: [this.routstrPubkey]
692
+ }).pipe(
693
+ onlyEvents(),
694
+ tap((event2) => {
695
+ sessionEvents.push(event2);
696
+ this.eventStore?.add(event2);
697
+ this.markEventFetched(event2);
698
+ })
699
+ ).subscribe({
700
+ complete: () => {
701
+ resolve();
702
+ }
703
+ });
704
+ setTimeout(() => {
547
705
  resolve();
548
- }
706
+ }, timeoutMs);
549
707
  });
550
- setTimeout(() => {
551
- resolve();
552
- }, timeoutMs);
553
- });
554
- const timeline = localEventStore.getTimeline({ kinds: [38423] });
555
- if (timeline.length === 0) {
708
+ } else {
709
+ this.logger.log(`Using ${cached.length} cached kind 38423 events from persistent store`);
710
+ }
711
+ if (sessionEvents.length === 0) {
556
712
  return cachedModels.length > 0 ? cachedModels : [];
557
713
  }
558
- const event = timeline[0];
714
+ const event = sessionEvents[0];
559
715
  try {
560
716
  const content = JSON.parse(event.content);
561
717
  const models = Array.isArray(content?.models) ? content.models : [];