fansunited-data-layer 0.15.3 → 0.17.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 +41 -0
- package/api/fansunited/index.d.ts +2 -0
- package/api/fansunited/index.d.ts.map +1 -1
- package/api/fansunited/search/__tests__/locale-agnostic-cache.test.d.ts +2 -0
- package/api/fansunited/search/__tests__/locale-agnostic-cache.test.d.ts.map +1 -0
- package/api/fansunited/search/index.d.ts.map +1 -1
- package/api/fansunited/search/index.js +41 -25
- package/api/fansunited/search/index.js.map +1 -1
- package/api/fansunited/search/transformer.js +3 -2
- package/api/fansunited/search/transformer.js.map +1 -1
- package/api/fansunited/sports/competition/__tests__/exports.test.d.ts +2 -0
- package/api/fansunited/sports/competition/__tests__/exports.test.d.ts.map +1 -0
- package/api/fansunited/sports/competition/__tests__/hydrated.test.d.ts +2 -0
- package/api/fansunited/sports/competition/__tests__/hydrated.test.d.ts.map +1 -0
- package/api/fansunited/sports/competition/__tests__/index.test.d.ts +2 -0
- package/api/fansunited/sports/competition/__tests__/index.test.d.ts.map +1 -0
- package/api/fansunited/sports/competition/hydrated.d.ts +45 -0
- package/api/fansunited/sports/competition/hydrated.d.ts.map +1 -0
- package/api/fansunited/sports/competition/hydrated.js +26 -0
- package/api/fansunited/sports/competition/hydrated.js.map +1 -0
- package/api/fansunited/sports/competition/index.d.ts +19 -0
- package/api/fansunited/sports/competition/index.d.ts.map +1 -0
- package/api/fansunited/sports/competition/index.js +27 -0
- package/api/fansunited/sports/competition/index.js.map +1 -0
- package/api/fansunited/sports/competition/transformer.d.ts +8 -0
- package/api/fansunited/sports/competition/transformer.d.ts.map +1 -0
- package/api/fansunited/sports/competition/transformer.js +52 -0
- package/api/fansunited/sports/competition/transformer.js.map +1 -0
- package/api/fansunited/sports/competition/types.d.ts +57 -0
- package/api/fansunited/sports/competition/types.d.ts.map +1 -0
- package/api/fansunited/sports/constants.d.ts +5 -0
- package/api/fansunited/sports/constants.d.ts.map +1 -0
- package/api/fansunited/sports/constants.js +5 -0
- package/api/fansunited/sports/constants.js.map +1 -0
- package/api/fansunited/sports/http.d.ts +8 -0
- package/api/fansunited/sports/http.d.ts.map +1 -0
- package/api/fansunited/sports/http.js +7 -0
- package/api/fansunited/sports/http.js.map +1 -0
- package/api/fansunited/sports/index.d.ts +15 -0
- package/api/fansunited/sports/index.d.ts.map +1 -0
- package/api/fansunited/sports/livescore/__tests__/hydrated.test.d.ts +2 -0
- package/api/fansunited/sports/livescore/__tests__/hydrated.test.d.ts.map +1 -0
- package/api/fansunited/sports/livescore/hydrated.d.ts +48 -0
- package/api/fansunited/sports/livescore/hydrated.d.ts.map +1 -0
- package/api/fansunited/sports/livescore/hydrated.js +34 -0
- package/api/fansunited/sports/livescore/hydrated.js.map +1 -0
- package/api/fansunited/sports/livescore/index.d.ts +27 -0
- package/api/fansunited/sports/livescore/index.d.ts.map +1 -0
- package/api/fansunited/sports/livescore/index.js +36 -0
- package/api/fansunited/sports/livescore/index.js.map +1 -0
- package/api/fansunited/sports/livescore/transformer.d.ts +8 -0
- package/api/fansunited/sports/livescore/transformer.d.ts.map +1 -0
- package/api/fansunited/sports/livescore/transformer.js +44 -0
- package/api/fansunited/sports/livescore/transformer.js.map +1 -0
- package/api/fansunited/sports/livescore/types.d.ts +58 -0
- package/api/fansunited/sports/livescore/types.d.ts.map +1 -0
- package/cache/__tests__/redis-integration.test.d.ts +14 -0
- package/cache/__tests__/redis-integration.test.d.ts.map +1 -0
- package/cache/__tests__/redis-l2-store.d.ts +52 -0
- package/cache/__tests__/redis-l2-store.d.ts.map +1 -0
- package/cache/__tests__/test-l2-store.d.ts +18 -0
- package/cache/__tests__/test-l2-store.d.ts.map +1 -0
- package/cache/cache-manager.d.ts +117 -12
- package/cache/cache-manager.d.ts.map +1 -1
- package/cache/cache-manager.js +23 -14
- package/cache/cache-manager.js.map +1 -1
- package/cache/cleanup.d.ts +1 -1
- package/cache/cleanup.d.ts.map +1 -1
- package/cache/index.d.ts +3 -2
- package/cache/index.d.ts.map +1 -1
- package/cache/sqlite-store.d.ts +4 -4
- package/cache/sqlite-store.d.ts.map +1 -1
- package/cache/types.d.ts +9 -1
- package/cache/types.d.ts.map +1 -1
- package/cache/types.js +5 -0
- package/cache/types.js.map +1 -0
- package/fansunited-data-layer.js +13 -1
- package/fansunited-data-layer.js.map +1 -1
- package/index.d.ts +5 -5
- package/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/types/canonical/index.d.ts +2 -0
- package/types/canonical/index.d.ts.map +1 -1
- package/types/canonical/sports-competition.types.d.ts +100 -0
- package/types/canonical/sports-competition.types.d.ts.map +1 -0
- package/types/canonical/sports-livescore.types.d.ts +84 -0
- package/types/canonical/sports-livescore.types.d.ts.map +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw API response and option types for the Fans United Sports API
|
|
3
|
+
* livescore endpoint.
|
|
4
|
+
*/
|
|
5
|
+
import type { NextCacheOptions } from "../../http";
|
|
6
|
+
import type { FUSportsLivescoreEventStatus, FUSportsLivescoreSport } from "../../../../types/canonical/sports-livescore.types";
|
|
7
|
+
export interface RawSportsLivescoreStatus {
|
|
8
|
+
type: string;
|
|
9
|
+
code: string;
|
|
10
|
+
sub_type: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RawSportsLivescoreScore {
|
|
13
|
+
key: string;
|
|
14
|
+
competitor_one: string;
|
|
15
|
+
competitor_two: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RawSportsLivescoreCompetitor {
|
|
18
|
+
id: string;
|
|
19
|
+
type: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RawSportsLivescoreEvent {
|
|
22
|
+
id: string;
|
|
23
|
+
status: RawSportsLivescoreStatus;
|
|
24
|
+
score?: RawSportsLivescoreScore[];
|
|
25
|
+
competitor_one: RawSportsLivescoreCompetitor;
|
|
26
|
+
competitor_two: RawSportsLivescoreCompetitor;
|
|
27
|
+
start_time: string;
|
|
28
|
+
}
|
|
29
|
+
export interface RawSportsLivescoreCompetitionGroup {
|
|
30
|
+
competition_id: string;
|
|
31
|
+
sport: string;
|
|
32
|
+
events: RawSportsLivescoreEvent[];
|
|
33
|
+
}
|
|
34
|
+
export interface RawSportsLivescoreMeta {
|
|
35
|
+
total_competitions: number;
|
|
36
|
+
total_events: number;
|
|
37
|
+
}
|
|
38
|
+
export interface RawSportsLivescoreResponse {
|
|
39
|
+
data: RawSportsLivescoreCompetitionGroup[];
|
|
40
|
+
meta: RawSportsLivescoreMeta;
|
|
41
|
+
}
|
|
42
|
+
export interface GetSportsLivescoreOptions {
|
|
43
|
+
/** Comma-joined or array of competition IDs to filter by. */
|
|
44
|
+
competitionIds?: string[];
|
|
45
|
+
/** Match IDs to filter by. */
|
|
46
|
+
matchIds?: string[];
|
|
47
|
+
/** Competition IDs defining the order of competition groups in the response. */
|
|
48
|
+
competitionsOrder?: string[];
|
|
49
|
+
/** Date in yyyy-MM-dd format. Defaults to the current date (UTC). */
|
|
50
|
+
date?: string;
|
|
51
|
+
/** Filter by event status. */
|
|
52
|
+
eventStatus?: FUSportsLivescoreEventStatus;
|
|
53
|
+
/** Filter by sport. */
|
|
54
|
+
sport?: FUSportsLivescoreSport;
|
|
55
|
+
/** Next.js cache options for ISR/on-demand revalidation. */
|
|
56
|
+
next?: NextCacheOptions;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/livescore/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EACR,4BAA4B,EAC5B,sBAAsB,EACzB,MAAM,oDAAoD,CAAC;AAM5D,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,wBAAwB,CAAC;IACjC,KAAK,CAAC,EAAE,uBAAuB,EAAE,CAAC;IAClC,cAAc,EAAE,4BAA4B,CAAC;IAC7C,cAAc,EAAE,4BAA4B,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kCAAkC;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,uBAAuB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACnC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACvC,IAAI,EAAE,kCAAkC,EAAE,CAAC;IAC3C,IAAI,EAAE,sBAAsB,CAAC;CAChC;AAMD,MAAM,WAAW,yBAAyB;IACtC,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,WAAW,CAAC,EAAE,4BAA4B,CAAC;IAC3C,uBAAuB;IACvB,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,gBAAgB,CAAC;CAC3B"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis L2 integration tests. Skipped unless REDIS_HOST is set, so the
|
|
3
|
+
* default `npm test` run stays hermetic.
|
|
4
|
+
*
|
|
5
|
+
* To run:
|
|
6
|
+
* docker compose up -d redis
|
|
7
|
+
* REDIS_HOST=redis npm test -- --run src/lib/cache/__tests__/redis-integration.test.ts
|
|
8
|
+
*
|
|
9
|
+
* From a host shell (Redis exposed on localhost:6379):
|
|
10
|
+
* docker compose up -d redis
|
|
11
|
+
* REDIS_HOST=localhost npm test -- --run src/lib/cache/__tests__/redis-integration.test.ts
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=redis-integration.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-integration.test.d.ts","sourceRoot":"","sources":["../../../src/lib/cache/__tests__/redis-integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference Redis L2 adapter — kept under __tests__/ so it ships only with
|
|
3
|
+
* the dev install (ioredis is a devDependency). Consumers copy this verbatim
|
|
4
|
+
* (or adapt) into their own codebase per the design spec's "Decision 2":
|
|
5
|
+
* the Redis client is consumer-supplied so the data-layer stays browser-safe.
|
|
6
|
+
*
|
|
7
|
+
* Implementation notes
|
|
8
|
+
* --------------------
|
|
9
|
+
*
|
|
10
|
+
* 1. **Cache envelope.** Each value is a JSON-encoded `CacheEntry<T>`
|
|
11
|
+
* (`{ data, storedAt }`). The `storedAt` timestamp is what the cache
|
|
12
|
+
* manager uses to decide freshness/expiry — Redis's own TTL is a memory
|
|
13
|
+
* safety net, not the source of truth for SWR semantics.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Eviction TTL.** Every `set`/`setMany` issues an `EX` matching
|
|
16
|
+
* `getEntityTTL(entity).maxTTL`. That keeps Redis from accumulating dead
|
|
17
|
+
* entries without needing a periodic cleanup scan. The cache manager
|
|
18
|
+
* still defensively rejects entries past `maxTTL` on read via
|
|
19
|
+
* `isExpired(entry, maxTTL)`, so the two layers can drift slightly
|
|
20
|
+
* without correctness issues.
|
|
21
|
+
*
|
|
22
|
+
* 3. **Graceful degradation.** The spec requires that a degraded L2 must
|
|
23
|
+
* never throw to the caller. We:
|
|
24
|
+
* - report `isInitialized` based on the ioredis status (only `"ready"`
|
|
25
|
+
* qualifies);
|
|
26
|
+
* - swallow read errors and return `undefined`;
|
|
27
|
+
* - swallow write errors and no-op.
|
|
28
|
+
* The cache manager will then fall back to L1 + API.
|
|
29
|
+
*
|
|
30
|
+
* 4. **`clear()` namespacing.** Uses `SCAN MATCH <prefix>*` + pipelined
|
|
31
|
+
* `DEL`. Other keys in the same Redis instance are untouched, which is
|
|
32
|
+
* what the `fudl:` prefix is for in the first place.
|
|
33
|
+
*/
|
|
34
|
+
import type { Redis } from "ioredis";
|
|
35
|
+
import type { L2Store } from "../cache-manager";
|
|
36
|
+
export interface RedisL2StoreOptions {
|
|
37
|
+
/** Connected (or auto-connecting) ioredis client. */
|
|
38
|
+
client: Redis;
|
|
39
|
+
/**
|
|
40
|
+
* Override the prefix used by `clear()` for SCAN. Defaults to the
|
|
41
|
+
* data-layer's CACHE_KEY_PREFIX so consumers can clear the data-layer's
|
|
42
|
+
* keys without touching unrelated data in a shared Redis instance.
|
|
43
|
+
*/
|
|
44
|
+
clearPrefix?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Optional logger. Defaults to console. Swap for your structured logger
|
|
47
|
+
* in production so failures don't get lost in stdout.
|
|
48
|
+
*/
|
|
49
|
+
log?: (level: "warn" | "error", msg: string, err?: unknown) => void;
|
|
50
|
+
}
|
|
51
|
+
export declare function createRedisL2Store(options: RedisL2StoreOptions): L2Store;
|
|
52
|
+
//# sourceMappingURL=redis-l2-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-l2-store.d.ts","sourceRoot":"","sources":["../../../src/lib/cache/__tests__/redis-l2-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAUhD,MAAM,WAAW,mBAAmB;IAChC,qDAAqD;IACrD,MAAM,EAAE,KAAK,CAAC;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACvE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAkFxE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async in-memory L2 stub. Used by tests that exercise the async L2 path
|
|
3
|
+
* (Redis adapters are async). Filename intentionally lacks `.test.ts` so
|
|
4
|
+
* vitest's `src/**\/*.test.ts` glob does not pick it up.
|
|
5
|
+
*/
|
|
6
|
+
import type { L2Store } from "../cache-manager";
|
|
7
|
+
import type { EntityType } from "../types";
|
|
8
|
+
interface StoredRow {
|
|
9
|
+
entity: EntityType;
|
|
10
|
+
data: unknown;
|
|
11
|
+
storedAt: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function createTestL2Store(): L2Store & {
|
|
14
|
+
reset(): void;
|
|
15
|
+
raw: Map<string, StoredRow>;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=test-l2-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-l2-store.d.ts","sourceRoot":"","sources":["../../../src/lib/cache/__tests__/test-l2-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,UAAU,CAAC;AAEvD,UAAU,SAAS;IACf,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,IAAI,OAAO,GAAG;IAAE,KAAK,IAAI,IAAI,CAAC;IAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;CAAE,CAoD5F"}
|
package/cache/cache-manager.d.ts
CHANGED
|
@@ -4,34 +4,139 @@
|
|
|
4
4
|
* L2 (SQLite) is never statically imported here — it's injected at runtime
|
|
5
5
|
* via initL2() so that browser bundles don't pull in better-sqlite3.
|
|
6
6
|
*/
|
|
7
|
-
import type { CacheEntry, EntityCacheConfig, EntityType } from "./types";
|
|
8
|
-
/** Interface that L2 stores must satisfy */
|
|
7
|
+
import type { CacheEntry, EntityCacheConfig, EntityType, MaybePromise } from "./types";
|
|
8
|
+
/** Interface that L2 stores must satisfy. Methods may be sync or async. */
|
|
9
9
|
export interface L2Store {
|
|
10
10
|
get isInitialized(): boolean;
|
|
11
|
-
get<T>(key: string): CacheEntry<T> | undefined
|
|
12
|
-
set(key: string, entity:
|
|
11
|
+
get<T>(key: string): MaybePromise<CacheEntry<T> | undefined>;
|
|
12
|
+
set(key: string, entity: EntityType, data: unknown): MaybePromise<void>;
|
|
13
13
|
setMany(entries: {
|
|
14
14
|
key: string;
|
|
15
|
-
entity:
|
|
15
|
+
entity: EntityType;
|
|
16
16
|
data: unknown;
|
|
17
|
-
}[]): void
|
|
18
|
-
cleanup(entity:
|
|
19
|
-
clear(): void
|
|
17
|
+
}[]): MaybePromise<void>;
|
|
18
|
+
cleanup(entity: EntityType, maxTTLSeconds: number): MaybePromise<number>;
|
|
19
|
+
clear(): MaybePromise<void>;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Initialize L2 cache layer. Call
|
|
23
|
-
*
|
|
22
|
+
* Initialize the L2 cache layer. Call once at server startup; the binding
|
|
23
|
+
* is process-wide and replaces any previously-set store.
|
|
24
|
+
*
|
|
25
|
+
* In browser bundles, never call this — L2 is opt-in and the cache will
|
|
26
|
+
* operate L1-only when uninitialized (correct, just slower).
|
|
27
|
+
*
|
|
28
|
+
* In multi-instance deployments (e.g. Cloud Run), L2 should be a shared,
|
|
29
|
+
* durable store (Redis, DynamoDB, …) so a warm cache from one instance
|
|
30
|
+
* serves every other instance. A reference Redis adapter is at
|
|
31
|
+
* `src/lib/cache/__tests__/redis-l2-store.ts`.
|
|
32
|
+
*
|
|
33
|
+
* @example Next.js instrumentation hook
|
|
34
|
+
* ```ts
|
|
35
|
+
* // instrumentation.ts
|
|
36
|
+
* export async function register() {
|
|
37
|
+
* if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
38
|
+
* const { initL2 } = await import("fansunited-data-layer");
|
|
39
|
+
* const { createRedisL2Store } = await import("./src/lib/cache/redisL2Store");
|
|
40
|
+
* const { getRedis } = await import("./src/lib/redis");
|
|
41
|
+
* initL2(createRedisL2Store({ client: getRedis() }));
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @see {@link L2Store} for the interface a custom store must satisfy.
|
|
24
46
|
*/
|
|
25
47
|
export declare function initL2(store: L2Store): void;
|
|
26
48
|
/** Get the L2 store (if initialized) */
|
|
27
49
|
export declare function getL2(): L2Store | null;
|
|
28
50
|
declare const ENTITY_TTL_CONFIG: Record<EntityType, EntityCacheConfig>;
|
|
29
51
|
/**
|
|
30
|
-
*
|
|
52
|
+
* Replace the TTL config for an entity type at runtime. The new values
|
|
53
|
+
* apply to subsequent `cached()` / `cachedBatch()` calls; existing L2
|
|
54
|
+
* entries keep whatever TTL they were written with.
|
|
55
|
+
*
|
|
56
|
+
* Useful for:
|
|
57
|
+
* - per-environment tuning (dev: short TTLs to see changes; prod: long)
|
|
58
|
+
* - overriding ahead of a known traffic spike
|
|
59
|
+
* - tests that need to simulate stale / expired entries in seconds
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* setEntityTTL("sportsCompetition", { staleTTL: 30, maxTTL: 600 });
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @see ../README.md § 7 for the full list of default TTLs.
|
|
67
|
+
*/
|
|
68
|
+
export declare function setEntityTTL(entity: EntityType, config: EntityCacheConfig): void;
|
|
69
|
+
/**
|
|
70
|
+
* Read the current TTL config for an entity type.
|
|
71
|
+
*
|
|
72
|
+
* Primarily for L2 adapters that translate `entity` into an underlying
|
|
73
|
+
* eviction TTL (e.g. Redis `EX` seconds = `maxTTL`). The returned object
|
|
74
|
+
* is a defensive copy — mutating it has no effect on the manager.
|
|
75
|
+
*
|
|
76
|
+
* @throws when called with an entity name not in the config.
|
|
77
|
+
*/
|
|
78
|
+
export declare function getEntityTTL(entity: EntityType): EntityCacheConfig;
|
|
79
|
+
/**
|
|
80
|
+
* Single-key cache-through with stale-while-revalidate.
|
|
81
|
+
*
|
|
82
|
+
* Flow:
|
|
83
|
+
* 1. L1 hit, fresh (< staleTTL) → return.
|
|
84
|
+
* 2. L1 hit, stale but valid (< maxTTL) → return + kick background refresh.
|
|
85
|
+
* 3. L1 miss, L2 hit, fresh → promote to L1 + return.
|
|
86
|
+
* 4. L1 miss, L2 hit, stale but valid → promote + return + background refresh.
|
|
87
|
+
* 5. Full miss → await `fetcher`, populate L1+L2, return.
|
|
88
|
+
*
|
|
89
|
+
* Background refreshes share a per-key in-flight set, so concurrent
|
|
90
|
+
* callers don't trigger a thundering herd of upstream fetches.
|
|
91
|
+
*
|
|
92
|
+
* The `key` must include the {@link CACHE_KEY_PREFIX} (`"fudl:"`) by
|
|
93
|
+
* convention so it can be SCANned safely in a shared Redis instance.
|
|
94
|
+
*
|
|
95
|
+
* @param key Fully-qualified cache key (e.g. `"fudl:sports:competition:fb:c:1:active"`).
|
|
96
|
+
* @param entity Entity type — drives the SWR window via `ENTITY_TTL_CONFIG`.
|
|
97
|
+
* @param fetcher Upstream call, invoked on miss or background refresh.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const data = await cached(
|
|
102
|
+
* `${CACHE_KEY_PREFIX}sports:competition:${id}:active`,
|
|
103
|
+
* "sportsCompetition",
|
|
104
|
+
* () => fetchFromSportsApi(id),
|
|
105
|
+
* );
|
|
106
|
+
* ```
|
|
31
107
|
*/
|
|
32
108
|
export declare function cached<T>(key: string, entity: EntityType, fetcher: () => Promise<T>): Promise<T>;
|
|
33
109
|
/**
|
|
34
|
-
*
|
|
110
|
+
* Multi-key cache-through with stale-while-revalidate.
|
|
111
|
+
*
|
|
112
|
+
* For each `id`, the cache is checked (L1, then L2) and only the *missing*
|
|
113
|
+
* IDs are passed to `batchFetcher` in a single upstream call. Stale-but-
|
|
114
|
+
* valid entries are returned immediately and refreshed in the background
|
|
115
|
+
* per-key, just like {@link cached}.
|
|
116
|
+
*
|
|
117
|
+
* Why a separate function from `cached`? An N-ID lookup is one upstream
|
|
118
|
+
* batch request, not N parallel single-key calls — which is the whole
|
|
119
|
+
* reason the Search API supports `?ids=…,…` in the first place.
|
|
120
|
+
*
|
|
121
|
+
* @param ids IDs to resolve, in request order.
|
|
122
|
+
* @param entity Entity type — drives the SWR window.
|
|
123
|
+
* @param batchFetcher Called with the subset of IDs not in cache.
|
|
124
|
+
* Must return a `Map<id, value>` (missing IDs okay).
|
|
125
|
+
* @param keyFn Builds a fully-qualified cache key from an ID.
|
|
126
|
+
* Convention: `(id) => \`${CACHE_KEY_PREFIX}entity:${id}\``.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const entities = await cachedBatch(
|
|
131
|
+
* ids,
|
|
132
|
+
* "search",
|
|
133
|
+
* async (missingIds) => {
|
|
134
|
+
* const resp = await searchApi.get({ ids: missingIds, limit: missingIds.length });
|
|
135
|
+
* return new Map(resp.data.map((e) => [e.id, e]));
|
|
136
|
+
* },
|
|
137
|
+
* (id) => `${CACHE_KEY_PREFIX}entity:${id}`,
|
|
138
|
+
* );
|
|
139
|
+
* ```
|
|
35
140
|
*/
|
|
36
141
|
export declare function cachedBatch<T>(ids: string[], entity: EntityType, batchFetcher: (missingIds: string[]) => Promise<Map<string, T>>, keyFn: (id: string) => string): Promise<Map<string, T>>;
|
|
37
142
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-manager.d.ts","sourceRoot":"","sources":["../../src/lib/cache/cache-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-manager.d.ts","sourceRoot":"","sources":["../../src/lib/cache/cache-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvF,2EAA2E;AAC3E,MAAM,WAAW,OAAO;IACpB,IAAI,aAAa,IAAI,OAAO,CAAC;IAC7B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC7D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACxE,OAAO,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAC3F,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACzE,KAAK,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;CAC/B;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAE3C;AAED,wCAAwC;AACxC,wBAAgB,KAAK,IAAI,OAAO,GAAG,IAAI,CAEtC;AAED,QAAA,MAAM,iBAAiB,EAAE,MAAM,CAAC,UAAU,EAAE,iBAAiB,CAS5D,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAEhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,iBAAiB,CAElE;AAwCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAoCtG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EAAE,EACb,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAC/D,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CA6DzB;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE5C;AAGD,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
package/cache/cache-manager.js
CHANGED
|
@@ -4,14 +4,21 @@ function initL2(store) {
|
|
|
4
4
|
l2 = store;
|
|
5
5
|
}
|
|
6
6
|
const ENTITY_TTL_CONFIG = {
|
|
7
|
-
competitions: { staleTTL:
|
|
8
|
-
teams: { staleTTL:
|
|
9
|
-
athletes: { staleTTL:
|
|
10
|
-
venues: { staleTTL:
|
|
11
|
-
countries: { staleTTL:
|
|
12
|
-
coaches: { staleTTL: 24 * 3600, maxTTL:
|
|
13
|
-
search: { staleTTL:
|
|
7
|
+
competitions: { staleTTL: 8 * 3600, maxTTL: 72 * 3600 },
|
|
8
|
+
teams: { staleTTL: 8 * 3600, maxTTL: 48 * 3600 },
|
|
9
|
+
athletes: { staleTTL: 8 * 3600, maxTTL: 48 * 3600 },
|
|
10
|
+
venues: { staleTTL: 7 * 24 * 3600, maxTTL: 14 * 24 * 3600 },
|
|
11
|
+
countries: { staleTTL: 7 * 24 * 3600, maxTTL: 14 * 24 * 3600 },
|
|
12
|
+
coaches: { staleTTL: 24 * 3600, maxTTL: 72 * 3600 },
|
|
13
|
+
search: { staleTTL: 8 * 3600, maxTTL: 2 * 24 * 3600 },
|
|
14
|
+
sportsCompetition: { staleTTL: 5 * 60, maxTTL: 3600 }
|
|
14
15
|
};
|
|
16
|
+
function setEntityTTL(entity, config) {
|
|
17
|
+
ENTITY_TTL_CONFIG[entity] = { ...config };
|
|
18
|
+
}
|
|
19
|
+
function getEntityTTL(entity) {
|
|
20
|
+
return { ...getConfig(entity) };
|
|
21
|
+
}
|
|
15
22
|
const refreshing = /* @__PURE__ */ new Set();
|
|
16
23
|
function getConfig(entity) {
|
|
17
24
|
const config = ENTITY_TTL_CONFIG[entity];
|
|
@@ -29,10 +36,10 @@ function isExpired(entry, maxTTL) {
|
|
|
29
36
|
function backgroundRefresh(key, entity, fetcher) {
|
|
30
37
|
if (refreshing.has(key)) return;
|
|
31
38
|
refreshing.add(key);
|
|
32
|
-
fetcher().then((data) => {
|
|
39
|
+
fetcher().then(async (data) => {
|
|
33
40
|
memoryStore.set(key, data);
|
|
34
41
|
if (l2?.isInitialized) {
|
|
35
|
-
l2.set(key, entity, data);
|
|
42
|
+
await l2.set(key, entity, data);
|
|
36
43
|
}
|
|
37
44
|
}).catch((err) => {
|
|
38
45
|
console.error(`[cache] Background refresh failed for ${key}:`, err);
|
|
@@ -53,7 +60,7 @@ async function cached(key, entity, fetcher) {
|
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
if (l2?.isInitialized) {
|
|
56
|
-
const l2Entry = l2.get(key);
|
|
63
|
+
const l2Entry = await l2.get(key);
|
|
57
64
|
if (l2Entry && !isExpired(l2Entry, maxTTL)) {
|
|
58
65
|
memoryStore.set(key, l2Entry.data);
|
|
59
66
|
if (isFresh(l2Entry, staleTTL)) {
|
|
@@ -66,7 +73,7 @@ async function cached(key, entity, fetcher) {
|
|
|
66
73
|
const data = await fetcher();
|
|
67
74
|
memoryStore.set(key, data);
|
|
68
75
|
if (l2?.isInitialized) {
|
|
69
|
-
l2.set(key, entity, data);
|
|
76
|
+
await l2.set(key, entity, data);
|
|
70
77
|
}
|
|
71
78
|
return data;
|
|
72
79
|
}
|
|
@@ -91,7 +98,7 @@ async function cachedBatch(ids, entity, batchFetcher, keyFn) {
|
|
|
91
98
|
found = true;
|
|
92
99
|
}
|
|
93
100
|
if (!found && l2?.isInitialized) {
|
|
94
|
-
const l2Entry = l2.get(key);
|
|
101
|
+
const l2Entry = await l2.get(key);
|
|
95
102
|
if (l2Entry && !isExpired(l2Entry, maxTTL)) {
|
|
96
103
|
memoryStore.set(key, l2Entry.data);
|
|
97
104
|
results.set(id, l2Entry.data);
|
|
@@ -116,7 +123,7 @@ async function cachedBatch(ids, entity, batchFetcher, keyFn) {
|
|
|
116
123
|
const key = keyFn(id);
|
|
117
124
|
memoryStore.set(key, data);
|
|
118
125
|
if (l2?.isInitialized) {
|
|
119
|
-
l2.set(key, entity, data);
|
|
126
|
+
await l2.set(key, entity, data);
|
|
120
127
|
}
|
|
121
128
|
results.set(id, data);
|
|
122
129
|
}
|
|
@@ -130,7 +137,9 @@ export {
|
|
|
130
137
|
ENTITY_TTL_CONFIG,
|
|
131
138
|
cached,
|
|
132
139
|
cachedBatch,
|
|
140
|
+
getEntityTTL,
|
|
133
141
|
initL2,
|
|
134
|
-
invalidate
|
|
142
|
+
invalidate,
|
|
143
|
+
setEntityTTL
|
|
135
144
|
};
|
|
136
145
|
//# sourceMappingURL=cache-manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-manager.js","sources":["../../src/lib/cache/cache-manager.ts"],"sourcesContent":["/**\n * Cache Manager - Orchestrates L1 (memory) + optional L2 with SWR pattern\n *\n * L2 (SQLite) is never statically imported here — it's injected at runtime\n * via initL2() so that browser bundles don't pull in better-sqlite3.\n */\n\nimport { memoryStore } from \"./memory-store\";\nimport type { CacheEntry, EntityCacheConfig, EntityType } from \"./types\";\n\n/** Interface that L2 stores must satisfy */\nexport interface L2Store {\n get isInitialized(): boolean;\n get<T>(key: string): CacheEntry<T> | undefined;\n set(key: string, entity: string, data: unknown): void;\n setMany(entries: { key: string; entity: string; data: unknown }[]): void;\n cleanup(entity: string, maxTTLSeconds: number): number;\n clear(): void;\n}\n\n/** The active L2 store — null until initL2() is called */\nlet l2: L2Store | null = null;\n\n/**\n * Initialize L2 cache layer. Call on server startup.\n * Client-side apps skip this — L2 is never loaded.\n */\nexport function initL2(store: L2Store): void {\n l2 = store;\n}\n\n/** Get the L2 store (if initialized) */\nexport function getL2(): L2Store | null {\n return l2;\n}\n\nconst ENTITY_TTL_CONFIG: Record<EntityType, EntityCacheConfig> = {\n competitions: { staleTTL: 24 * 3600, maxTTL: 72 * 3600 },\n teams: { staleTTL: 24 * 3600, maxTTL: 48 * 3600 },\n athletes: { staleTTL: 24 * 3600, maxTTL: 48 * 3600 },\n venues: { staleTTL: 72 * 3600, maxTTL: 168 * 3600 },\n countries: { staleTTL: 96 * 3600, maxTTL: 168 * 3600 },\n coaches: { staleTTL: 24 * 3600, maxTTL: 48 * 3600 },\n search: { staleTTL: 300, maxTTL: 1800 },\n};\n\n/** Set of keys currently being refreshed in the background (thundering herd prevention) */\nconst refreshing = new Set<string>();\n\nfunction getConfig(entity: EntityType): EntityCacheConfig {\n const config = ENTITY_TTL_CONFIG[entity];\n if (!config) {\n throw new Error(`Unknown entity type: ${entity}`);\n }\n return config;\n}\n\nfunction isFresh(entry: CacheEntry<unknown>, staleTTL: number): boolean {\n return Date.now() - entry.storedAt < staleTTL * 1000;\n}\n\nfunction isExpired(entry: CacheEntry<unknown>, maxTTL: number): boolean {\n return Date.now() - entry.storedAt >= maxTTL * 1000;\n}\n\nfunction backgroundRefresh<T>(key: string, entity: EntityType, fetcher: () => Promise<T>): void {\n if (refreshing.has(key)) return;\n refreshing.add(key);\n\n fetcher()\n .then((data) => {\n memoryStore.set(key, data);\n if (l2?.isInitialized) {\n l2.set(key, entity, data);\n }\n })\n .catch((err) => {\n console.error(`[cache] Background refresh failed for ${key}:`, err);\n })\n .finally(() => {\n refreshing.delete(key);\n });\n}\n\n/**\n * Single-key cache-through with SWR (Stale-While-Revalidate)\n */\nexport async function cached<T>(key: string, entity: EntityType, fetcher: () => Promise<T>): Promise<T> {\n const { staleTTL, maxTTL } = getConfig(entity);\n\n // L1 check\n const l1 = memoryStore.get<T>(key);\n if (l1) {\n if (isFresh(l1, staleTTL)) {\n return l1.data;\n }\n if (!isExpired(l1, maxTTL)) {\n backgroundRefresh(key, entity, fetcher);\n return l1.data;\n }\n }\n\n // L2 check\n if (l2?.isInitialized) {\n const l2Entry = l2.get<T>(key);\n if (l2Entry && !isExpired(l2Entry, maxTTL)) {\n // Promote to L1\n memoryStore.set(key, l2Entry.data);\n if (isFresh(l2Entry, staleTTL)) {\n return l2Entry.data;\n }\n backgroundRefresh(key, entity, fetcher);\n return l2Entry.data;\n }\n }\n\n // Full miss — await fetcher\n const data = await fetcher();\n memoryStore.set(key, data);\n if (l2?.isInitialized) {\n l2.set(key, entity, data);\n }\n return data;\n}\n\n/**\n * Smart batch cache — only fetches uncached IDs from the API\n */\nexport async function cachedBatch<T>(\n ids: string[],\n entity: EntityType,\n batchFetcher: (missingIds: string[]) => Promise<Map<string, T>>,\n keyFn: (id: string) => string\n): Promise<Map<string, T>> {\n const { staleTTL, maxTTL } = getConfig(entity);\n const results = new Map<string, T>();\n const missingIds: string[] = [];\n\n for (const id of ids) {\n const key = keyFn(id);\n let found = false;\n\n // L1 check\n const l1 = memoryStore.get<T>(key);\n if (l1 && !isExpired(l1, maxTTL)) {\n results.set(id, l1.data);\n if (!isFresh(l1, staleTTL)) {\n backgroundRefresh(key, entity, async () => {\n const fetched = await batchFetcher([id]);\n const item = fetched.get(id);\n if (!item) throw new Error(`Batch fetcher did not return ID: ${id}`);\n return item;\n });\n }\n found = true;\n }\n\n // L2 check\n if (!found && l2?.isInitialized) {\n const l2Entry = l2.get<T>(key);\n if (l2Entry && !isExpired(l2Entry, maxTTL)) {\n memoryStore.set(key, l2Entry.data);\n results.set(id, l2Entry.data);\n if (!isFresh(l2Entry, staleTTL)) {\n backgroundRefresh(key, entity, async () => {\n const fetched = await batchFetcher([id]);\n const item = fetched.get(id);\n if (!item) throw new Error(`Batch fetcher did not return ID: ${id}`);\n return item;\n });\n }\n found = true;\n }\n }\n\n if (!found) {\n missingIds.push(id);\n }\n }\n\n // Fetch missing IDs in a single batch\n if (missingIds.length > 0) {\n const fetched = await batchFetcher(missingIds);\n for (const [id, data] of fetched) {\n const key = keyFn(id);\n memoryStore.set(key, data);\n if (l2?.isInitialized) {\n l2.set(key, entity, data);\n }\n results.set(id, data);\n }\n }\n\n return results;\n}\n\n/**\n * Remove an entry from both cache layers\n */\nexport function invalidate(key: string): void {\n memoryStore.delete(key);\n}\n\n// Export for testing\nexport { ENTITY_TTL_CONFIG };\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cache-manager.js","sources":["../../src/lib/cache/cache-manager.ts"],"sourcesContent":["/**\n * Cache Manager - Orchestrates L1 (memory) + optional L2 with SWR pattern\n *\n * L2 (SQLite) is never statically imported here — it's injected at runtime\n * via initL2() so that browser bundles don't pull in better-sqlite3.\n */\n\nimport { memoryStore } from \"./memory-store\";\nimport type { CacheEntry, EntityCacheConfig, EntityType, MaybePromise } from \"./types\";\n\n/** Interface that L2 stores must satisfy. Methods may be sync or async. */\nexport interface L2Store {\n get isInitialized(): boolean;\n get<T>(key: string): MaybePromise<CacheEntry<T> | undefined>;\n set(key: string, entity: EntityType, data: unknown): MaybePromise<void>;\n setMany(entries: { key: string; entity: EntityType; data: unknown }[]): MaybePromise<void>;\n cleanup(entity: EntityType, maxTTLSeconds: number): MaybePromise<number>;\n clear(): MaybePromise<void>;\n}\n\n/** The active L2 store — null until initL2() is called */\nlet l2: L2Store | null = null;\n\n/**\n * Initialize the L2 cache layer. Call once at server startup; the binding\n * is process-wide and replaces any previously-set store.\n *\n * In browser bundles, never call this — L2 is opt-in and the cache will\n * operate L1-only when uninitialized (correct, just slower).\n *\n * In multi-instance deployments (e.g. Cloud Run), L2 should be a shared,\n * durable store (Redis, DynamoDB, …) so a warm cache from one instance\n * serves every other instance. A reference Redis adapter is at\n * `src/lib/cache/__tests__/redis-l2-store.ts`.\n *\n * @example Next.js instrumentation hook\n * ```ts\n * // instrumentation.ts\n * export async function register() {\n * if (process.env.NEXT_RUNTIME !== \"nodejs\") return;\n * const { initL2 } = await import(\"fansunited-data-layer\");\n * const { createRedisL2Store } = await import(\"./src/lib/cache/redisL2Store\");\n * const { getRedis } = await import(\"./src/lib/redis\");\n * initL2(createRedisL2Store({ client: getRedis() }));\n * }\n * ```\n *\n * @see {@link L2Store} for the interface a custom store must satisfy.\n */\nexport function initL2(store: L2Store): void {\n l2 = store;\n}\n\n/** Get the L2 store (if initialized) */\nexport function getL2(): L2Store | null {\n return l2;\n}\n\nconst ENTITY_TTL_CONFIG: Record<EntityType, EntityCacheConfig> = {\n competitions: { staleTTL: 8 * 3600, maxTTL: 72 * 3600 },\n teams: { staleTTL: 8 * 3600, maxTTL: 48 * 3600 },\n athletes: { staleTTL: 8 * 3600, maxTTL: 48 * 3600 },\n venues: { staleTTL: 7 * 24 * 3600, maxTTL: 14 * 24 * 3600 },\n countries: { staleTTL: 7 * 24 * 3600, maxTTL: 14 * 24 * 3600 },\n coaches: { staleTTL: 24 * 3600, maxTTL: 72 * 3600 },\n search: { staleTTL: 8 * 3600, maxTTL: 2 * 24 * 3600 },\n sportsCompetition: { staleTTL: 5 * 60, maxTTL: 3600 },\n};\n\n/**\n * Replace the TTL config for an entity type at runtime. The new values\n * apply to subsequent `cached()` / `cachedBatch()` calls; existing L2\n * entries keep whatever TTL they were written with.\n *\n * Useful for:\n * - per-environment tuning (dev: short TTLs to see changes; prod: long)\n * - overriding ahead of a known traffic spike\n * - tests that need to simulate stale / expired entries in seconds\n *\n * @example\n * ```ts\n * setEntityTTL(\"sportsCompetition\", { staleTTL: 30, maxTTL: 600 });\n * ```\n *\n * @see ../README.md § 7 for the full list of default TTLs.\n */\nexport function setEntityTTL(entity: EntityType, config: EntityCacheConfig): void {\n ENTITY_TTL_CONFIG[entity] = { ...config };\n}\n\n/**\n * Read the current TTL config for an entity type.\n *\n * Primarily for L2 adapters that translate `entity` into an underlying\n * eviction TTL (e.g. Redis `EX` seconds = `maxTTL`). The returned object\n * is a defensive copy — mutating it has no effect on the manager.\n *\n * @throws when called with an entity name not in the config.\n */\nexport function getEntityTTL(entity: EntityType): EntityCacheConfig {\n return { ...getConfig(entity) };\n}\n\n/** Set of keys currently being refreshed in the background (thundering herd prevention) */\nconst refreshing = new Set<string>();\n\nfunction getConfig(entity: EntityType): EntityCacheConfig {\n const config = ENTITY_TTL_CONFIG[entity];\n if (!config) {\n throw new Error(`Unknown entity type: ${entity}`);\n }\n return config;\n}\n\nfunction isFresh(entry: CacheEntry<unknown>, staleTTL: number): boolean {\n return Date.now() - entry.storedAt < staleTTL * 1000;\n}\n\nfunction isExpired(entry: CacheEntry<unknown>, maxTTL: number): boolean {\n return Date.now() - entry.storedAt >= maxTTL * 1000;\n}\n\nfunction backgroundRefresh<T>(key: string, entity: EntityType, fetcher: () => Promise<T>): void {\n if (refreshing.has(key)) return;\n refreshing.add(key);\n\n fetcher()\n .then(async (data) => {\n memoryStore.set(key, data);\n if (l2?.isInitialized) {\n await l2.set(key, entity, data);\n }\n })\n .catch((err) => {\n console.error(`[cache] Background refresh failed for ${key}:`, err);\n })\n .finally(() => {\n refreshing.delete(key);\n });\n}\n\n/**\n * Single-key cache-through with stale-while-revalidate.\n *\n * Flow:\n * 1. L1 hit, fresh (< staleTTL) → return.\n * 2. L1 hit, stale but valid (< maxTTL) → return + kick background refresh.\n * 3. L1 miss, L2 hit, fresh → promote to L1 + return.\n * 4. L1 miss, L2 hit, stale but valid → promote + return + background refresh.\n * 5. Full miss → await `fetcher`, populate L1+L2, return.\n *\n * Background refreshes share a per-key in-flight set, so concurrent\n * callers don't trigger a thundering herd of upstream fetches.\n *\n * The `key` must include the {@link CACHE_KEY_PREFIX} (`\"fudl:\"`) by\n * convention so it can be SCANned safely in a shared Redis instance.\n *\n * @param key Fully-qualified cache key (e.g. `\"fudl:sports:competition:fb:c:1:active\"`).\n * @param entity Entity type — drives the SWR window via `ENTITY_TTL_CONFIG`.\n * @param fetcher Upstream call, invoked on miss or background refresh.\n *\n * @example\n * ```ts\n * const data = await cached(\n * `${CACHE_KEY_PREFIX}sports:competition:${id}:active`,\n * \"sportsCompetition\",\n * () => fetchFromSportsApi(id),\n * );\n * ```\n */\nexport async function cached<T>(key: string, entity: EntityType, fetcher: () => Promise<T>): Promise<T> {\n const { staleTTL, maxTTL } = getConfig(entity);\n\n // L1 check\n const l1 = memoryStore.get<T>(key);\n if (l1) {\n if (isFresh(l1, staleTTL)) {\n return l1.data;\n }\n if (!isExpired(l1, maxTTL)) {\n backgroundRefresh(key, entity, fetcher);\n return l1.data;\n }\n }\n\n // L2 check\n if (l2?.isInitialized) {\n const l2Entry = await l2.get<T>(key);\n if (l2Entry && !isExpired(l2Entry, maxTTL)) {\n // Promote to L1\n memoryStore.set(key, l2Entry.data);\n if (isFresh(l2Entry, staleTTL)) {\n return l2Entry.data;\n }\n backgroundRefresh(key, entity, fetcher);\n return l2Entry.data;\n }\n }\n\n // Full miss — await fetcher\n const data = await fetcher();\n memoryStore.set(key, data);\n if (l2?.isInitialized) {\n await l2.set(key, entity, data);\n }\n return data;\n}\n\n/**\n * Multi-key cache-through with stale-while-revalidate.\n *\n * For each `id`, the cache is checked (L1, then L2) and only the *missing*\n * IDs are passed to `batchFetcher` in a single upstream call. Stale-but-\n * valid entries are returned immediately and refreshed in the background\n * per-key, just like {@link cached}.\n *\n * Why a separate function from `cached`? An N-ID lookup is one upstream\n * batch request, not N parallel single-key calls — which is the whole\n * reason the Search API supports `?ids=…,…` in the first place.\n *\n * @param ids IDs to resolve, in request order.\n * @param entity Entity type — drives the SWR window.\n * @param batchFetcher Called with the subset of IDs not in cache.\n * Must return a `Map<id, value>` (missing IDs okay).\n * @param keyFn Builds a fully-qualified cache key from an ID.\n * Convention: `(id) => \\`${CACHE_KEY_PREFIX}entity:${id}\\``.\n *\n * @example\n * ```ts\n * const entities = await cachedBatch(\n * ids,\n * \"search\",\n * async (missingIds) => {\n * const resp = await searchApi.get({ ids: missingIds, limit: missingIds.length });\n * return new Map(resp.data.map((e) => [e.id, e]));\n * },\n * (id) => `${CACHE_KEY_PREFIX}entity:${id}`,\n * );\n * ```\n */\nexport async function cachedBatch<T>(\n ids: string[],\n entity: EntityType,\n batchFetcher: (missingIds: string[]) => Promise<Map<string, T>>,\n keyFn: (id: string) => string\n): Promise<Map<string, T>> {\n const { staleTTL, maxTTL } = getConfig(entity);\n const results = new Map<string, T>();\n const missingIds: string[] = [];\n\n for (const id of ids) {\n const key = keyFn(id);\n let found = false;\n\n // L1 check\n const l1 = memoryStore.get<T>(key);\n if (l1 && !isExpired(l1, maxTTL)) {\n results.set(id, l1.data);\n if (!isFresh(l1, staleTTL)) {\n backgroundRefresh(key, entity, async () => {\n const fetched = await batchFetcher([id]);\n const item = fetched.get(id);\n if (!item) throw new Error(`Batch fetcher did not return ID: ${id}`);\n return item;\n });\n }\n found = true;\n }\n\n // L2 check\n if (!found && l2?.isInitialized) {\n const l2Entry = await l2.get<T>(key);\n if (l2Entry && !isExpired(l2Entry, maxTTL)) {\n memoryStore.set(key, l2Entry.data);\n results.set(id, l2Entry.data);\n if (!isFresh(l2Entry, staleTTL)) {\n backgroundRefresh(key, entity, async () => {\n const fetched = await batchFetcher([id]);\n const item = fetched.get(id);\n if (!item) throw new Error(`Batch fetcher did not return ID: ${id}`);\n return item;\n });\n }\n found = true;\n }\n }\n\n if (!found) {\n missingIds.push(id);\n }\n }\n\n // Fetch missing IDs in a single batch\n if (missingIds.length > 0) {\n const fetched = await batchFetcher(missingIds);\n for (const [id, data] of fetched) {\n const key = keyFn(id);\n memoryStore.set(key, data);\n if (l2?.isInitialized) {\n await l2.set(key, entity, data);\n }\n results.set(id, data);\n }\n }\n\n return results;\n}\n\n/**\n * Remove an entry from both cache layers\n */\nexport function invalidate(key: string): void {\n memoryStore.delete(key);\n}\n\n// Export for testing\nexport { ENTITY_TTL_CONFIG };\n"],"names":[],"mappings":"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"}
|
package/cache/cleanup.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Periodic L2 cache cleanup
|
|
3
3
|
*/
|
|
4
|
-
export declare function runCleanup(): void
|
|
4
|
+
export declare function runCleanup(): Promise<void>;
|
|
5
5
|
export declare function startCleanupSchedule(intervalMs?: number): void;
|
|
6
6
|
export declare function stopCleanupSchedule(): void;
|
|
7
7
|
//# sourceMappingURL=cleanup.d.ts.map
|
package/cache/cleanup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../src/lib/cache/cleanup.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../src/lib/cache/cleanup.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAWhD;AAED,wBAAgB,oBAAoB,CAAC,UAAU,SAAiB,GAAG,IAAI,CAGtE;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C"}
|
package/cache/index.d.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* import { initSqliteStore, sqliteStore } from './cache/sqlite-store';
|
|
7
7
|
*/
|
|
8
8
|
export { memoryStore } from "./memory-store";
|
|
9
|
-
export { cached, cachedBatch, invalidate, initL2 } from "./cache-manager";
|
|
9
|
+
export { cached, cachedBatch, invalidate, initL2, setEntityTTL, getEntityTTL } from "./cache-manager";
|
|
10
10
|
export type { L2Store } from "./cache-manager";
|
|
11
11
|
export { runCleanup, startCleanupSchedule, stopCleanupSchedule } from "./cleanup";
|
|
12
|
-
export type { CacheEntry, EntityCacheConfig, EntityType } from "./types";
|
|
12
|
+
export type { CacheEntry, EntityCacheConfig, EntityType, MaybePromise } from "./types";
|
|
13
|
+
export { CACHE_KEY_PREFIX } from "./types";
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
package/cache/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/cache/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/cache/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACtG,YAAY,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAClF,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvF,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC"}
|
package/cache/sqlite-store.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* L2 Cache - SQLite on-disk store (server-only)
|
|
3
3
|
*/
|
|
4
|
-
import type { CacheEntry } from "./types";
|
|
4
|
+
import type { CacheEntry, EntityType } from "./types";
|
|
5
5
|
export declare function initSqliteStore(dbPath?: string): void;
|
|
6
6
|
export declare const sqliteStore: {
|
|
7
7
|
readonly isInitialized: boolean;
|
|
8
8
|
get<T>(key: string): CacheEntry<T> | undefined;
|
|
9
|
-
set(key: string, entity:
|
|
9
|
+
set(key: string, entity: EntityType, data: unknown): void;
|
|
10
10
|
setMany(entries: {
|
|
11
11
|
key: string;
|
|
12
|
-
entity:
|
|
12
|
+
entity: EntityType;
|
|
13
13
|
data: unknown;
|
|
14
14
|
}[]): void;
|
|
15
|
-
cleanup(entity:
|
|
15
|
+
cleanup(entity: EntityType, maxTTLSeconds: number): number;
|
|
16
16
|
clear(): void;
|
|
17
17
|
close(): void;
|
|
18
18
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/lib/cache/sqlite-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/lib/cache/sqlite-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAWtD,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CA4BrD;AAED,eAAO,MAAM,WAAW;4BACC,OAAO;QAIxB,CAAC,OAAO,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,SAAS;aAarC,MAAM,UAAU,UAAU,QAAQ,OAAO,GAAG,IAAI;qBASxC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EAAE,GAAG,IAAI;oBAgB5D,UAAU,iBAAiB,MAAM,GAAG,MAAM;aAQjD,IAAI;aAIJ,IAAI;CAMhB,CAAC"}
|
package/cache/types.d.ts
CHANGED
|
@@ -11,5 +11,13 @@ export interface EntityCacheConfig {
|
|
|
11
11
|
/** Time in seconds before data is completely expired and must be re-fetched */
|
|
12
12
|
maxTTL: number;
|
|
13
13
|
}
|
|
14
|
-
export type EntityType = "competitions" | "teams" | "athletes" | "venues" | "countries" | "coaches" | "search";
|
|
14
|
+
export type EntityType = "competitions" | "teams" | "athletes" | "venues" | "countries" | "coaches" | "search" | "sportsCompetition";
|
|
15
|
+
/** Convenience: a value that may or may not be wrapped in a Promise. */
|
|
16
|
+
export type MaybePromise<T> = T | Promise<T>;
|
|
17
|
+
/**
|
|
18
|
+
* Prefix for every cache key emitted by this library.
|
|
19
|
+
* Namespaces the data-layer's entries inside a shared store (Redis SCAN by `fudl:*`).
|
|
20
|
+
* Bump the literal (e.g. `"fudl:v2:"`) to invalidate every cached entry across deploys.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CACHE_KEY_PREFIX = "fudl:";
|
|
15
23
|
//# sourceMappingURL=types.d.ts.map
|
package/cache/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/cache/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU,CAAC,CAAC;IACzB,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/cache/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU,CAAC,CAAC;IACzB,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAChB,cAAc,GACd,OAAO,GACP,UAAU,GACV,QAAQ,GACR,WAAW,GACX,SAAS,GACT,QAAQ,GACR,mBAAmB,CAAC;AAE1B,wEAAwE;AACxE,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,UAAU,CAAC"}
|
package/cache/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sources":["../../src/lib/cache/types.ts"],"sourcesContent":["/**\n * Shared cache types\n */\n\nexport interface CacheEntry<T> {\n data: T;\n storedAt: number; // Unix timestamp in ms\n}\n\nexport interface EntityCacheConfig {\n /** Time in seconds before data is considered stale (triggers background refresh) */\n staleTTL: number;\n /** Time in seconds before data is completely expired and must be re-fetched */\n maxTTL: number;\n}\n\nexport type EntityType =\n | \"competitions\"\n | \"teams\"\n | \"athletes\"\n | \"venues\"\n | \"countries\"\n | \"coaches\"\n | \"search\"\n | \"sportsCompetition\";\n\n/** Convenience: a value that may or may not be wrapped in a Promise. */\nexport type MaybePromise<T> = T | Promise<T>;\n\n/**\n * Prefix for every cache key emitted by this library.\n * Namespaces the data-layer's entries inside a shared store (Redis SCAN by `fudl:*`).\n * Bump the literal (e.g. `\"fudl:v2:\"`) to invalidate every cached entry across deploys.\n */\nexport const CACHE_KEY_PREFIX = \"fudl:\";\n"],"names":[],"mappings":"AAkCO,MAAM,mBAAmB;"}
|
package/fansunited-data-layer.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getConfig, isConfigured, setConfig } from "./config/index.js";
|
|
2
2
|
import { memoryStore } from "./cache/memory-store.js";
|
|
3
|
+
import { CACHE_KEY_PREFIX } from "./cache/types.js";
|
|
3
4
|
import { GOAL_EVENT_TYPES } from "./utilities/stats/core/types.js";
|
|
4
5
|
import { addProviderRef, getProviderRefId, toProviderRefArray } from "./api/sportal365-sports/shared/providerRef.helper.js";
|
|
5
6
|
import { aggregatePlayerStatisticsFromMatches, getAllPlayerStatisticsFromMatches, getMostCardedPlayersFromMatches, getTopAssistersFromMatches, getTopGoalContributorsFromMatches, getTopScorersFromMatches } from "./helpers/player.helpers.js";
|
|
6
7
|
import { analyzeMatch, calcAverage, calcPercentage, formatAsAverage, formatPossessionPercentage, formatStatValue, formatWithAverage, getOpponentScore, getTeamScore, isMatchFinished, isTeamAway, isTeamHome, isTeamInMatch } from "./utilities/stats/core/helpers.js";
|
|
7
|
-
import { cached, cachedBatch, initL2, invalidate } from "./cache/cache-manager.js";
|
|
8
|
+
import { cached, cachedBatch, getEntityTTL, initL2, invalidate, setEntityTTL } from "./cache/cache-manager.js";
|
|
8
9
|
import { calculateH2HStats, getRecentH2HMeetings } from "./utilities/stats/match/headToHead.js";
|
|
9
10
|
import { calculateStreak, getTeamStreaks, getTeamStreaksComparison, streakFilters } from "./utilities/stats/team/streaks.js";
|
|
10
11
|
import { getActiveSeason } from "./helpers/competition.helpers.js";
|
|
@@ -17,6 +18,10 @@ import { getFansUnitedFootballMatch, getFansUnitedFootballMatches, getFansUnited
|
|
|
17
18
|
import { getFansUnitedFootballPlayer, getFansUnitedFootballPlayers } from "./api/fansunited/football/players/index.js";
|
|
18
19
|
import { getFansUnitedFootballSearch } from "./api/fansunited/football/search/index.js";
|
|
19
20
|
import { getFansUnitedFootballTeam, getFansUnitedFootballTeams } from "./api/fansunited/football/teams/index.js";
|
|
21
|
+
import { getFansUnitedSportsCompetition } from "./api/fansunited/sports/competition/index.js";
|
|
22
|
+
import { getFansUnitedSportsCompetitionHydrated } from "./api/fansunited/sports/competition/hydrated.js";
|
|
23
|
+
import { getFansUnitedSportsLivescore } from "./api/fansunited/sports/livescore/index.js";
|
|
24
|
+
import { getFansUnitedSportsLivescoreHydrated } from "./api/fansunited/sports/livescore/hydrated.js";
|
|
20
25
|
import { getFootballCompetition, getFootballSeasonDetails } from "./api/sportal365-sports/football/competitions/index.js";
|
|
21
26
|
import { getFootballLivescore, getFootballMatch, getFootballMatchCommentary, getFootballMatchEvents, getFootballMatchLineups, getFootballMatchOdds, getFootballMatchStatistics, getFootballMatches } from "./api/sportal365-sports/football/matches/index.js";
|
|
22
27
|
import { getFootballPlayerCareerStatistics, getFootballPlayerRecentStatistics, getFootballPlayerSeasonStatistics, getFootballPlayerSeasonStatisticsByPlayerIds } from "./api/sportal365-sports/football/statistics/index.js";
|
|
@@ -37,6 +42,7 @@ import { search } from "./api/sportal365-sports/search/index.js";
|
|
|
37
42
|
import { searchFootball } from "./api/sportal365-sports/football/search/index.js";
|
|
38
43
|
import { useAllPlayerStatisticsFromMatches, useMostCardedPlayersFromMatches, usePlayerStatistics, usePlayerStatisticsMap, useTopAssistersFromMatches, useTopGoalContributorsFromMatches, useTopScorersFromMatches } from "./helpers/player.hooks.js";
|
|
39
44
|
export {
|
|
45
|
+
CACHE_KEY_PREFIX,
|
|
40
46
|
GOAL_EVENT_TYPES,
|
|
41
47
|
addProviderRef,
|
|
42
48
|
aggregatePlayerStatisticsFromMatches,
|
|
@@ -56,6 +62,7 @@ export {
|
|
|
56
62
|
getBasketballLivescore,
|
|
57
63
|
getBatchMatchOdds,
|
|
58
64
|
getConfig,
|
|
65
|
+
getEntityTTL,
|
|
59
66
|
getFansUnitedCompetitions,
|
|
60
67
|
getFansUnitedCountries,
|
|
61
68
|
getFansUnitedEntitiesByIds,
|
|
@@ -74,6 +81,10 @@ export {
|
|
|
74
81
|
getFansUnitedFootballTeamPreviousMatch,
|
|
75
82
|
getFansUnitedFootballTeams,
|
|
76
83
|
getFansUnitedSearchEntities,
|
|
84
|
+
getFansUnitedSportsCompetition,
|
|
85
|
+
getFansUnitedSportsCompetitionHydrated,
|
|
86
|
+
getFansUnitedSportsLivescore,
|
|
87
|
+
getFansUnitedSportsLivescoreHydrated,
|
|
77
88
|
getFansUnitedTeamsByCompetition,
|
|
78
89
|
getFansUnitedTeamsByCountry,
|
|
79
90
|
getFansUnitedVenues,
|
|
@@ -137,6 +148,7 @@ export {
|
|
|
137
148
|
search,
|
|
138
149
|
searchFootball,
|
|
139
150
|
setConfig,
|
|
151
|
+
setEntityTTL,
|
|
140
152
|
streakFilters,
|
|
141
153
|
toProviderRefArray,
|
|
142
154
|
useAllPlayerStatisticsFromMatches,
|