@logg/signals 0.1.5 → 0.3.0

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.mjs CHANGED
@@ -1,9 +1,6 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
1
+ import {
2
+ __require
3
+ } from "./chunk-Y6FXYEAI.mjs";
7
4
 
8
5
  // src/utils/helpers.ts
9
6
  function uuid() {
@@ -50,19 +47,23 @@ var EventQueue = class {
50
47
  this.getEventContext = getEventContext;
51
48
  }
52
49
  /**
53
- * Initialize queue by loading persisted events
50
+ * Initialize queue by loading persisted events.
51
+ * Returns the number of events loaded from storage so callers can decide
52
+ * whether to force an immediate flush without racing against new events.
54
53
  */
55
54
  async init() {
56
55
  try {
57
56
  const stored = await this.storage.getItem(STORAGE_KEY);
58
57
  if (stored) {
59
58
  const parsed = JSON.parse(stored);
60
- this.queue = Array.isArray(parsed) ? parsed : [];
59
+ const loaded = Array.isArray(parsed) ? parsed : [];
60
+ this.queue = loaded.concat(this.queue);
61
+ return loaded.length;
61
62
  }
62
63
  } catch (error) {
63
64
  console.warn("Failed to load persisted events:", error);
64
- this.queue = [];
65
65
  }
66
+ return 0;
66
67
  }
67
68
  /**
68
69
  * Add event to queue
@@ -326,7 +327,8 @@ var Signals = class {
326
327
  constructor(config) {
327
328
  this.flushTimer = null;
328
329
  this.isDestroyed = false;
329
- this.isFlushing = false;
330
+ this.flushingPromise = null;
331
+ this.errorListeners = /* @__PURE__ */ new Set();
330
332
  if (!config.apiKey) {
331
333
  throw new Error("Signals: apiKey is required");
332
334
  }
@@ -360,13 +362,13 @@ var Signals = class {
360
362
  * Initialize client
361
363
  */
362
364
  async init() {
363
- await this.queue.init();
365
+ const persistedCount = await this.queue.init();
364
366
  this.log("Signals initialized", {
365
367
  sessionId: this.config.sessionId,
366
368
  queueSize: this.queue.size()
367
369
  });
368
370
  this.startFlushTimer();
369
- if (!this.queue.isEmpty()) {
371
+ if (persistedCount > 0) {
370
372
  await this.flush();
371
373
  }
372
374
  }
@@ -384,18 +386,55 @@ var Signals = class {
384
386
  }
385
387
  }
386
388
  /**
387
- * Manually flush events
389
+ * Send one batch of up to `batchSize` events. If a flush is already
390
+ * in-flight, callers join that one rather than starting a new request — this
391
+ * preserves at-most-one-in-flight delivery without silently no-op'ing.
392
+ *
393
+ * Failures do not throw. They emit a `send_failed` error event and leave
394
+ * events in the queue for the next flush.
388
395
  */
389
396
  async flush() {
390
- if (this.isDestroyed || this.queue.isEmpty() || this.isFlushing) {
397
+ if (this.queue.isEmpty()) return;
398
+ if (this.flushingPromise) {
399
+ await this.flushingPromise.catch(() => {
400
+ });
391
401
  return;
392
402
  }
393
- this.isFlushing = true;
394
- const events = this.queue.getBatch(this.config.batchSize);
395
- if (events.length === 0) {
396
- this.isFlushing = false;
397
- return;
403
+ this.flushingPromise = this.doFlushOnce();
404
+ try {
405
+ await this.flushingPromise;
406
+ } finally {
407
+ this.flushingPromise = null;
398
408
  }
409
+ }
410
+ /**
411
+ * Drain the entire queue. Loops `flush()` until the queue is empty or sends
412
+ * keep failing without making progress. Use at process shutdown or anywhere
413
+ * a backfill / migration script needs every queued event to land.
414
+ *
415
+ * Returns the number of events still queued (and thus unsent) after the
416
+ * drain attempt completes. Zero means full success.
417
+ */
418
+ async flushAll() {
419
+ let consecutiveStalls = 0;
420
+ while (!this.queue.isEmpty() && consecutiveStalls < 3) {
421
+ const before = this.queue.size();
422
+ await this.flush();
423
+ if (this.queue.size() === before) {
424
+ consecutiveStalls++;
425
+ } else {
426
+ consecutiveStalls = 0;
427
+ }
428
+ }
429
+ return this.queue.size();
430
+ }
431
+ /**
432
+ * Send a single batch. Internal — call via flush() so the in-flight gate
433
+ * is honored.
434
+ */
435
+ async doFlushOnce() {
436
+ const events = this.queue.getBatch(this.config.batchSize);
437
+ if (events.length === 0) return;
399
438
  this.log(`Flushing ${events.length} events`);
400
439
  const batch = {
401
440
  api_key: this.config.apiKey,
@@ -410,8 +449,14 @@ var Signals = class {
410
449
  this.log("Batch sent successfully");
411
450
  } catch (error) {
412
451
  this.log("Failed to send batch", error);
413
- } finally {
414
- this.isFlushing = false;
452
+ this.emitError({
453
+ type: "send_failed",
454
+ message: `Failed to send batch after ${this.config.maxRetries} retries`,
455
+ error: error instanceof Error ? error : new Error(String(error)),
456
+ batchId: batch.batch_id,
457
+ eventCount: events.length,
458
+ pendingCount: this.queue.size()
459
+ });
415
460
  }
416
461
  }
417
462
  /**
@@ -439,10 +484,56 @@ var Signals = class {
439
484
  initialDelay: this.config.retryDelay,
440
485
  onRetry: (attempt, error) => {
441
486
  this.log(`Retry attempt ${attempt}`, error);
487
+ this.emitError({
488
+ type: "send_retry",
489
+ message: `Retrying batch send (attempt ${attempt})`,
490
+ error,
491
+ batchId: batch.batch_id,
492
+ eventCount: batch.events.length,
493
+ attempt
494
+ });
442
495
  }
443
496
  }
444
497
  );
445
498
  }
499
+ /**
500
+ * Subscribe to error events from the SDK. Returns an unsubscribe function.
501
+ *
502
+ * @example
503
+ * const off = signals.on('error', (e) => {
504
+ * console.error('[signals]', e.type, e.message, e.error);
505
+ * });
506
+ * // ... later
507
+ * off();
508
+ */
509
+ on(event, listener) {
510
+ if (event !== "error") {
511
+ throw new Error(`Signals.on: unknown event "${event}"`);
512
+ }
513
+ this.errorListeners.add(listener);
514
+ return () => this.errorListeners.delete(listener);
515
+ }
516
+ /** Remove a previously registered error listener. */
517
+ off(event, listener) {
518
+ if (event !== "error") return;
519
+ this.errorListeners.delete(listener);
520
+ }
521
+ emitError(payload) {
522
+ if (this.errorListeners.size === 0) {
523
+ console.warn(
524
+ `[Signals] ${payload.type}: ${payload.message}`,
525
+ payload.error ?? "",
526
+ payload.pendingCount !== void 0 ? `(${payload.pendingCount} events still queued)` : ""
527
+ );
528
+ return;
529
+ }
530
+ for (const listener of this.errorListeners) {
531
+ try {
532
+ listener(payload);
533
+ } catch {
534
+ }
535
+ }
536
+ }
446
537
  /**
447
538
  * Start periodic flush timer
448
539
  */
@@ -534,21 +625,38 @@ var Signals = class {
534
625
  this.log("Context reset");
535
626
  }
536
627
  /**
537
- * Destroy client and cleanup
628
+ * Destroy the client and drain the queue.
629
+ *
630
+ * Awaits any in-flight flush, then keeps flushing batches until the queue
631
+ * is empty (or sends keep failing without progress). Only after that does
632
+ * the instance refuse new events. If anything remains undrainable, emits a
633
+ * `destroy_pending` error so callers can see the loss instead of having it
634
+ * silently swallowed at process exit.
538
635
  */
539
636
  async destroy() {
540
637
  if (this.isDestroyed) return;
541
638
  this.log("Destroying Signals instance");
542
- this.isDestroyed = true;
543
639
  this.stopFlushTimer();
544
- await this.flush();
640
+ if (this.flushingPromise) {
641
+ await this.flushingPromise.catch(() => {
642
+ });
643
+ }
644
+ const stranded = await this.flushAll();
645
+ this.isDestroyed = true;
646
+ if (stranded > 0) {
647
+ this.emitError({
648
+ type: "destroy_pending",
649
+ message: `Destroyed with ${stranded} unsent events \u2014 sends are failing`,
650
+ pendingCount: stranded
651
+ });
652
+ }
545
653
  }
546
654
  /**
547
655
  * Debug logging
548
656
  */
549
657
  log(message, data) {
550
658
  if (this.config.debug) {
551
- console.log(`[LoggClient] ${message}`, data ?? "");
659
+ console.log(`[Signals] ${message}`, data ?? "");
552
660
  }
553
661
  }
554
662
  };
@@ -0,0 +1,216 @@
1
+ interface BaseItem {
2
+ id: string;
3
+ }
4
+ interface FeatureConfig<T extends BaseItem> {
5
+ extract: (item: T) => string | null;
6
+ capacity?: number;
7
+ challengerCapacity?: number;
8
+ epsilon?: number;
9
+ weight?: number;
10
+ decrementCap?: number;
11
+ }
12
+ type Schema<T extends BaseItem> = Record<string, FeatureConfig<T>>;
13
+ declare const DEFAULT_CAPACITY = 3;
14
+ declare const DEFAULT_DECREMENT_CAP = 4;
15
+ declare const DEFAULT_EPSILON = 0.1;
16
+ declare const SIGNALS: {
17
+ readonly view: 0.5;
18
+ readonly click: 2;
19
+ readonly dwell: 3;
20
+ readonly collect: 8;
21
+ readonly skip: -1;
22
+ };
23
+ type SignalName = keyof typeof SIGNALS;
24
+ interface Slot {
25
+ value: string;
26
+ power: number;
27
+ }
28
+
29
+ interface SlotTableConfig {
30
+ capacity: number;
31
+ challengerCapacity: number;
32
+ epsilon: number;
33
+ }
34
+ declare const DEFAULT_SLOT_CONFIG: SlotTableConfig;
35
+ interface SlotTableState {
36
+ slots: Slot[];
37
+ challengers: Slot[];
38
+ }
39
+ declare function emptyState(): SlotTableState;
40
+ declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
41
+ declare function decay(state: SlotTableState, factor: number): void;
42
+ declare function contribution(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
43
+ declare function isEmpty$1(state: SlotTableState): boolean;
44
+
45
+ interface InterestConfig<T extends BaseItem> {
46
+ schema: Schema<T>;
47
+ popularity?: (item: T) => number;
48
+ popularityWeight: number;
49
+ decayFactor: number;
50
+ decayEvery: number;
51
+ }
52
+ declare const DEFAULT_INTEREST_CONFIG: Omit<InterestConfig<BaseItem>, 'schema'>;
53
+ type InterestState = {
54
+ tables: Record<string, SlotTableState>;
55
+ eventsSinceDecay: number;
56
+ totalEvents: number;
57
+ };
58
+ declare function emptyInterest<T extends BaseItem>(schema: Schema<T>): InterestState;
59
+ declare function applyEvent<T extends BaseItem>(state: InterestState, item: T, magnitude: number, config: InterestConfig<T>): void;
60
+ declare function scoreItem$1<T extends BaseItem>(state: InterestState, item: T, popularityNorm: number, config: InterestConfig<T>): number;
61
+ declare function isCold$1(state: InterestState): boolean;
62
+ declare function describe$1(state: InterestState, top?: number): Record<string, Array<[string, number]>>;
63
+
64
+ type RNG = () => number;
65
+ declare const defaultRng: RNG;
66
+ declare function weightedSampleWithoutReplacement<T>(pool: T[], weight: (item: T) => number, count: number, rng?: RNG): T[];
67
+ declare function seededRng(seed: number): RNG;
68
+
69
+ interface RecommenderConfig<T extends BaseItem> {
70
+ schema: Schema<T>;
71
+ popularity?: (item: T) => number;
72
+ popularityWeight?: number;
73
+ decayFactor?: number;
74
+ decayEvery?: number;
75
+ seenWindow?: number;
76
+ batchSize?: number;
77
+ rng?: RNG;
78
+ }
79
+ interface ResolvedConfig$1<T extends BaseItem> {
80
+ interest: InterestConfig<T>;
81
+ popularity: ((item: T) => number) | null;
82
+ seenWindow: number;
83
+ batchSize: number;
84
+ rng: RNG;
85
+ }
86
+ interface RecommendDiagnostics {
87
+ cold: boolean;
88
+ candidates: number;
89
+ excluded_seen: number;
90
+ excluded_owned: number;
91
+ }
92
+ interface RecommendResult<T extends BaseItem> {
93
+ items: T[];
94
+ scores: number[];
95
+ diagnostics: RecommendDiagnostics;
96
+ }
97
+ declare class Recommender<T extends BaseItem> {
98
+ readonly catalog: T[];
99
+ readonly byId: Map<string, T>;
100
+ readonly interest: InterestState;
101
+ readonly config: ResolvedConfig$1<T>;
102
+ readonly maxPopularity: number;
103
+ private ownedIds;
104
+ private seenOrder;
105
+ private seen;
106
+ constructor(catalog: T[], config: RecommenderConfig<T>);
107
+ setOwned(ids: Iterable<string>): void;
108
+ prime(items: T[], magnitudePerItem?: number): void;
109
+ engage(item: T, score: number): void;
110
+ recommend(count?: number): RecommendResult<T>;
111
+ seenCount(): number;
112
+ clearSeen(): void;
113
+ private markSeen;
114
+ reset(): void;
115
+ }
116
+
117
+ interface BucketState {
118
+ slots: Slot[];
119
+ pointer: number;
120
+ }
121
+ interface BucketConfig {
122
+ capacity: number;
123
+ decrementCap: number;
124
+ }
125
+ declare function emptyBucket(): BucketState;
126
+ declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
127
+ declare function powerOf(state: BucketState, value: string | null): number;
128
+ declare function isEmpty(state: BucketState): boolean;
129
+
130
+ interface DualInterestConfig<T extends BaseItem> {
131
+ schema: Schema<T>;
132
+ }
133
+ interface DualBucketState {
134
+ liked: Record<string, BucketState>;
135
+ disliked: Record<string, BucketState>;
136
+ totalEvents: number;
137
+ }
138
+ declare function emptyDualState<T extends BaseItem>(schema: Schema<T>): DualBucketState;
139
+ declare function engagePositive<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
140
+ declare function engageNegative<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
141
+ interface ItemScore {
142
+ liked: number;
143
+ disliked: number;
144
+ net: number;
145
+ byFeature: Array<{
146
+ key: string;
147
+ value: string | null;
148
+ liked: number;
149
+ disliked: number;
150
+ }>;
151
+ }
152
+ declare function scoreItem<T extends BaseItem>(state: DualBucketState, item: T, config: DualInterestConfig<T>): ItemScore;
153
+ declare function isCold(state: DualBucketState): boolean;
154
+ declare function describe(state: DualBucketState, top?: number): {
155
+ liked: Record<string, [string, number][]>;
156
+ disliked: Record<string, [string, number][]>;
157
+ };
158
+
159
+ interface DualRecommenderConfig<T extends BaseItem> {
160
+ schema: Schema<T>;
161
+ sampleSize?: number;
162
+ shuffleThreshold?: number;
163
+ rng?: RNG;
164
+ }
165
+ interface ResolvedConfig<T extends BaseItem> {
166
+ interest: DualInterestConfig<T>;
167
+ sampleSize: number;
168
+ shuffleThreshold: number;
169
+ rng: RNG;
170
+ }
171
+ interface DualRecommendDiagnostics<T extends BaseItem> {
172
+ cold: boolean;
173
+ candidatesPool: number;
174
+ sampleSize: number;
175
+ bestNet: number;
176
+ bestLiked: number;
177
+ bestDisliked: number;
178
+ freshPagesIssued: number;
179
+ breakdown: ItemScore['byFeature'] | null;
180
+ runnersUp: Array<{
181
+ item: T;
182
+ net: number;
183
+ liked: number;
184
+ disliked: number;
185
+ }>;
186
+ }
187
+ interface DualRecommendResult<T extends BaseItem> {
188
+ items: T[];
189
+ scores: number[];
190
+ diagnostics: DualRecommendDiagnostics<T>;
191
+ }
192
+ declare class DualBucketRecommender<T extends BaseItem> {
193
+ readonly catalog: T[];
194
+ readonly byId: Map<string, T>;
195
+ readonly interest: DualBucketState;
196
+ readonly config: ResolvedConfig<T>;
197
+ private ownedIds;
198
+ private seen;
199
+ private freshPagesIssued;
200
+ constructor(catalog: T[], config: DualRecommenderConfig<T>);
201
+ setOwned(ids: Iterable<string>): void;
202
+ setSampleSize(k: number): void;
203
+ prime(items: T[], magnitudePerItem?: number): void;
204
+ engage(item: T, score: number): void;
205
+ recommend(count?: number): DualRecommendResult<T>;
206
+ private pickOne;
207
+ private candidatePool;
208
+ seenCount(): number;
209
+ clearSeen(): void;
210
+ reset(): void;
211
+ }
212
+
213
+ declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
214
+ declare function yearBucket(year: number | null, bandSize?: number): string | null;
215
+
216
+ export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };
package/dist/reco.d.ts ADDED
@@ -0,0 +1,216 @@
1
+ interface BaseItem {
2
+ id: string;
3
+ }
4
+ interface FeatureConfig<T extends BaseItem> {
5
+ extract: (item: T) => string | null;
6
+ capacity?: number;
7
+ challengerCapacity?: number;
8
+ epsilon?: number;
9
+ weight?: number;
10
+ decrementCap?: number;
11
+ }
12
+ type Schema<T extends BaseItem> = Record<string, FeatureConfig<T>>;
13
+ declare const DEFAULT_CAPACITY = 3;
14
+ declare const DEFAULT_DECREMENT_CAP = 4;
15
+ declare const DEFAULT_EPSILON = 0.1;
16
+ declare const SIGNALS: {
17
+ readonly view: 0.5;
18
+ readonly click: 2;
19
+ readonly dwell: 3;
20
+ readonly collect: 8;
21
+ readonly skip: -1;
22
+ };
23
+ type SignalName = keyof typeof SIGNALS;
24
+ interface Slot {
25
+ value: string;
26
+ power: number;
27
+ }
28
+
29
+ interface SlotTableConfig {
30
+ capacity: number;
31
+ challengerCapacity: number;
32
+ epsilon: number;
33
+ }
34
+ declare const DEFAULT_SLOT_CONFIG: SlotTableConfig;
35
+ interface SlotTableState {
36
+ slots: Slot[];
37
+ challengers: Slot[];
38
+ }
39
+ declare function emptyState(): SlotTableState;
40
+ declare function observe(state: SlotTableState, value: string, weight: number, config?: SlotTableConfig): void;
41
+ declare function decay(state: SlotTableState, factor: number): void;
42
+ declare function contribution(state: SlotTableState, value: string | null, config?: SlotTableConfig): number;
43
+ declare function isEmpty$1(state: SlotTableState): boolean;
44
+
45
+ interface InterestConfig<T extends BaseItem> {
46
+ schema: Schema<T>;
47
+ popularity?: (item: T) => number;
48
+ popularityWeight: number;
49
+ decayFactor: number;
50
+ decayEvery: number;
51
+ }
52
+ declare const DEFAULT_INTEREST_CONFIG: Omit<InterestConfig<BaseItem>, 'schema'>;
53
+ type InterestState = {
54
+ tables: Record<string, SlotTableState>;
55
+ eventsSinceDecay: number;
56
+ totalEvents: number;
57
+ };
58
+ declare function emptyInterest<T extends BaseItem>(schema: Schema<T>): InterestState;
59
+ declare function applyEvent<T extends BaseItem>(state: InterestState, item: T, magnitude: number, config: InterestConfig<T>): void;
60
+ declare function scoreItem$1<T extends BaseItem>(state: InterestState, item: T, popularityNorm: number, config: InterestConfig<T>): number;
61
+ declare function isCold$1(state: InterestState): boolean;
62
+ declare function describe$1(state: InterestState, top?: number): Record<string, Array<[string, number]>>;
63
+
64
+ type RNG = () => number;
65
+ declare const defaultRng: RNG;
66
+ declare function weightedSampleWithoutReplacement<T>(pool: T[], weight: (item: T) => number, count: number, rng?: RNG): T[];
67
+ declare function seededRng(seed: number): RNG;
68
+
69
+ interface RecommenderConfig<T extends BaseItem> {
70
+ schema: Schema<T>;
71
+ popularity?: (item: T) => number;
72
+ popularityWeight?: number;
73
+ decayFactor?: number;
74
+ decayEvery?: number;
75
+ seenWindow?: number;
76
+ batchSize?: number;
77
+ rng?: RNG;
78
+ }
79
+ interface ResolvedConfig$1<T extends BaseItem> {
80
+ interest: InterestConfig<T>;
81
+ popularity: ((item: T) => number) | null;
82
+ seenWindow: number;
83
+ batchSize: number;
84
+ rng: RNG;
85
+ }
86
+ interface RecommendDiagnostics {
87
+ cold: boolean;
88
+ candidates: number;
89
+ excluded_seen: number;
90
+ excluded_owned: number;
91
+ }
92
+ interface RecommendResult<T extends BaseItem> {
93
+ items: T[];
94
+ scores: number[];
95
+ diagnostics: RecommendDiagnostics;
96
+ }
97
+ declare class Recommender<T extends BaseItem> {
98
+ readonly catalog: T[];
99
+ readonly byId: Map<string, T>;
100
+ readonly interest: InterestState;
101
+ readonly config: ResolvedConfig$1<T>;
102
+ readonly maxPopularity: number;
103
+ private ownedIds;
104
+ private seenOrder;
105
+ private seen;
106
+ constructor(catalog: T[], config: RecommenderConfig<T>);
107
+ setOwned(ids: Iterable<string>): void;
108
+ prime(items: T[], magnitudePerItem?: number): void;
109
+ engage(item: T, score: number): void;
110
+ recommend(count?: number): RecommendResult<T>;
111
+ seenCount(): number;
112
+ clearSeen(): void;
113
+ private markSeen;
114
+ reset(): void;
115
+ }
116
+
117
+ interface BucketState {
118
+ slots: Slot[];
119
+ pointer: number;
120
+ }
121
+ interface BucketConfig {
122
+ capacity: number;
123
+ decrementCap: number;
124
+ }
125
+ declare function emptyBucket(): BucketState;
126
+ declare function admit(state: BucketState, value: string, score: number, config?: BucketConfig): void;
127
+ declare function powerOf(state: BucketState, value: string | null): number;
128
+ declare function isEmpty(state: BucketState): boolean;
129
+
130
+ interface DualInterestConfig<T extends BaseItem> {
131
+ schema: Schema<T>;
132
+ }
133
+ interface DualBucketState {
134
+ liked: Record<string, BucketState>;
135
+ disliked: Record<string, BucketState>;
136
+ totalEvents: number;
137
+ }
138
+ declare function emptyDualState<T extends BaseItem>(schema: Schema<T>): DualBucketState;
139
+ declare function engagePositive<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
140
+ declare function engageNegative<T extends BaseItem>(state: DualBucketState, item: T, magnitude: number, config: DualInterestConfig<T>): void;
141
+ interface ItemScore {
142
+ liked: number;
143
+ disliked: number;
144
+ net: number;
145
+ byFeature: Array<{
146
+ key: string;
147
+ value: string | null;
148
+ liked: number;
149
+ disliked: number;
150
+ }>;
151
+ }
152
+ declare function scoreItem<T extends BaseItem>(state: DualBucketState, item: T, config: DualInterestConfig<T>): ItemScore;
153
+ declare function isCold(state: DualBucketState): boolean;
154
+ declare function describe(state: DualBucketState, top?: number): {
155
+ liked: Record<string, [string, number][]>;
156
+ disliked: Record<string, [string, number][]>;
157
+ };
158
+
159
+ interface DualRecommenderConfig<T extends BaseItem> {
160
+ schema: Schema<T>;
161
+ sampleSize?: number;
162
+ shuffleThreshold?: number;
163
+ rng?: RNG;
164
+ }
165
+ interface ResolvedConfig<T extends BaseItem> {
166
+ interest: DualInterestConfig<T>;
167
+ sampleSize: number;
168
+ shuffleThreshold: number;
169
+ rng: RNG;
170
+ }
171
+ interface DualRecommendDiagnostics<T extends BaseItem> {
172
+ cold: boolean;
173
+ candidatesPool: number;
174
+ sampleSize: number;
175
+ bestNet: number;
176
+ bestLiked: number;
177
+ bestDisliked: number;
178
+ freshPagesIssued: number;
179
+ breakdown: ItemScore['byFeature'] | null;
180
+ runnersUp: Array<{
181
+ item: T;
182
+ net: number;
183
+ liked: number;
184
+ disliked: number;
185
+ }>;
186
+ }
187
+ interface DualRecommendResult<T extends BaseItem> {
188
+ items: T[];
189
+ scores: number[];
190
+ diagnostics: DualRecommendDiagnostics<T>;
191
+ }
192
+ declare class DualBucketRecommender<T extends BaseItem> {
193
+ readonly catalog: T[];
194
+ readonly byId: Map<string, T>;
195
+ readonly interest: DualBucketState;
196
+ readonly config: ResolvedConfig<T>;
197
+ private ownedIds;
198
+ private seen;
199
+ private freshPagesIssued;
200
+ constructor(catalog: T[], config: DualRecommenderConfig<T>);
201
+ setOwned(ids: Iterable<string>): void;
202
+ setSampleSize(k: number): void;
203
+ prime(items: T[], magnitudePerItem?: number): void;
204
+ engage(item: T, score: number): void;
205
+ recommend(count?: number): DualRecommendResult<T>;
206
+ private pickOne;
207
+ private candidatePool;
208
+ seenCount(): number;
209
+ clearSeen(): void;
210
+ reset(): void;
211
+ }
212
+
213
+ declare function logDecadeBucket(value: number | null, multiplier?: number, prefix?: string): string | null;
214
+ declare function yearBucket(year: number | null, bandSize?: number): string | null;
215
+
216
+ export { type BaseItem, type BucketConfig, type BucketState, DEFAULT_CAPACITY, DEFAULT_DECREMENT_CAP, DEFAULT_EPSILON, DEFAULT_INTEREST_CONFIG, DEFAULT_SLOT_CONFIG, DualBucketRecommender, type DualBucketState, type DualInterestConfig, type ItemScore as DualItemScore, type DualRecommendDiagnostics, type DualRecommendResult, type DualRecommenderConfig, type FeatureConfig, type InterestConfig, type InterestState, type RNG, type RecommendDiagnostics, type RecommendResult, Recommender, type RecommenderConfig, SIGNALS, type Schema, type SignalName, type Slot, type SlotTableConfig, type SlotTableState, admit, applyEvent, contribution, decay, defaultRng, describe$1 as describe, describe as describeDual, emptyBucket, emptyDualState, emptyInterest, emptyState, engageNegative, engagePositive, isCold$1 as isCold, isCold as isColdDual, isEmpty$1 as isEmpty, isEmpty as isEmptyBucket, logDecadeBucket, observe, powerOf, scoreItem$1 as scoreItem, scoreItem as scoreItemDual, seededRng, weightedSampleWithoutReplacement, yearBucket };