@poolzin/pool-bot 2026.2.20 → 2026.2.21

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.
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Session Manager
3
+ *
4
+ * Manages sticky session bindings between conversation sessions and API tokens.
5
+ * Ensures conversation continuity by routing requests to the same key when possible.
6
+ *
7
+ * @module provider/session-binding
8
+ */
9
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
10
+ export var SessionManager;
11
+ (function (SessionManager) {
12
+ const log = createSubsystemLogger("provider/session-binding");
13
+ // ============================================================================
14
+ // Internal State
15
+ // ============================================================================
16
+ /** Session bindings: sessionID:providerID -> Binding */
17
+ const bindings = new Map();
18
+ /** Configuration */
19
+ let config = {
20
+ defaultTTLMs: 60_000, // 60 seconds
21
+ maxBindings: 10_000,
22
+ extendOnAccess: true,
23
+ };
24
+ /** Cleanup interval handle */
25
+ let cleanupInterval = null;
26
+ // ============================================================================
27
+ // Helper Functions
28
+ // ============================================================================
29
+ /**
30
+ * Creates a composite key for the bindings map.
31
+ */
32
+ function getBindingKey(sessionID, providerID) {
33
+ return `${sessionID}:${providerID}`;
34
+ }
35
+ /**
36
+ * Checks if a binding is expired.
37
+ */
38
+ function isExpired(binding) {
39
+ return Date.now() > binding.expiresAt;
40
+ }
41
+ /**
42
+ * Evicts oldest bindings if over capacity.
43
+ */
44
+ function evictIfNeeded() {
45
+ if (bindings.size <= config.maxBindings)
46
+ return;
47
+ // Sort by last accessed time and remove oldest
48
+ const sorted = Array.from(bindings.entries()).sort((a, b) => a[1].lastAccessedAt - b[1].lastAccessedAt);
49
+ const toRemove = sorted.slice(0, bindings.size - config.maxBindings);
50
+ for (const [key] of toRemove) {
51
+ bindings.delete(key);
52
+ }
53
+ log.info("evicted", { count: toRemove.length });
54
+ }
55
+ // ============================================================================
56
+ // Public API
57
+ // ============================================================================
58
+ /**
59
+ * Configures the session manager.
60
+ *
61
+ * @param newConfig - Partial configuration to merge
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * SessionManager.configure({
66
+ * defaultTTLMs: 120_000, // 2 minutes
67
+ * extendOnAccess: true
68
+ * })
69
+ * ```
70
+ */
71
+ function configure(newConfig) {
72
+ config = { ...config, ...newConfig };
73
+ log.info("configured", { ...config });
74
+ }
75
+ SessionManager.configure = configure;
76
+ /**
77
+ * Gets the current configuration.
78
+ */
79
+ function getConfig() {
80
+ return { ...config };
81
+ }
82
+ SessionManager.getConfig = getConfig;
83
+ /**
84
+ * Creates or updates a session binding.
85
+ *
86
+ * @param sessionID - Session identifier
87
+ * @param providerID - Provider identifier
88
+ * @param tokenID - Token identifier to bind to
89
+ * @param ttlMs - Custom TTL (uses default if not specified)
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * SessionManager.bind("session-123", "anthropic", "key-1")
94
+ * ```
95
+ */
96
+ function bind(sessionID, providerID, tokenID, ttlMs) {
97
+ const key = getBindingKey(sessionID, providerID);
98
+ const now = Date.now();
99
+ const ttl = ttlMs ?? config.defaultTTLMs;
100
+ const existing = bindings.get(key);
101
+ if (existing && existing.tokenID === tokenID) {
102
+ // Update existing binding
103
+ existing.lastAccessedAt = now;
104
+ existing.expiresAt = now + ttl;
105
+ existing.requestCount++;
106
+ return existing;
107
+ }
108
+ // Create new binding
109
+ const binding = {
110
+ sessionID,
111
+ providerID,
112
+ tokenID,
113
+ createdAt: now,
114
+ lastAccessedAt: now,
115
+ expiresAt: now + ttl,
116
+ requestCount: 1,
117
+ };
118
+ bindings.set(key, binding);
119
+ evictIfNeeded();
120
+ log.info("bound", { sessionID, providerID, tokenID, ttlMs: ttl });
121
+ return binding;
122
+ }
123
+ SessionManager.bind = bind;
124
+ /**
125
+ * Looks up a session binding.
126
+ *
127
+ * @param sessionID - Session identifier
128
+ * @param providerID - Provider identifier
129
+ * @returns Lookup result with binding if found and valid
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const result = SessionManager.lookup("session-123", "anthropic")
134
+ * if (result.found && !result.expired) {
135
+ * console.log("Using token:", result.binding.tokenID)
136
+ * }
137
+ * ```
138
+ */
139
+ function lookup(sessionID, providerID) {
140
+ const key = getBindingKey(sessionID, providerID);
141
+ const binding = bindings.get(key);
142
+ if (!binding) {
143
+ return { found: false, expired: false };
144
+ }
145
+ if (isExpired(binding)) {
146
+ bindings.delete(key);
147
+ return { found: true, expired: true };
148
+ }
149
+ // Extend TTL on access if configured
150
+ if (config.extendOnAccess) {
151
+ binding.lastAccessedAt = Date.now();
152
+ binding.expiresAt = binding.lastAccessedAt + config.defaultTTLMs;
153
+ }
154
+ return { binding, found: true, expired: false };
155
+ }
156
+ SessionManager.lookup = lookup;
157
+ /**
158
+ * Gets the bound token ID for a session, if any.
159
+ *
160
+ * @param sessionID - Session identifier
161
+ * @param providerID - Provider identifier
162
+ * @returns Token ID or undefined if not bound
163
+ */
164
+ function getBoundToken(sessionID, providerID) {
165
+ const result = lookup(sessionID, providerID);
166
+ return result.binding?.tokenID;
167
+ }
168
+ SessionManager.getBoundToken = getBoundToken;
169
+ /**
170
+ * Removes a session binding.
171
+ *
172
+ * @param sessionID - Session identifier
173
+ * @param providerID - Provider identifier
174
+ * @returns Whether a binding was found and removed
175
+ */
176
+ function unbind(sessionID, providerID) {
177
+ const key = getBindingKey(sessionID, providerID);
178
+ const removed = bindings.delete(key);
179
+ if (removed) {
180
+ log.info("unbound", { sessionID, providerID });
181
+ }
182
+ return removed;
183
+ }
184
+ SessionManager.unbind = unbind;
185
+ /**
186
+ * Removes all bindings for a session.
187
+ *
188
+ * @param sessionID - Session identifier
189
+ * @returns Number of bindings removed
190
+ */
191
+ function unbindSession(sessionID) {
192
+ let count = 0;
193
+ const prefix = `${sessionID}:`;
194
+ for (const key of bindings.keys()) {
195
+ if (key.startsWith(prefix)) {
196
+ bindings.delete(key);
197
+ count++;
198
+ }
199
+ }
200
+ if (count > 0) {
201
+ log.info("session-unbound", { sessionID, count });
202
+ }
203
+ return count;
204
+ }
205
+ SessionManager.unbindSession = unbindSession;
206
+ /**
207
+ * Removes all bindings for a provider.
208
+ *
209
+ * @param providerID - Provider identifier
210
+ * @returns Number of bindings removed
211
+ */
212
+ function unbindProvider(providerID) {
213
+ let count = 0;
214
+ const suffix = `:${providerID}`;
215
+ for (const key of bindings.keys()) {
216
+ if (key.endsWith(suffix)) {
217
+ bindings.delete(key);
218
+ count++;
219
+ }
220
+ }
221
+ if (count > 0) {
222
+ log.info("provider-unbound", { providerID, count });
223
+ }
224
+ return count;
225
+ }
226
+ SessionManager.unbindProvider = unbindProvider;
227
+ /**
228
+ * Removes all bindings for a specific token.
229
+ *
230
+ * @param providerID - Provider identifier
231
+ * @param tokenID - Token identifier
232
+ * @returns Number of bindings removed
233
+ */
234
+ function unbindToken(providerID, tokenID) {
235
+ let count = 0;
236
+ for (const [key, binding] of bindings.entries()) {
237
+ if (binding.providerID === providerID && binding.tokenID === tokenID) {
238
+ bindings.delete(key);
239
+ count++;
240
+ }
241
+ }
242
+ if (count > 0) {
243
+ log.info("token-unbound", { providerID, tokenID, count });
244
+ }
245
+ return count;
246
+ }
247
+ SessionManager.unbindToken = unbindToken;
248
+ /**
249
+ * Gets all bindings for a session.
250
+ *
251
+ * @param sessionID - Session identifier
252
+ * @returns Array of bindings
253
+ */
254
+ function getSessionBindings(sessionID) {
255
+ const result = [];
256
+ const now = Date.now();
257
+ const prefix = `${sessionID}:`;
258
+ for (const [key, binding] of bindings.entries()) {
259
+ if (key.startsWith(prefix) && binding.expiresAt > now) {
260
+ result.push({ ...binding });
261
+ }
262
+ }
263
+ return result;
264
+ }
265
+ SessionManager.getSessionBindings = getSessionBindings;
266
+ /**
267
+ * Gets all bindings for a provider.
268
+ *
269
+ * @param providerID - Provider identifier
270
+ * @returns Array of bindings
271
+ */
272
+ function getProviderBindings(providerID) {
273
+ const result = [];
274
+ const now = Date.now();
275
+ for (const binding of bindings.values()) {
276
+ if (binding.providerID === providerID && binding.expiresAt > now) {
277
+ result.push({ ...binding });
278
+ }
279
+ }
280
+ return result;
281
+ }
282
+ SessionManager.getProviderBindings = getProviderBindings;
283
+ /**
284
+ * Gets summary statistics for session bindings.
285
+ */
286
+ function getSummary() {
287
+ const now = Date.now();
288
+ let activeCount = 0;
289
+ let expiredCount = 0;
290
+ let totalRequests = 0;
291
+ let totalAge = 0;
292
+ const byProvider = {};
293
+ for (const binding of bindings.values()) {
294
+ if (binding.expiresAt > now) {
295
+ activeCount++;
296
+ totalRequests += binding.requestCount;
297
+ totalAge += now - binding.createdAt;
298
+ byProvider[binding.providerID] = (byProvider[binding.providerID] ?? 0) + 1;
299
+ }
300
+ else {
301
+ expiredCount++;
302
+ }
303
+ }
304
+ return {
305
+ totalBindings: bindings.size,
306
+ activeBindings: activeCount,
307
+ expiredBindings: expiredCount,
308
+ byProvider,
309
+ avgRequestsPerBinding: activeCount > 0 ? totalRequests / activeCount : 0,
310
+ avgAgeMs: activeCount > 0 ? totalAge / activeCount : 0,
311
+ };
312
+ }
313
+ SessionManager.getSummary = getSummary;
314
+ /**
315
+ * Removes expired bindings.
316
+ *
317
+ * @returns Number of bindings removed
318
+ */
319
+ function cleanupExpired() {
320
+ const now = Date.now();
321
+ let removed = 0;
322
+ for (const [key, binding] of bindings.entries()) {
323
+ if (binding.expiresAt <= now) {
324
+ bindings.delete(key);
325
+ removed++;
326
+ }
327
+ }
328
+ if (removed > 0) {
329
+ log.info("cleanup", { removed });
330
+ }
331
+ return removed;
332
+ }
333
+ SessionManager.cleanupExpired = cleanupExpired;
334
+ /**
335
+ * Clears all bindings.
336
+ */
337
+ function clear() {
338
+ const count = bindings.size;
339
+ bindings.clear();
340
+ if (count > 0) {
341
+ log.info("cleared", { count });
342
+ }
343
+ }
344
+ SessionManager.clear = clear;
345
+ /**
346
+ * Starts the automatic cleanup interval.
347
+ *
348
+ * @param intervalMs - Cleanup interval in milliseconds (default: 30000)
349
+ */
350
+ function startCleanup(intervalMs = 30_000) {
351
+ if (cleanupInterval)
352
+ return;
353
+ cleanupInterval = setInterval(() => {
354
+ cleanupExpired();
355
+ }, intervalMs);
356
+ // Don't keep the process alive just for cleanup
357
+ if (typeof cleanupInterval.unref === "function") {
358
+ cleanupInterval.unref();
359
+ }
360
+ log.debug("cleanup-started", { intervalMs });
361
+ }
362
+ SessionManager.startCleanup = startCleanup;
363
+ /**
364
+ * Stops the automatic cleanup interval.
365
+ */
366
+ function stopCleanup() {
367
+ if (cleanupInterval) {
368
+ clearInterval(cleanupInterval);
369
+ cleanupInterval = null;
370
+ log.info("cleanup-stopped");
371
+ }
372
+ }
373
+ SessionManager.stopCleanup = stopCleanup;
374
+ // Start cleanup on module load
375
+ startCleanup();
376
+ })(SessionManager || (SessionManager = {}));