@audd/sdk 1.4.7 → 1.5.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.
@@ -1,4 +1,4 @@
1
- import { F as FetchLike } from './http-y2YwjfAl.cjs';
1
+ import { F as FetchLike, L as LongpollPoll } from './longpollCore-DMBdcGSM.cjs';
2
2
 
3
3
  /**
4
4
  * Tokenless longpoll consumer for browser/widget/extension use cases.
@@ -9,11 +9,12 @@ import { F as FetchLike } from './http-y2YwjfAl.cjs';
9
9
  * token).
10
10
  *
11
11
  * Behavior:
12
- * - HTTP non-2xx → throws `AudDServerError` (no silent infinite loop)
13
- * - JSON decode failure on 2xx → throws `AudDSerializationError`
12
+ * - HTTP non-2xx → terminal {@link AudDServerError} on the `errors` iterable
13
+ * (no silent infinite loop).
14
+ * - JSON decode failure on 2xx → terminal {@link AudDSerializationError}.
14
15
  * - READ-class retries on 5xx + connection failures, configurable via
15
- * `maxRetries` / `backoffFactorMs`
16
- * - `[Symbol.asyncDispose]()` + manual `close()` for resource cleanup
16
+ * `maxRetries` / `backoffFactorMs`.
17
+ * - `[Symbol.asyncDispose]()` + manual `close()` for resource cleanup.
17
18
  *
18
19
  * Imported from `audd/longpoll` (browser-safe sub-entry, NOT the main
19
20
  * `audd` module — bundlers tree-shake the auth client out).
@@ -33,26 +34,36 @@ interface IterateOptions {
33
34
  timeout?: number;
34
35
  }
35
36
  /**
36
- * Tokenless async longpoll consumer.
37
+ * Tokenless longpoll consumer.
37
38
  *
38
39
  * ```ts
39
40
  * import { LongpollConsumer } from 'audd/longpoll';
40
41
  *
41
42
  * const consumer = new LongpollConsumer("abc123def");
42
- * for await (const event of consumer.iterate({ timeout: 30 })) {
43
- * console.log(event);
43
+ * const poll = consumer.iterate({ timeout: 30 });
44
+ * for await (const m of poll.matches) {
45
+ * console.log(m.song.artist, m.song.title);
44
46
  * }
45
47
  * ```
48
+ *
49
+ * Or iterate matches and notifications concurrently:
50
+ *
51
+ * ```ts
52
+ * await Promise.all([
53
+ * (async () => { for await (const m of poll.matches) { ... } })(),
54
+ * (async () => { for await (const n of poll.notifications) { ... } })(),
55
+ * ]);
56
+ * ```
46
57
  */
47
58
  declare class LongpollConsumer {
48
59
  readonly category: string;
49
60
  private readonly fetchImpl;
50
61
  private readonly policy;
51
62
  constructor(category: string, opts?: LongpollConsumerOptions);
52
- iterate(opts?: IterateOptions): AsyncIterable<Record<string, unknown>>;
63
+ iterate(opts?: IterateOptions): LongpollPoll;
53
64
  /** Release any underlying resources. */
54
65
  close(): void;
55
66
  [Symbol.asyncDispose](): Promise<void>;
56
67
  }
57
68
 
58
- export { type IterateOptions, LongpollConsumer, type LongpollConsumerOptions };
69
+ export { type IterateOptions, LongpollConsumer, type LongpollConsumerOptions, LongpollPoll };
@@ -1,4 +1,4 @@
1
- import { F as FetchLike } from './http-y2YwjfAl.js';
1
+ import { F as FetchLike, L as LongpollPoll } from './longpollCore-DMBdcGSM.js';
2
2
 
3
3
  /**
4
4
  * Tokenless longpoll consumer for browser/widget/extension use cases.
@@ -9,11 +9,12 @@ import { F as FetchLike } from './http-y2YwjfAl.js';
9
9
  * token).
10
10
  *
11
11
  * Behavior:
12
- * - HTTP non-2xx → throws `AudDServerError` (no silent infinite loop)
13
- * - JSON decode failure on 2xx → throws `AudDSerializationError`
12
+ * - HTTP non-2xx → terminal {@link AudDServerError} on the `errors` iterable
13
+ * (no silent infinite loop).
14
+ * - JSON decode failure on 2xx → terminal {@link AudDSerializationError}.
14
15
  * - READ-class retries on 5xx + connection failures, configurable via
15
- * `maxRetries` / `backoffFactorMs`
16
- * - `[Symbol.asyncDispose]()` + manual `close()` for resource cleanup
16
+ * `maxRetries` / `backoffFactorMs`.
17
+ * - `[Symbol.asyncDispose]()` + manual `close()` for resource cleanup.
17
18
  *
18
19
  * Imported from `audd/longpoll` (browser-safe sub-entry, NOT the main
19
20
  * `audd` module — bundlers tree-shake the auth client out).
@@ -33,26 +34,36 @@ interface IterateOptions {
33
34
  timeout?: number;
34
35
  }
35
36
  /**
36
- * Tokenless async longpoll consumer.
37
+ * Tokenless longpoll consumer.
37
38
  *
38
39
  * ```ts
39
40
  * import { LongpollConsumer } from 'audd/longpoll';
40
41
  *
41
42
  * const consumer = new LongpollConsumer("abc123def");
42
- * for await (const event of consumer.iterate({ timeout: 30 })) {
43
- * console.log(event);
43
+ * const poll = consumer.iterate({ timeout: 30 });
44
+ * for await (const m of poll.matches) {
45
+ * console.log(m.song.artist, m.song.title);
44
46
  * }
45
47
  * ```
48
+ *
49
+ * Or iterate matches and notifications concurrently:
50
+ *
51
+ * ```ts
52
+ * await Promise.all([
53
+ * (async () => { for await (const m of poll.matches) { ... } })(),
54
+ * (async () => { for await (const n of poll.notifications) { ... } })(),
55
+ * ]);
56
+ * ```
46
57
  */
47
58
  declare class LongpollConsumer {
48
59
  readonly category: string;
49
60
  private readonly fetchImpl;
50
61
  private readonly policy;
51
62
  constructor(category: string, opts?: LongpollConsumerOptions);
52
- iterate(opts?: IterateOptions): AsyncIterable<Record<string, unknown>>;
63
+ iterate(opts?: IterateOptions): LongpollPoll;
53
64
  /** Release any underlying resources. */
54
65
  close(): void;
55
66
  [Symbol.asyncDispose](): Promise<void>;
56
67
  }
57
68
 
58
- export { type IterateOptions, LongpollConsumer, type LongpollConsumerOptions };
69
+ export { type IterateOptions, LongpollConsumer, type LongpollConsumerOptions, LongpollPoll };
package/dist/longpoll.js CHANGED
@@ -45,6 +45,272 @@ var AudDSerializationError = class extends AudDError {
45
45
  }
46
46
  };
47
47
 
48
+ // src/models.ts
49
+ function pickExtras(raw, knownKeys) {
50
+ const known = new Set(knownKeys);
51
+ const extras = {};
52
+ for (const [k, v] of Object.entries(raw)) {
53
+ if (!known.has(k)) extras[k] = v;
54
+ }
55
+ return extras;
56
+ }
57
+ function asObject(raw) {
58
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
59
+ return raw;
60
+ }
61
+ return void 0;
62
+ }
63
+ function requireObject(raw, what) {
64
+ const o = asObject(raw);
65
+ if (o === void 0) throw new TypeError(`${what}: expected object`);
66
+ return o;
67
+ }
68
+ function asString(v) {
69
+ return typeof v === "string" ? v : void 0;
70
+ }
71
+ function asNumber(v) {
72
+ return typeof v === "number" ? v : void 0;
73
+ }
74
+ function asBoolean(v) {
75
+ return typeof v === "boolean" ? v : void 0;
76
+ }
77
+ var STREAM_CALLBACK_SONG_KEYS = [
78
+ "artist",
79
+ "title",
80
+ "score",
81
+ "album",
82
+ "release_date",
83
+ "label",
84
+ "song_link",
85
+ "isrc",
86
+ "upc",
87
+ "apple_music",
88
+ "spotify",
89
+ "deezer",
90
+ "napster",
91
+ "musicbrainz"
92
+ ];
93
+ function parseStreamCallbackSong(raw) {
94
+ const r = requireObject(raw, "StreamCallbackSong");
95
+ const artist = asString(r.artist);
96
+ const title = asString(r.title);
97
+ const score = asNumber(r.score);
98
+ if (artist === void 0) throw new TypeError("StreamCallbackSong: artist not string");
99
+ if (title === void 0) throw new TypeError("StreamCallbackSong: title not string");
100
+ if (score === void 0) throw new TypeError("StreamCallbackSong: score not number");
101
+ const musicbrainz = Array.isArray(r.musicbrainz) ? r.musicbrainz.filter((x) => asObject(x) !== void 0) : void 0;
102
+ return {
103
+ artist,
104
+ title,
105
+ score,
106
+ album: asString(r.album),
107
+ releaseDate: asString(r.release_date),
108
+ label: asString(r.label),
109
+ songLink: asString(r.song_link),
110
+ isrc: asString(r.isrc),
111
+ upc: asString(r.upc),
112
+ appleMusic: asObject(r.apple_music),
113
+ spotify: asObject(r.spotify),
114
+ deezer: asObject(r.deezer),
115
+ napster: asObject(r.napster),
116
+ musicbrainz,
117
+ extras: pickExtras(r, STREAM_CALLBACK_SONG_KEYS)
118
+ };
119
+ }
120
+ var STREAM_CALLBACK_MATCH_KEYS = [
121
+ "radio_id",
122
+ "timestamp",
123
+ "play_length",
124
+ "results"
125
+ ];
126
+ function parseStreamCallbackMatch(raw) {
127
+ const r = requireObject(raw, "StreamCallbackMatch");
128
+ const radioId = asNumber(r.radio_id);
129
+ if (radioId === void 0) throw new TypeError("StreamCallbackMatch: radio_id not number");
130
+ const resultsRaw = r.results;
131
+ if (!Array.isArray(resultsRaw)) throw new TypeError("StreamCallbackMatch: results not array");
132
+ if (resultsRaw.length === 0) throw new TypeError("StreamCallbackMatch: results is empty");
133
+ const songs = resultsRaw.map(parseStreamCallbackSong);
134
+ const [first, ...rest] = songs;
135
+ if (first === void 0) throw new TypeError("StreamCallbackMatch: results is empty");
136
+ return {
137
+ radioId,
138
+ timestamp: asString(r.timestamp),
139
+ playLength: asNumber(r.play_length),
140
+ song: first,
141
+ alternatives: rest,
142
+ extras: pickExtras(r, STREAM_CALLBACK_MATCH_KEYS),
143
+ rawResponse: r
144
+ };
145
+ }
146
+ var STREAM_CALLBACK_NOTIFICATION_KEYS = [
147
+ "radio_id",
148
+ "stream_running",
149
+ "notification_code",
150
+ "notification_message"
151
+ ];
152
+ function parseStreamCallbackNotification(raw) {
153
+ const r = requireObject(raw, "StreamCallbackNotification");
154
+ const radioId = asNumber(r.radio_id);
155
+ if (radioId === void 0)
156
+ throw new TypeError("StreamCallbackNotification: radio_id not number");
157
+ const code = asNumber(r.notification_code);
158
+ if (code === void 0)
159
+ throw new TypeError("StreamCallbackNotification: notification_code not number");
160
+ const message = asString(r.notification_message);
161
+ if (message === void 0)
162
+ throw new TypeError("StreamCallbackNotification: notification_message not string");
163
+ return {
164
+ radioId,
165
+ streamRunning: asBoolean(r.stream_running),
166
+ notificationCode: code,
167
+ notificationMessage: message,
168
+ extras: pickExtras(r, STREAM_CALLBACK_NOTIFICATION_KEYS),
169
+ rawResponse: r
170
+ };
171
+ }
172
+
173
+ // src/longpollCore.ts
174
+ var HTTP_CLIENT_ERROR_FLOOR = 400;
175
+ var AsyncQueue = class {
176
+ items = [];
177
+ waiters = [];
178
+ done = false;
179
+ push(value) {
180
+ if (this.done) return;
181
+ const w = this.waiters.shift();
182
+ if (w !== void 0) {
183
+ w({ value, done: false });
184
+ return;
185
+ }
186
+ this.items.push(value);
187
+ }
188
+ finish() {
189
+ if (this.done) return;
190
+ this.done = true;
191
+ while (this.waiters.length > 0) {
192
+ const w = this.waiters.shift();
193
+ if (w !== void 0) w({ value: void 0, done: true });
194
+ }
195
+ }
196
+ next() {
197
+ const item = this.items.shift();
198
+ if (item !== void 0) return Promise.resolve({ value: item, done: false });
199
+ if (this.done) return Promise.resolve({ value: void 0, done: true });
200
+ return new Promise((resolve) => {
201
+ this.waiters.push(resolve);
202
+ });
203
+ }
204
+ toIterable() {
205
+ const next = () => this.next();
206
+ return {
207
+ [Symbol.asyncIterator]() {
208
+ return { next };
209
+ }
210
+ };
211
+ }
212
+ };
213
+ function decodeOne(resp) {
214
+ if (resp.httpStatus >= HTTP_CLIENT_ERROR_FLOOR) {
215
+ throw new AudDServerError({
216
+ errorCode: 0,
217
+ message: `Longpoll endpoint returned HTTP ${String(resp.httpStatus)}`,
218
+ httpStatus: resp.httpStatus,
219
+ requestId: resp.requestId,
220
+ rawResponse: resp.jsonBody ?? resp.rawText
221
+ });
222
+ }
223
+ const body = resp.jsonBody;
224
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
225
+ throw new AudDSerializationError(
226
+ "Longpoll response was not a JSON object",
227
+ resp.rawText
228
+ );
229
+ }
230
+ const dict = body;
231
+ if ("notification" in dict) {
232
+ const notification = parseStreamCallbackNotification(dict.notification);
233
+ const t = dict["time"];
234
+ if (typeof t === "number") notification.time = t;
235
+ return { kind: "notification", notification };
236
+ }
237
+ if ("result" in dict) {
238
+ return { kind: "match", match: parseStreamCallbackMatch(dict.result) };
239
+ }
240
+ return { kind: "keepalive" };
241
+ }
242
+ function startLongpoll(cfg) {
243
+ const matches = new AsyncQueue();
244
+ const notifications = new AsyncQueue();
245
+ const errors = new AsyncQueue();
246
+ const controller = new AbortController();
247
+ let closed = false;
248
+ const finishAll = () => {
249
+ matches.finish();
250
+ notifications.finish();
251
+ errors.finish();
252
+ };
253
+ const close = () => {
254
+ if (closed) return;
255
+ closed = true;
256
+ controller.abort();
257
+ finishAll();
258
+ };
259
+ const run = async () => {
260
+ let curSince = cfg.sinceTime;
261
+ while (!closed) {
262
+ const params = {
263
+ category: cfg.category,
264
+ timeout: String(cfg.timeout)
265
+ };
266
+ if (curSince !== void 0) params["since_time"] = String(curSince);
267
+ let resp;
268
+ try {
269
+ resp = await cfg.fetchOnce(params, controller.signal);
270
+ } catch (err) {
271
+ if (closed) return;
272
+ if (err instanceof TypeError) {
273
+ errors.push(new AudDConnectionError(`Network error: ${err.message}`, err));
274
+ } else if (err !== null && typeof err === "object" && err.name === "AbortError") {
275
+ if (closed) return;
276
+ errors.push(new AudDConnectionError("Request was aborted (timeout)", err));
277
+ } else if (err instanceof Error) {
278
+ errors.push(err);
279
+ } else {
280
+ errors.push(new Error(String(err)));
281
+ }
282
+ finishAll();
283
+ return;
284
+ }
285
+ try {
286
+ const decoded = decodeOne(resp);
287
+ if (decoded.kind === "match") matches.push(decoded.match);
288
+ else if (decoded.kind === "notification") notifications.push(decoded.notification);
289
+ } catch (err) {
290
+ errors.push(err instanceof Error ? err : new Error(String(err)));
291
+ finishAll();
292
+ return;
293
+ }
294
+ const dict = resp.jsonBody;
295
+ if (typeof dict === "object" && dict !== null && !Array.isArray(dict)) {
296
+ const ts = dict["timestamp"];
297
+ if (typeof ts === "number") curSince = ts;
298
+ }
299
+ }
300
+ };
301
+ void run();
302
+ return {
303
+ matches: matches.toIterable(),
304
+ notifications: notifications.toIterable(),
305
+ errors: errors.toIterable(),
306
+ close,
307
+ async [Symbol.asyncDispose]() {
308
+ close();
309
+ await Promise.resolve();
310
+ }
311
+ };
312
+ }
313
+
48
314
  // src/retry.ts
49
315
  var DEFAULT_BACKOFF_FACTOR_MS = 500;
50
316
  var DEFAULT_BACKOFF_MAX_MS = 3e4;
@@ -133,23 +399,6 @@ function userAgent() {
133
399
 
134
400
  // src/longpoll.ts
135
401
  var LONGPOLL_URL = "https://api.audd.io/longpoll/";
136
- var HTTP_CLIENT_ERROR_FLOOR = 400;
137
- var DEFAULT_TIMEOUT_MS = 12e4;
138
- function decode(jsonBody, httpStatus, rawText) {
139
- if (httpStatus >= HTTP_CLIENT_ERROR_FLOOR) {
140
- throw new AudDServerError({
141
- errorCode: 0,
142
- message: `Longpoll endpoint returned HTTP ${String(httpStatus)}`,
143
- httpStatus,
144
- requestId: null,
145
- rawResponse: jsonBody ?? rawText
146
- });
147
- }
148
- if (typeof jsonBody !== "object" || jsonBody === null || Array.isArray(jsonBody)) {
149
- throw new AudDSerializationError("Longpoll response was not a JSON object", rawText);
150
- }
151
- return jsonBody;
152
- }
153
402
  var LongpollConsumer = class {
154
403
  constructor(category, opts = {}) {
155
404
  this.category = category;
@@ -165,70 +414,54 @@ var LongpollConsumer = class {
165
414
  fetchImpl;
166
415
  policy;
167
416
  iterate(opts = {}) {
168
- const category = this.category;
169
- const policy = this.policy;
170
417
  const fetchImpl = this.fetchImpl;
171
- const initialSince = opts.sinceTime;
418
+ const policy = this.policy;
172
419
  const timeoutSec = opts.timeout ?? 50;
173
- async function fetchOnce(curSince) {
420
+ const fetchOnceRaw = async (params, signal) => {
174
421
  const u = new URL(LONGPOLL_URL);
175
- u.searchParams.set("category", category);
176
- u.searchParams.set("timeout", String(timeoutSec));
177
- if (curSince !== void 0) u.searchParams.set("since_time", String(curSince));
178
- const controller = new AbortController();
179
- const timer = setTimeout(() => {
180
- controller.abort();
181
- }, DEFAULT_TIMEOUT_MS);
182
- try {
183
- const response = await fetchImpl(u.toString(), {
184
- method: "GET",
185
- headers: { "User-Agent": userAgent() },
186
- signal: controller.signal
187
- });
188
- const text = await response.text();
189
- let json = null;
190
- if (text !== "") {
191
- try {
192
- json = JSON.parse(text);
193
- } catch {
194
- json = null;
195
- }
422
+ for (const [k, v] of Object.entries(params)) {
423
+ if (v !== void 0) u.searchParams.set(k, v);
424
+ }
425
+ const response = await fetchImpl(u.toString(), {
426
+ method: "GET",
427
+ headers: { "User-Agent": userAgent() },
428
+ signal
429
+ });
430
+ const text = await response.text();
431
+ let json = null;
432
+ if (text !== "") {
433
+ try {
434
+ json = JSON.parse(text);
435
+ } catch {
436
+ json = null;
196
437
  }
197
- return {
198
- jsonBody: json,
199
- httpStatus: response.status,
200
- requestId: response.headers.get("x-request-id"),
201
- rawText: text
202
- };
203
- } finally {
204
- clearTimeout(timer);
205
438
  }
206
- }
207
- return {
208
- [Symbol.asyncIterator]() {
209
- let curSince = initialSince;
210
- return {
211
- async next() {
212
- let resp;
213
- try {
214
- resp = await retry(() => fetchOnce(curSince), policy);
215
- } catch (err) {
216
- if (err instanceof TypeError) {
217
- throw new AudDConnectionError(`Network error: ${err.message}`, err);
218
- }
219
- if (err !== null && typeof err === "object" && err.name === "AbortError") {
220
- throw new AudDConnectionError("Request was aborted (timeout)", err);
221
- }
222
- throw err;
223
- }
224
- const body = decode(resp.jsonBody, resp.httpStatus, resp.rawText);
225
- const ts = body["timestamp"];
226
- if (typeof ts === "number") curSince = ts;
227
- return { value: body, done: false };
228
- }
229
- };
439
+ return {
440
+ jsonBody: json,
441
+ httpStatus: response.status,
442
+ requestId: response.headers.get("x-request-id"),
443
+ rawText: text
444
+ };
445
+ };
446
+ const fetchOnce = async (params, signal) => {
447
+ try {
448
+ return await retry(() => fetchOnceRaw(params, signal), policy);
449
+ } catch (err) {
450
+ if (err instanceof TypeError) {
451
+ throw new AudDConnectionError(`Network error: ${err.message}`, err);
452
+ }
453
+ if (err !== null && typeof err === "object" && err.name === "AbortError") {
454
+ throw new AudDConnectionError("Request was aborted (timeout)", err);
455
+ }
456
+ throw err;
230
457
  }
231
458
  };
459
+ return startLongpoll({
460
+ category: this.category,
461
+ timeout: timeoutSec,
462
+ sinceTime: opts.sinceTime,
463
+ fetchOnce
464
+ });
232
465
  }
233
466
  /** Release any underlying resources. */
234
467
  close() {