@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/README.md +115 -3
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/index.d.mts +20 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.js +133 -22
- package/dist/index.mjs +133 -25
- package/dist/reco.d.mts +216 -0
- package/dist/reco.d.ts +216 -0
- package/dist/reco.js +659 -0
- package/dist/reco.mjs +602 -0
- package/package.json +11 -4
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
}
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
-
*
|
|
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.
|
|
397
|
+
if (this.queue.isEmpty()) return;
|
|
398
|
+
if (this.flushingPromise) {
|
|
399
|
+
await this.flushingPromise.catch(() => {
|
|
400
|
+
});
|
|
391
401
|
return;
|
|
392
402
|
}
|
|
393
|
-
this.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
|
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
|
-
|
|
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(`[
|
|
659
|
+
console.log(`[Signals] ${message}`, data ?? "");
|
|
552
660
|
}
|
|
553
661
|
}
|
|
554
662
|
};
|
package/dist/reco.d.mts
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 };
|
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 };
|