gerdur-core 2.13.3 → 2.14.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/CHANGELOG.md +34 -0
- package/README.md +35 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/lib/caches.d.ts +59 -0
- package/dist/lib/caches.js +100 -0
- package/dist/lib/decrypt.js +3 -0
- package/dist/lib/session.d.ts +8 -1
- package/dist/lib/session.js +46 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.14.0 - 2026-08-31
|
|
4
|
+
|
|
5
|
+
Multi-tenant caching — for backends where many accounts share one process.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Shared gateway metadata cache.** Most gw payloads embed a per-account
|
|
10
|
+
`TRACK_TOKEN`, so each `Session` keeps its own cache. Five methods carry
|
|
11
|
+
nothing account-scoped — `album.getData`, `artist.getData`, `song.getLyrics`,
|
|
12
|
+
`album.getDiscography`, `playlist.getData` — and now go to a process-wide cache
|
|
13
|
+
**partitioned by country**, with a shared in-flight map so a concurrent burst
|
|
14
|
+
collapses into one request. Measured with 500 sessions reading the same album:
|
|
15
|
+
**500 gateway requests → 1**, and 1.8 MB of duplicated payload → one copy.
|
|
16
|
+
Track-bearing methods (`song.getData`, `playlist.getSongs`,
|
|
17
|
+
`song.getListByAlbum`, `song.getListData`, `episode.getData`, `mobile.*`,
|
|
18
|
+
`deezer.pageSearch`, `user_getInfo`, …) are never shared.
|
|
19
|
+
- **`configureCache({shared: {maxSize, ttl}})`** — size the shared cache to your
|
|
20
|
+
catalogue. Default `{maxSize: 2000, ttl: 3_600_000}`.
|
|
21
|
+
- **`cacheStats()`** → `{shared: {size, maxSize, hits, misses, inFlight}}` — for
|
|
22
|
+
a `/metrics` or health endpoint.
|
|
23
|
+
- **`clearSharedCaches()`** — drops the shared cache; per-session caches are
|
|
24
|
+
untouched.
|
|
25
|
+
- `__tests__/caches.ts` — offline tests pinning the isolation guarantees: no
|
|
26
|
+
cross-account token reuse, no cross-country bleed, in-flight entries released.
|
|
27
|
+
- README: a **Running this on a server** section (stream don't buffer, decrypt is
|
|
28
|
+
on the event loop, size the cache, evict idle sessions).
|
|
29
|
+
|
|
30
|
+
### Not done, deliberately
|
|
31
|
+
|
|
32
|
+
- Memoising the initialised Blowfish key schedule per track was implemented,
|
|
33
|
+
measured and **reverted**: key setup is 39.7 µs against 33 ms to decrypt an
|
|
34
|
+
8 MiB file — **0.12%**, with break-even at ~10 KiB decrypted per key. Not worth
|
|
35
|
+
a cache or the public knob it would need.
|
|
36
|
+
|
|
3
37
|
## 2.13.3 - 2026-08-31
|
|
4
38
|
|
|
5
39
|
### Docs
|
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ the CLI and the file-writing layer on top.
|
|
|
55
55
|
- [Tag MP3 / FLAC](#tag-mp3--flac)
|
|
56
56
|
- [Enrichment (MusicBrainz + Cover Art Archive)](#enrichment)
|
|
57
57
|
- [Use multiple accounts](#use-multiple-accounts)
|
|
58
|
+
- [Running this on a server](#running-this-on-a-server)
|
|
58
59
|
- [HTTP helpers](#http-helpers)
|
|
59
60
|
- [Errors](#errors)
|
|
60
61
|
- [Types](#types)
|
|
@@ -604,6 +605,38 @@ and the raw channels (`gw`, `gwLight`, `gwGet`).
|
|
|
604
605
|
in tests). The free `getTrackDownloadUrl` / `resolveDownloadUrls` /
|
|
605
606
|
`streamTrackDownload` / `refreshTrackTokens` all take an optional `session`.
|
|
606
607
|
|
|
608
|
+
**Caching across sessions.** Most gw payloads embed a per-account `TRACK_TOKEN`,
|
|
609
|
+
so each `Session` has its own response cache. Five methods carry nothing
|
|
610
|
+
account-scoped — `album.getData`, `artist.getData`, `song.getLyrics`,
|
|
611
|
+
`album.getDiscography`, `playlist.getData` — and those go to a **process-wide
|
|
612
|
+
cache partitioned by country**, so one copy serves every session and a
|
|
613
|
+
concurrent burst collapses into one request. Measured with 500 sessions reading
|
|
614
|
+
the same album: **500 gateway requests → 1**, and 1.8 MB of duplicated payload →
|
|
615
|
+
one copy. Track lists (`song.getData`, `playlist.getSongs`, `song.getListByAlbum`,
|
|
616
|
+
`episode.getData`, …) are never shared.
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
import {configureCache, cacheStats, clearSharedCaches} from 'gerdur-core';
|
|
620
|
+
|
|
621
|
+
configureCache({shared: {maxSize: 20_000, ttl: 30 * 60_000}}); // once, at startup
|
|
622
|
+
cacheStats(); // {shared: {size, maxSize, hits, misses, inFlight}} — for /metrics
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Running this on a server
|
|
626
|
+
|
|
627
|
+
- **Stream, don't buffer.** `downloadTrackBuffer` / `getTrackBuffer` hold the
|
|
628
|
+
whole file (and `Buffer.concat` doubles it briefly) — fine for a script, a
|
|
629
|
+
memory bomb at concurrency. Use `streamTrackDownload` in a request path.
|
|
630
|
+
- **Decryption runs on the event loop.** Blowfish costs ~33 ms per 8 MiB
|
|
631
|
+
(~243 MiB/s), so heavy concurrent traffic will compete with everything else in
|
|
632
|
+
the process. Put the decrypt in a worker if you saturate a core.
|
|
633
|
+
- **Size the shared cache** to your catalogue with `configureCache`, and export
|
|
634
|
+
`cacheStats()` so you can see the hit rate.
|
|
635
|
+
- **Evict idle sessions yourself.** `createSession` has no lifecycle — a session
|
|
636
|
+
per user, kept forever, keeps its cache forever.
|
|
637
|
+
- `httpAgent` / `httpsAgent` are process-global (`maxSockets: 64`) and shared
|
|
638
|
+
between API calls and CDN downloads.
|
|
639
|
+
|
|
607
640
|
### HTTP helpers
|
|
608
641
|
|
|
609
642
|
The zero-dependency HTTP client (`get`/`post`/`head`, redirects, gzip/br/deflate,
|
|
@@ -759,7 +792,8 @@ import type {
|
|
|
759
792
|
<details>
|
|
760
793
|
<summary><b>HTTP & errors</b></summary>
|
|
761
794
|
|
|
762
|
-
`
|
|
795
|
+
`configureCache` · `cacheStats` · `clearSharedCaches` · `getJson` · `getText` ·
|
|
796
|
+
`getBuffer` · `getStream` · `HttpClient` · `httpAgent` ·
|
|
763
797
|
`httpsAgent` · `HttpStatusError` · `DeezerError` · `GeoBlocked` · `WrongLicense`
|
|
764
798
|
· `ExpiredTrackToken`
|
|
765
799
|
</details>
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { initDeezerApi, createSession, defaultSession, Session, RETRY_POLICY, DE
|
|
|
3
3
|
export type { SessionUserData } from './lib/session';
|
|
4
4
|
export { DeezerError } from './lib/errors';
|
|
5
5
|
export type { DeezerErrorPayload } from './lib/errors';
|
|
6
|
+
export { configureCache, cacheStats, clearSharedCaches } from './lib/caches';
|
|
7
|
+
export type { CacheOptions, CacheStats } from './lib/caches';
|
|
6
8
|
export * from './api';
|
|
7
9
|
export * from './converter';
|
|
8
10
|
export * from './lib/decrypt';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.getStream = exports.getText = exports.getJson = exports.getBuffer = exports.httpsAgent = exports.httpAgent = exports.DeezerError = exports.DEFAULT_ARL = exports.RETRY_POLICY = exports.Session = exports.defaultSession = exports.createSession = exports.initDeezerApi = void 0;
|
|
17
|
+
exports.getStream = exports.getText = exports.getJson = exports.getBuffer = exports.httpsAgent = exports.httpAgent = exports.clearSharedCaches = exports.cacheStats = exports.configureCache = exports.DeezerError = exports.DEFAULT_ARL = exports.RETRY_POLICY = exports.Session = exports.defaultSession = exports.createSession = exports.initDeezerApi = void 0;
|
|
18
18
|
require("./lib/session-augment"); // wires the download methods onto Session.prototype
|
|
19
19
|
var session_1 = require("./lib/session");
|
|
20
20
|
Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return session_1.initDeezerApi; } });
|
|
@@ -25,6 +25,10 @@ Object.defineProperty(exports, "RETRY_POLICY", { enumerable: true, get: function
|
|
|
25
25
|
Object.defineProperty(exports, "DEFAULT_ARL", { enumerable: true, get: function () { return session_1.DEFAULT_ARL; } });
|
|
26
26
|
var errors_1 = require("./lib/errors");
|
|
27
27
|
Object.defineProperty(exports, "DeezerError", { enumerable: true, get: function () { return errors_1.DeezerError; } });
|
|
28
|
+
var caches_1 = require("./lib/caches");
|
|
29
|
+
Object.defineProperty(exports, "configureCache", { enumerable: true, get: function () { return caches_1.configureCache; } });
|
|
30
|
+
Object.defineProperty(exports, "cacheStats", { enumerable: true, get: function () { return caches_1.cacheStats; } });
|
|
31
|
+
Object.defineProperty(exports, "clearSharedCaches", { enumerable: true, get: function () { return caches_1.clearSharedCaches; } });
|
|
28
32
|
__exportStar(require("./api"), exports);
|
|
29
33
|
__exportStar(require("./converter"), exports);
|
|
30
34
|
__exportStar(require("./lib/decrypt"), exports);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** An LRU that counts hits and misses and can be resized at runtime. */
|
|
2
|
+
declare class CountingCache {
|
|
3
|
+
private lru;
|
|
4
|
+
private opts;
|
|
5
|
+
hits: number;
|
|
6
|
+
misses: number;
|
|
7
|
+
constructor(maxSize: number, ttl?: number);
|
|
8
|
+
get(key: string): any;
|
|
9
|
+
set(key: string, value: any): void;
|
|
10
|
+
clear(): void;
|
|
11
|
+
/** Resize / re-TTL. Drops the current contents (a new LRU is built). */
|
|
12
|
+
reconfigure(opts: {
|
|
13
|
+
maxSize?: number;
|
|
14
|
+
ttl?: number;
|
|
15
|
+
}): void;
|
|
16
|
+
get size(): number;
|
|
17
|
+
get maxSize(): number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Account-independent gateway payloads, shared across every {@link Session}.
|
|
21
|
+
* Keyed by country + method + params — see `Session.gw`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const sharedGatewayCache: CountingCache;
|
|
24
|
+
/** In-flight requests for {@link sharedGatewayCache}, so concurrent sessions coalesce. */
|
|
25
|
+
export declare const sharedInFlight: Map<string, Promise<any>>;
|
|
26
|
+
export interface CacheOptions {
|
|
27
|
+
/**
|
|
28
|
+
* The shared gateway metadata cache. Default `{maxSize: 2000, ttl: 3_600_000}`.
|
|
29
|
+
* Raise `maxSize` for a large catalogue; entries are JSON payloads of a few KB.
|
|
30
|
+
*/
|
|
31
|
+
shared?: {
|
|
32
|
+
maxSize?: number;
|
|
33
|
+
ttl?: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resize the shared cache. Call once at startup — reconfiguring drops its
|
|
38
|
+
* contents.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* configureCache({shared: {maxSize: 20_000, ttl: 30 * 60_000}});
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare const configureCache: (options: CacheOptions) => void;
|
|
45
|
+
/** Drop the shared cache. Per-session caches are untouched (`session.cache.clear()` for those). */
|
|
46
|
+
export declare const clearSharedCaches: () => void;
|
|
47
|
+
export interface CacheStats {
|
|
48
|
+
/** the shared account-independent gateway metadata cache */
|
|
49
|
+
shared: {
|
|
50
|
+
size: number;
|
|
51
|
+
maxSize: number;
|
|
52
|
+
hits: number;
|
|
53
|
+
misses: number;
|
|
54
|
+
inFlight: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A snapshot of shared-cache occupancy and hit rate — for metrics / health endpoints. */
|
|
58
|
+
export declare const cacheStats: () => CacheStats;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cacheStats = exports.clearSharedCaches = exports.configureCache = exports.sharedInFlight = exports.sharedGatewayCache = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* The process-wide cache for account-independent gateway metadata.
|
|
9
|
+
*
|
|
10
|
+
* Each {@link Session} keeps its own response cache because most gw payloads
|
|
11
|
+
* embed a per-account `TRACK_TOKEN`. A handful of methods carry nothing
|
|
12
|
+
* account-scoped, though — album, artist, lyrics, discography and playlist
|
|
13
|
+
* metadata are the same bytes for every account in a country. Caching those per
|
|
14
|
+
* session multiplies one payload by the number of logged-in accounts, which is
|
|
15
|
+
* fine for a script and wrong for a server.
|
|
16
|
+
*
|
|
17
|
+
* Holding them here means they are stored once, and — via {@link sharedInFlight}
|
|
18
|
+
* — a burst of sessions asking for the same album shares a single request
|
|
19
|
+
* instead of issuing one each. Measured at 500 sessions reading one album:
|
|
20
|
+
* **500 gateway requests → 1**, and 1.8 MB of duplicated payload → one copy.
|
|
21
|
+
*
|
|
22
|
+
* Tune with {@link configureCache}, inspect with {@link cacheStats}.
|
|
23
|
+
*/
|
|
24
|
+
const fast_lru_1 = __importDefault(require("./fast-lru"));
|
|
25
|
+
/** An LRU that counts hits and misses and can be resized at runtime. */
|
|
26
|
+
class CountingCache {
|
|
27
|
+
constructor(maxSize, ttl = 0) {
|
|
28
|
+
this.hits = 0;
|
|
29
|
+
this.misses = 0;
|
|
30
|
+
this.opts = { maxSize, ttl };
|
|
31
|
+
this.lru = new fast_lru_1.default(this.opts);
|
|
32
|
+
}
|
|
33
|
+
get(key) {
|
|
34
|
+
const value = this.lru.get(key);
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
this.misses++;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this.hits++;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
set(key, value) {
|
|
44
|
+
this.lru.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
clear() {
|
|
47
|
+
this.lru.clear();
|
|
48
|
+
}
|
|
49
|
+
/** Resize / re-TTL. Drops the current contents (a new LRU is built). */
|
|
50
|
+
reconfigure(opts) {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
this.opts = { maxSize: (_a = opts.maxSize) !== null && _a !== void 0 ? _a : this.opts.maxSize, ttl: (_b = opts.ttl) !== null && _b !== void 0 ? _b : this.opts.ttl };
|
|
53
|
+
this.lru = new fast_lru_1.default(this.opts);
|
|
54
|
+
}
|
|
55
|
+
get size() {
|
|
56
|
+
return this.lru.size;
|
|
57
|
+
}
|
|
58
|
+
get maxSize() {
|
|
59
|
+
return this.opts.maxSize;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Account-independent gateway payloads, shared across every {@link Session}.
|
|
64
|
+
* Keyed by country + method + params — see `Session.gw`.
|
|
65
|
+
*/
|
|
66
|
+
exports.sharedGatewayCache = new CountingCache(2000, 60 * 60000);
|
|
67
|
+
/** In-flight requests for {@link sharedGatewayCache}, so concurrent sessions coalesce. */
|
|
68
|
+
exports.sharedInFlight = new Map();
|
|
69
|
+
/**
|
|
70
|
+
* Resize the shared cache. Call once at startup — reconfiguring drops its
|
|
71
|
+
* contents.
|
|
72
|
+
*
|
|
73
|
+
* ```ts
|
|
74
|
+
* configureCache({shared: {maxSize: 20_000, ttl: 30 * 60_000}});
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
const configureCache = (options) => {
|
|
78
|
+
if (options.shared) {
|
|
79
|
+
exports.sharedGatewayCache.reconfigure(options.shared);
|
|
80
|
+
exports.sharedInFlight.clear();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
exports.configureCache = configureCache;
|
|
84
|
+
/** Drop the shared cache. Per-session caches are untouched (`session.cache.clear()` for those). */
|
|
85
|
+
const clearSharedCaches = () => {
|
|
86
|
+
exports.sharedGatewayCache.clear();
|
|
87
|
+
exports.sharedInFlight.clear();
|
|
88
|
+
};
|
|
89
|
+
exports.clearSharedCaches = clearSharedCaches;
|
|
90
|
+
/** A snapshot of shared-cache occupancy and hit rate — for metrics / health endpoints. */
|
|
91
|
+
const cacheStats = () => ({
|
|
92
|
+
shared: {
|
|
93
|
+
size: exports.sharedGatewayCache.size,
|
|
94
|
+
maxSize: exports.sharedGatewayCache.maxSize,
|
|
95
|
+
hits: exports.sharedGatewayCache.hits,
|
|
96
|
+
misses: exports.sharedGatewayCache.misses,
|
|
97
|
+
inFlight: exports.sharedInFlight.size,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
exports.cacheStats = cacheStats;
|
package/dist/lib/decrypt.js
CHANGED
|
@@ -34,6 +34,9 @@ const getBlowfishKey = (trackId) => {
|
|
|
34
34
|
return key;
|
|
35
35
|
};
|
|
36
36
|
const CHUNK = 2048;
|
|
37
|
+
// Note: memoising the initialised Blowfish schedule per track was measured and
|
|
38
|
+
// rejected — key setup is 39.7µs against 33ms to decrypt an 8 MiB file (0.12%),
|
|
39
|
+
// break-even at ~10 KiB decrypted per key. Not worth the cache.
|
|
37
40
|
/**
|
|
38
41
|
* Decrypt a downloaded track. Deezer applies Blowfish-CBC "stripe" obfuscation:
|
|
39
42
|
* the file is split into 2048-byte chunks and only every third chunk (0, 3, 6…)
|
package/dist/lib/session.d.ts
CHANGED
|
@@ -102,7 +102,14 @@ export declare class Session {
|
|
|
102
102
|
* backoff, and a wall-clock deadline. Throws `DeezerError` on exhaustion.
|
|
103
103
|
*/
|
|
104
104
|
request<T>(method: 'GET' | 'POST', url: string, body?: unknown, config?: SessionRequestConfig): Promise<HttpResponse<T>>;
|
|
105
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Single-flight + LRU around a fetcher.
|
|
107
|
+
*
|
|
108
|
+
* `shared` routes to the process-wide cache and in-flight map instead of this
|
|
109
|
+
* session's — used for gw methods whose payloads carry nothing account-scoped
|
|
110
|
+
* (see {@link ACCOUNT_INDEPENDENT_GW}). Two sessions asking for the same album
|
|
111
|
+
* concurrently then share one request rather than issuing one each.
|
|
112
|
+
*/
|
|
106
113
|
private coalesce;
|
|
107
114
|
/** POST `gateway.php` — the main gw method channel. Coalesced + cached. */
|
|
108
115
|
gw<T = any>(body: Record<string, unknown>, method: string): Promise<T>;
|
package/dist/lib/session.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.setDefaultSession = exports.createSession = exports.initDeezerApi = exports.defaultSession = exports.Session = exports.RETRY_POLICY = exports.DEFAULT_ARL = void 0;
|
|
7
7
|
const delay_1 = __importDefault(require("delay"));
|
|
8
|
+
const caches_1 = require("./caches");
|
|
8
9
|
const errors_1 = require("./errors");
|
|
9
10
|
const fast_lru_1 = __importDefault(require("./fast-lru"));
|
|
10
11
|
const http_1 = require("./http");
|
|
@@ -40,6 +41,27 @@ const backoffDelay = (attempt) => {
|
|
|
40
41
|
};
|
|
41
42
|
/** How long a loaded `deezer.getUserData` payload is trusted before a refresh. */
|
|
42
43
|
const USER_DATA_TTL_MS = 25 * 60 * 1000;
|
|
44
|
+
/**
|
|
45
|
+
* gw methods whose payloads carry **no account-scoped fields**, so one copy can
|
|
46
|
+
* serve every session instead of one copy per session.
|
|
47
|
+
*
|
|
48
|
+
* The account-scoped field that matters is `TRACK_TOKEN`, and it appears on
|
|
49
|
+
* exactly two response shapes — `trackType` and `showEpisodeType`. None of the
|
|
50
|
+
* methods below embed either, so their results are identical for every account
|
|
51
|
+
* in a given country. Everything else (`song.getData`, `playlist.getSongs`,
|
|
52
|
+
* `song.getListByAlbum`, `song.getListData`, `episode.getData`, `mobile.*`,
|
|
53
|
+
* `deezer.pageSearch`, `user_getInfo`, …) stays in the per-session cache.
|
|
54
|
+
*
|
|
55
|
+
* The shared key is prefixed with the session's country, so a multi-region
|
|
56
|
+
* deployment never serves one country's availability view to another.
|
|
57
|
+
*/
|
|
58
|
+
const ACCOUNT_INDEPENDENT_GW = new Set([
|
|
59
|
+
'album.getData',
|
|
60
|
+
'artist.getData',
|
|
61
|
+
'song.getLyrics',
|
|
62
|
+
'album.getDiscography',
|
|
63
|
+
'playlist.getData', // playlistInfo — metadata only (playlist.getSongs is NOT shared)
|
|
64
|
+
]);
|
|
43
65
|
/**
|
|
44
66
|
* One Deezer session — owns the `arl`, the HTTP client (and its `sid` /
|
|
45
67
|
* `api_token`), and the account's `license_token` / `country` / streaming
|
|
@@ -217,38 +239,52 @@ class Session {
|
|
|
217
239
|
}
|
|
218
240
|
}
|
|
219
241
|
// ─── Cached request primitives ──────────────────────────────────────────────
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Single-flight + LRU around a fetcher.
|
|
244
|
+
*
|
|
245
|
+
* `shared` routes to the process-wide cache and in-flight map instead of this
|
|
246
|
+
* session's — used for gw methods whose payloads carry nothing account-scoped
|
|
247
|
+
* (see {@link ACCOUNT_INDEPENDENT_GW}). Two sessions asking for the same album
|
|
248
|
+
* concurrently then share one request rather than issuing one each.
|
|
249
|
+
*/
|
|
250
|
+
coalesce(key, fetcher, shared = false) {
|
|
251
|
+
const cache = shared ? caches_1.sharedGatewayCache : this.cache;
|
|
252
|
+
const inFlight = shared ? caches_1.sharedInFlight : this.inFlight;
|
|
253
|
+
const cached = cache.get(key);
|
|
223
254
|
if (cached) {
|
|
224
255
|
return Promise.resolve(cached);
|
|
225
256
|
}
|
|
226
|
-
const pending =
|
|
257
|
+
const pending = inFlight.get(key);
|
|
227
258
|
if (pending) {
|
|
228
259
|
return pending;
|
|
229
260
|
}
|
|
230
261
|
const promise = (async () => {
|
|
231
262
|
try {
|
|
232
|
-
|
|
263
|
+
const value = await fetcher();
|
|
264
|
+
cache.set(key, value);
|
|
265
|
+
return value;
|
|
233
266
|
}
|
|
234
267
|
finally {
|
|
235
|
-
|
|
268
|
+
inFlight.delete(key);
|
|
236
269
|
}
|
|
237
270
|
})();
|
|
238
|
-
|
|
271
|
+
inFlight.set(key, promise);
|
|
239
272
|
return promise;
|
|
240
273
|
}
|
|
241
274
|
/** POST `gateway.php` — the main gw method channel. Coalesced + cached. */
|
|
242
275
|
gw(body, method) {
|
|
243
|
-
|
|
276
|
+
var _a;
|
|
277
|
+
const shared = ACCOUNT_INDEPENDENT_GW.has(method);
|
|
278
|
+
const key = shared
|
|
279
|
+
? `${(_a = this.country) !== null && _a !== void 0 ? _a : 'XX'}:gw:${method}:${Object.entries(body).join(':')}`
|
|
280
|
+
: `gw:${method}:${Object.entries(body).join(':')}`;
|
|
244
281
|
return this.coalesce(key, async () => {
|
|
245
282
|
const { data: { error, results }, } = await this.request('POST', '/gateway.php', body, { params: { method } });
|
|
246
283
|
if (results && Object.keys(results).length > 0) {
|
|
247
|
-
this.cache.set(key, results);
|
|
248
284
|
return results;
|
|
249
285
|
}
|
|
250
286
|
throw new errors_1.DeezerError(error);
|
|
251
|
-
});
|
|
287
|
+
}, shared);
|
|
252
288
|
}
|
|
253
289
|
/** POST `gw-light.php` — the lighter method channel (search, suggest, …). */
|
|
254
290
|
gwLight(body, method) {
|
|
@@ -258,7 +294,6 @@ class Session {
|
|
|
258
294
|
params: { method, api_version: '1.0' },
|
|
259
295
|
});
|
|
260
296
|
if (results && Object.keys(results).length > 0) {
|
|
261
|
-
this.cache.set(key, results);
|
|
262
297
|
return results;
|
|
263
298
|
}
|
|
264
299
|
throw new errors_1.DeezerError(error);
|
|
@@ -272,7 +307,6 @@ class Session {
|
|
|
272
307
|
params: { method, ...params },
|
|
273
308
|
});
|
|
274
309
|
if (results && Object.keys(results).length > 0) {
|
|
275
|
-
this.cache.set(cacheKey, results);
|
|
276
310
|
return results;
|
|
277
311
|
}
|
|
278
312
|
throw new errors_1.DeezerError(error);
|
package/package.json
CHANGED