@mrdoge/node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,949 @@
1
+ // src/client.ts
2
+ import { createHttpClient } from "@mrdoge/http";
3
+
4
+ // src/connection.ts
5
+ import WebSocket from "ws";
6
+ import {
7
+ PROTOCOL_VERSION,
8
+ SUBPROTOCOL
9
+ } from "@mrdoge/protocol";
10
+
11
+ // src/errors.ts
12
+ var MrDogeError = class extends Error {
13
+ code;
14
+ data;
15
+ constructor(code, message, data) {
16
+ super(message);
17
+ this.name = this.constructor.name;
18
+ this.code = code;
19
+ this.data = data;
20
+ }
21
+ };
22
+ var UnauthorizedError = class extends MrDogeError {
23
+ constructor(message, data) {
24
+ super("unauthorized", message, data);
25
+ }
26
+ };
27
+ var ForbiddenError = class extends MrDogeError {
28
+ constructor(message, data) {
29
+ super("forbidden", message, data);
30
+ }
31
+ };
32
+ var NotFoundError = class extends MrDogeError {
33
+ constructor(message, data) {
34
+ super("not_found", message, data);
35
+ }
36
+ };
37
+ var ValidationError = class extends MrDogeError {
38
+ constructor(message, data) {
39
+ super("invalid_params", message, data);
40
+ }
41
+ };
42
+ var RateLimitError = class extends MrDogeError {
43
+ retryAfterMs;
44
+ limit;
45
+ remaining;
46
+ resetAt;
47
+ constructor(message, data) {
48
+ super("rate_limited", message, data);
49
+ const d = data ?? {};
50
+ this.retryAfterMs = d.retryAfterMs ?? 0;
51
+ this.limit = d.limit;
52
+ this.remaining = d.remaining;
53
+ this.resetAt = d.resetAt ? new Date(d.resetAt) : void 0;
54
+ }
55
+ };
56
+ var SubscriptionLimitError = class extends MrDogeError {
57
+ constructor(message, data) {
58
+ super("subscription_limit_exceeded", message, data);
59
+ }
60
+ };
61
+ var ConnectionLimitError = class extends MrDogeError {
62
+ current;
63
+ max;
64
+ constructor(message, data) {
65
+ super("connection_limit_exceeded", message, data);
66
+ const d = data ?? {};
67
+ this.current = d.current;
68
+ this.max = d.max;
69
+ }
70
+ };
71
+ var UnavailableError = class extends MrDogeError {
72
+ constructor(message, data) {
73
+ super("unavailable", message, data);
74
+ }
75
+ };
76
+ var InternalError = class extends MrDogeError {
77
+ constructor(message, data) {
78
+ super("internal_error", message, data);
79
+ }
80
+ };
81
+ var ProtocolError = class extends MrDogeError {
82
+ constructor(message, data) {
83
+ super("protocol_error", message, data);
84
+ }
85
+ };
86
+ var ConnectionError = class extends MrDogeError {
87
+ constructor(message, data) {
88
+ super("connection_error", message, data);
89
+ }
90
+ };
91
+ var DisconnectedError = class extends MrDogeError {
92
+ constructor(message = "Connection dropped before response", data) {
93
+ super("disconnected", message, data);
94
+ }
95
+ };
96
+ var TimeoutError = class extends MrDogeError {
97
+ constructor(message, data) {
98
+ super("timeout", message, data);
99
+ }
100
+ };
101
+ var AbortError = class extends MrDogeError {
102
+ constructor(message = "Request aborted") {
103
+ super("aborted", message);
104
+ }
105
+ };
106
+ var CODE_MAP = {
107
+ invalid_request: ProtocolError,
108
+ invalid_params: ValidationError,
109
+ method_not_found: ProtocolError,
110
+ unauthorized: UnauthorizedError,
111
+ forbidden: ForbiddenError,
112
+ not_found: NotFoundError,
113
+ rate_limited: RateLimitError,
114
+ subscription_limit_exceeded: SubscriptionLimitError,
115
+ connection_limit_exceeded: ConnectionLimitError,
116
+ unavailable: UnavailableError,
117
+ internal_error: InternalError,
118
+ protocol_error: ProtocolError
119
+ };
120
+ function rpcErrorToTyped(err) {
121
+ const Ctor = CODE_MAP[err.code];
122
+ if (!Ctor) return new MrDogeError("unknown", err.message, err.data);
123
+ return new Ctor(err.message, err.data);
124
+ }
125
+
126
+ // src/internal/emitter.ts
127
+ var Emitter = class {
128
+ listeners = /* @__PURE__ */ new Map();
129
+ on(event, fn) {
130
+ let set = this.listeners.get(event);
131
+ if (!set) {
132
+ set = /* @__PURE__ */ new Set();
133
+ this.listeners.set(event, set);
134
+ }
135
+ set.add(fn);
136
+ return () => set.delete(fn);
137
+ }
138
+ off(event, fn) {
139
+ this.listeners.get(event)?.delete(fn);
140
+ }
141
+ emit(event, payload) {
142
+ const set = this.listeners.get(event);
143
+ if (!set || set.size === 0) return;
144
+ for (const fn of [...set]) {
145
+ try {
146
+ fn(payload);
147
+ } catch (err) {
148
+ queueMicrotask(() => {
149
+ throw err;
150
+ });
151
+ }
152
+ }
153
+ }
154
+ clear() {
155
+ this.listeners.clear();
156
+ }
157
+ };
158
+
159
+ // src/internal/backoff.ts
160
+ var DEFAULT_BACKOFF = {
161
+ minMs: 1e3,
162
+ maxMs: 3e4,
163
+ jitter: 0.2
164
+ };
165
+ function nextDelay(attempt, cfg = DEFAULT_BACKOFF) {
166
+ const exp = Math.min(cfg.maxMs, cfg.minMs * 2 ** (attempt - 1));
167
+ const jitter = exp * cfg.jitter * (Math.random() * 2 - 1);
168
+ return Math.max(0, Math.round(exp + jitter));
169
+ }
170
+ function sleep(ms, signal) {
171
+ return new Promise((resolve, reject) => {
172
+ if (signal?.aborted) {
173
+ reject(new Error("aborted"));
174
+ return;
175
+ }
176
+ const t = setTimeout(resolve, ms);
177
+ signal?.addEventListener("abort", () => {
178
+ clearTimeout(t);
179
+ reject(new Error("aborted"));
180
+ });
181
+ });
182
+ }
183
+
184
+ // src/connection.ts
185
+ var TERMINAL_CLOSE_CODES = /* @__PURE__ */ new Set([4001, 4002, 4003, 4029]);
186
+ var DEFAULT_CONFIG = {
187
+ requestTimeoutMs: 1e4,
188
+ maxReconnectAttempts: Infinity,
189
+ reconnectBackoff: DEFAULT_BACKOFF,
190
+ compression: true,
191
+ authTimeoutMs: 1e4
192
+ };
193
+ var Connection = class {
194
+ emitter = new Emitter();
195
+ config;
196
+ ws = null;
197
+ nextRequestId = 1;
198
+ pending = /* @__PURE__ */ new Map();
199
+ subscriptions = /* @__PURE__ */ new Map();
200
+ connectingPromise = null;
201
+ welcome = null;
202
+ closed = false;
203
+ reconnectAttempt = 0;
204
+ reconnectAbort = null;
205
+ constructor(config) {
206
+ this.config = config;
207
+ }
208
+ on = this.emitter.on.bind(this.emitter);
209
+ off = this.emitter.off.bind(this.emitter);
210
+ get isConnected() {
211
+ return this.ws?.readyState === WebSocket.OPEN && this.welcome !== null;
212
+ }
213
+ /**
214
+ * Idempotent. Returns the welcome payload once connected + authed.
215
+ */
216
+ async connect() {
217
+ if (this.closed) throw new ConnectionError("Connection is closed");
218
+ if (this.welcome && this.ws?.readyState === WebSocket.OPEN) return this.welcome;
219
+ if (this.connectingPromise) return this.connectingPromise;
220
+ this.connectingPromise = this.openAndAuth().finally(() => {
221
+ this.connectingPromise = null;
222
+ });
223
+ return this.connectingPromise;
224
+ }
225
+ async call(method, params, options) {
226
+ await this.connect();
227
+ return this.send(method, params, options);
228
+ }
229
+ /**
230
+ * Registers a subscription with the connection. The connection sends the
231
+ * subscribe call, stores the registration for reconnect, and routes incoming
232
+ * push events to the handler.
233
+ */
234
+ async registerSubscription(method, params, handlers, options) {
235
+ await this.connect();
236
+ const result = await this.send(method, params, options);
237
+ const registration = {
238
+ subId: result.sub,
239
+ method,
240
+ params,
241
+ onEvent: handlers.onEvent,
242
+ onClosed: handlers.onClosed,
243
+ onSnapshot: handlers.onSnapshot
244
+ };
245
+ this.subscriptions.set(result.sub, registration);
246
+ return { subId: result.sub, snapshot: result.snapshot };
247
+ }
248
+ /**
249
+ * Cancels a subscription server-side and removes the registration locally.
250
+ */
251
+ async cancelSubscription(subId) {
252
+ const reg = this.subscriptions.get(subId);
253
+ if (!reg) return;
254
+ this.subscriptions.delete(subId);
255
+ if (this.ws?.readyState === WebSocket.OPEN) {
256
+ try {
257
+ await this.send("subscription.cancel", { sub: subId });
258
+ } catch {
259
+ }
260
+ }
261
+ }
262
+ async close() {
263
+ this.closed = true;
264
+ this.reconnectAbort?.abort();
265
+ this.reconnectAbort = null;
266
+ for (const [, p] of this.pending) {
267
+ clearTimeout(p.timer);
268
+ p.reject(new DisconnectedError("Connection closed by client"));
269
+ }
270
+ this.pending.clear();
271
+ this.subscriptions.clear();
272
+ if (this.ws && this.ws.readyState <= WebSocket.OPEN) {
273
+ this.ws.close(1e3, "client_close");
274
+ }
275
+ this.ws = null;
276
+ this.welcome = null;
277
+ }
278
+ // -------------------------------------------------------------------------
279
+ // Internals
280
+ // -------------------------------------------------------------------------
281
+ async openAndAuth() {
282
+ const ws = new WebSocket(this.config.baseUrl, [SUBPROTOCOL], {
283
+ perMessageDeflate: this.config.compression ? { threshold: 1024 } : false,
284
+ handshakeTimeout: 15e3
285
+ });
286
+ this.ws = ws;
287
+ this.welcome = null;
288
+ await new Promise((resolve, reject) => {
289
+ const onOpen = () => {
290
+ cleanup();
291
+ resolve();
292
+ };
293
+ const onError = (err) => {
294
+ cleanup();
295
+ reject(new ConnectionError(`Failed to open WebSocket: ${err.message}`));
296
+ };
297
+ const onClose = (code, reason) => {
298
+ cleanup();
299
+ reject(new ConnectionError(`WebSocket closed before open (${code}): ${reason.toString()}`));
300
+ };
301
+ const cleanup = () => {
302
+ ws.off("open", onOpen);
303
+ ws.off("error", onError);
304
+ ws.off("close", onClose);
305
+ };
306
+ ws.once("open", onOpen);
307
+ ws.once("error", onError);
308
+ ws.once("close", onClose);
309
+ });
310
+ ws.on("message", (data) => this.handleMessage(data));
311
+ ws.on("close", (code, reason) => this.handleClose(code, reason.toString()));
312
+ ws.on("error", (err) => this.handleSocketError(err));
313
+ const welcome = await this.performAuth();
314
+ this.welcome = welcome;
315
+ this.reconnectAttempt = 0;
316
+ this.emitter.emit("connected", { welcome });
317
+ if (this.subscriptions.size > 0) {
318
+ await this.resubscribeAll();
319
+ }
320
+ return welcome;
321
+ }
322
+ async performAuth() {
323
+ const welcomePromise = new Promise((resolve, reject) => {
324
+ const timer = setTimeout(() => {
325
+ reject(new TimeoutError("Auth timed out waiting for welcome"));
326
+ }, this.config.authTimeoutMs);
327
+ this.pendingWelcome = {
328
+ resolve,
329
+ reject,
330
+ timer
331
+ };
332
+ });
333
+ const authAck = this.send("auth", { apiKey: this.config.apiKey }).catch(
334
+ (err) => {
335
+ if (err instanceof MrDogeError) throw err;
336
+ throw new UnauthorizedError(`Authentication failed: ${err.message}`);
337
+ }
338
+ );
339
+ await authAck;
340
+ return welcomePromise;
341
+ }
342
+ /**
343
+ * Set during `performAuth` so we can resolve when the `welcome` notification arrives.
344
+ */
345
+ pendingWelcome = null;
346
+ send(method, params, options) {
347
+ if (options?.signal?.aborted) {
348
+ return Promise.reject(new AbortError());
349
+ }
350
+ const ws = this.ws;
351
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
352
+ return Promise.reject(new DisconnectedError("Socket not open"));
353
+ }
354
+ const id = String(this.nextRequestId++);
355
+ const frame = {
356
+ jsonrpc: "2.0",
357
+ id,
358
+ method,
359
+ params
360
+ };
361
+ return new Promise((resolve, reject) => {
362
+ const onAbort = () => {
363
+ this.pending.delete(id);
364
+ clearTimeout(timer);
365
+ options.signal.removeEventListener("abort", onAbort);
366
+ reject(new AbortError());
367
+ };
368
+ const detachSignal = () => {
369
+ options?.signal?.removeEventListener("abort", onAbort);
370
+ };
371
+ const wrappedResolve = (val) => {
372
+ detachSignal();
373
+ resolve(val);
374
+ };
375
+ const wrappedReject = (err) => {
376
+ detachSignal();
377
+ reject(err);
378
+ };
379
+ const timer = setTimeout(() => {
380
+ this.pending.delete(id);
381
+ detachSignal();
382
+ reject(new TimeoutError(`Request "${method}" timed out after ${this.config.requestTimeoutMs}ms`));
383
+ }, this.config.requestTimeoutMs);
384
+ this.pending.set(id, { resolve: wrappedResolve, reject: wrappedReject, timer });
385
+ options?.signal?.addEventListener("abort", onAbort);
386
+ try {
387
+ ws.send(JSON.stringify(frame));
388
+ } catch (err) {
389
+ this.pending.delete(id);
390
+ clearTimeout(timer);
391
+ detachSignal();
392
+ reject(new ConnectionError(`Failed to send frame: ${err.message}`));
393
+ }
394
+ });
395
+ }
396
+ handleMessage(raw) {
397
+ let text;
398
+ if (typeof raw === "string") text = raw;
399
+ else if (Buffer.isBuffer(raw)) text = raw.toString("utf8");
400
+ else if (raw instanceof ArrayBuffer) text = Buffer.from(raw).toString("utf8");
401
+ else text = Buffer.concat(raw).toString("utf8");
402
+ let frame;
403
+ try {
404
+ frame = JSON.parse(text);
405
+ } catch {
406
+ return;
407
+ }
408
+ if (frame.jsonrpc !== "2.0") return;
409
+ if (typeof frame.id === "string") {
410
+ const p = this.pending.get(frame.id);
411
+ if (!p) return;
412
+ this.pending.delete(frame.id);
413
+ clearTimeout(p.timer);
414
+ if (frame.error) {
415
+ p.reject(rpcErrorToTyped(frame.error));
416
+ } else {
417
+ p.resolve(frame.result);
418
+ }
419
+ return;
420
+ }
421
+ if (typeof frame.method === "string") {
422
+ this.handleNotification(frame.method, frame.params);
423
+ }
424
+ }
425
+ handleNotification(method, params) {
426
+ if (method === "welcome") {
427
+ const pending = this.pendingWelcome;
428
+ this.pendingWelcome = null;
429
+ if (pending) {
430
+ clearTimeout(pending.timer);
431
+ pending.resolve(params);
432
+ }
433
+ return;
434
+ }
435
+ if (method === "subscription.event") {
436
+ const p = params;
437
+ const reg = this.subscriptions.get(p.sub);
438
+ reg?.onEvent(p);
439
+ return;
440
+ }
441
+ if (method === "subscription.closed") {
442
+ const p = params;
443
+ const reg = this.subscriptions.get(p.sub);
444
+ if (reg) {
445
+ this.subscriptions.delete(p.sub);
446
+ reg.onClosed(p);
447
+ }
448
+ return;
449
+ }
450
+ }
451
+ handleClose(code, reason) {
452
+ const wasConnected = this.welcome !== null;
453
+ this.welcome = null;
454
+ this.ws = null;
455
+ for (const [, p] of this.pending) {
456
+ clearTimeout(p.timer);
457
+ p.reject(new DisconnectedError(`Connection closed (${code}): ${reason}`));
458
+ }
459
+ this.pending.clear();
460
+ if (this.pendingWelcome) {
461
+ clearTimeout(this.pendingWelcome.timer);
462
+ this.pendingWelcome.reject(new DisconnectedError(`Connection closed during auth (${code})`));
463
+ this.pendingWelcome = null;
464
+ }
465
+ if (this.closed) return;
466
+ this.emitter.emit("disconnected", { code, reason });
467
+ if (TERMINAL_CLOSE_CODES.has(code)) {
468
+ this.closed = true;
469
+ return;
470
+ }
471
+ if (wasConnected || this.subscriptions.size > 0) {
472
+ this.scheduleReconnect();
473
+ }
474
+ }
475
+ handleSocketError(_err) {
476
+ }
477
+ async scheduleReconnect() {
478
+ if (this.closed) return;
479
+ if (this.reconnectAbort) return;
480
+ this.reconnectAbort = new AbortController();
481
+ const signal = this.reconnectAbort.signal;
482
+ while (!this.closed && this.reconnectAttempt < this.config.maxReconnectAttempts) {
483
+ this.reconnectAttempt += 1;
484
+ const delayMs = nextDelay(this.reconnectAttempt, this.config.reconnectBackoff);
485
+ this.emitter.emit("reconnecting", { attempt: this.reconnectAttempt, delayMs });
486
+ try {
487
+ await sleep(delayMs, signal);
488
+ } catch {
489
+ return;
490
+ }
491
+ if (this.closed) return;
492
+ try {
493
+ await this.openAndAuth();
494
+ this.reconnectAbort = null;
495
+ return;
496
+ } catch (err) {
497
+ if (err instanceof UnauthorizedError) {
498
+ this.closed = true;
499
+ this.reconnectAbort = null;
500
+ return;
501
+ }
502
+ }
503
+ }
504
+ this.reconnectAbort = null;
505
+ }
506
+ async resubscribeAll() {
507
+ const olds = Array.from(this.subscriptions.values());
508
+ this.subscriptions.clear();
509
+ for (const old of olds) {
510
+ try {
511
+ const result = await this.send(old.method, old.params);
512
+ old.subId = result.sub;
513
+ this.subscriptions.set(result.sub, old);
514
+ old.onSnapshot(result.sub, result.snapshot);
515
+ } catch (err) {
516
+ old.onClosed({
517
+ sub: old.subId,
518
+ reason: "internal_error",
519
+ message: err instanceof Error ? `Resubscribe failed: ${err.message}` : "Resubscribe failed"
520
+ });
521
+ }
522
+ }
523
+ }
524
+ };
525
+
526
+ // src/resources/regions.ts
527
+ var Regions = class {
528
+ constructor(conn, defaults) {
529
+ this.conn = conn;
530
+ this.defaults = defaults;
531
+ }
532
+ conn;
533
+ defaults;
534
+ list(params = {}, options) {
535
+ return this.conn.call(
536
+ "regions.list",
537
+ { locale: this.defaults.locale, ...params },
538
+ options
539
+ );
540
+ }
541
+ };
542
+
543
+ // src/resources/competitions.ts
544
+ var Competitions = class {
545
+ constructor(conn, defaults) {
546
+ this.conn = conn;
547
+ this.defaults = defaults;
548
+ }
549
+ conn;
550
+ defaults;
551
+ list(params = {}, options) {
552
+ return this.conn.call(
553
+ "competitions.list",
554
+ { locale: this.defaults.locale, ...params },
555
+ options
556
+ );
557
+ }
558
+ };
559
+
560
+ // src/resources/teams.ts
561
+ var Teams = class {
562
+ constructor(conn, defaults) {
563
+ this.conn = conn;
564
+ this.defaults = defaults;
565
+ }
566
+ conn;
567
+ defaults;
568
+ list(params = {}, options) {
569
+ return this.conn.call(
570
+ "teams.list",
571
+ { locale: this.defaults.locale, ...params },
572
+ options
573
+ );
574
+ }
575
+ get(params, options) {
576
+ return this.conn.call(
577
+ "teams.get",
578
+ { locale: this.defaults.locale, ...params },
579
+ options
580
+ );
581
+ }
582
+ form(params, options) {
583
+ return this.conn.call(
584
+ "teams.form",
585
+ { locale: this.defaults.locale, ...params },
586
+ options
587
+ );
588
+ }
589
+ };
590
+
591
+ // src/subscription.ts
592
+ var PENDING_SUB_ID = "__pending_ws__";
593
+ var Subscription = class {
594
+ emitter = new Emitter();
595
+ connection;
596
+ internalSubId;
597
+ currentSnapshot;
598
+ cancelled = false;
599
+ /**
600
+ * True when `cancel()` was called before the WS subscribe arrived (subId
601
+ * still pending). Once WS resolves, `_deliverSnapshot` issues the
602
+ * server-side cancel so we don't leak a registered subscription.
603
+ */
604
+ pendingCancel = false;
605
+ /** @internal — constructed by the SDK, not by user code. */
606
+ constructor(connection, subId, initialSnapshot) {
607
+ this.connection = connection;
608
+ this.internalSubId = subId;
609
+ this.currentSnapshot = initialSnapshot;
610
+ }
611
+ /** The latest snapshot the server has emitted (initial, or post-reconnect). */
612
+ get snapshot() {
613
+ return this.currentSnapshot;
614
+ }
615
+ /** Current server-issued subscription id. Changes on reconnect. */
616
+ get id() {
617
+ return this.internalSubId;
618
+ }
619
+ /**
620
+ * Listen for a push event from this subscription. Returns an unsubscribe
621
+ * function that removes only this listener.
622
+ *
623
+ * In addition to protocol push events, two synthetic events fire:
624
+ * - `snapshot` — when a reconnect produced a fresh snapshot
625
+ * - `closed` — when the server terminates the subscription (or cancel)
626
+ */
627
+ on(event, fn) {
628
+ return this.emitter.on(event, fn);
629
+ }
630
+ /**
631
+ * Cancel the subscription server-side. Idempotent — calling twice is safe.
632
+ */
633
+ async cancel() {
634
+ if (this.cancelled) return;
635
+ this.cancelled = true;
636
+ if (this.internalSubId === PENDING_SUB_ID) {
637
+ this.pendingCancel = true;
638
+ this.emitter.clear();
639
+ return;
640
+ }
641
+ await this.connection.cancelSubscription(this.internalSubId);
642
+ this.emitter.clear();
643
+ }
644
+ // ---------------------------------------------------------------------
645
+ // Internal — invoked by Connection's routing layer.
646
+ // ---------------------------------------------------------------------
647
+ /** @internal */
648
+ _deliverEvent(params) {
649
+ if (this.cancelled) return;
650
+ this.emitter.emit(
651
+ params.event,
652
+ params.data
653
+ );
654
+ }
655
+ /** @internal */
656
+ _deliverSnapshot(newSubId, snapshot) {
657
+ this.internalSubId = newSubId;
658
+ if (this.pendingCancel) {
659
+ this.pendingCancel = false;
660
+ this.connection.cancelSubscription(newSubId).catch(() => void 0);
661
+ return;
662
+ }
663
+ if (this.cancelled) return;
664
+ this.currentSnapshot = snapshot;
665
+ this.emitter.emit("snapshot", snapshot);
666
+ }
667
+ /** @internal */
668
+ _deliverClosed(reason, message) {
669
+ if (this.cancelled) return;
670
+ this.cancelled = true;
671
+ this.emitter.emit(
672
+ "closed",
673
+ { reason, message }
674
+ );
675
+ this.emitter.clear();
676
+ }
677
+ };
678
+
679
+ // src/resources/matches.ts
680
+ var Matches = class {
681
+ constructor(conn, defaults, http) {
682
+ this.conn = conn;
683
+ this.defaults = defaults;
684
+ this.http = http;
685
+ }
686
+ conn;
687
+ defaults;
688
+ http;
689
+ list(params = {}, options) {
690
+ return this.conn.call(
691
+ "matches.list",
692
+ {
693
+ locale: this.defaults.locale,
694
+ timezone: this.defaults.timezone,
695
+ ...params
696
+ },
697
+ options
698
+ );
699
+ }
700
+ get(params, options) {
701
+ return this.conn.call(
702
+ "matches.get",
703
+ { locale: this.defaults.locale, ...params },
704
+ options
705
+ );
706
+ }
707
+ trending(params = {}, options) {
708
+ return this.conn.call(
709
+ "matches.trending",
710
+ {
711
+ locale: this.defaults.locale,
712
+ timezone: this.defaults.timezone,
713
+ ...params
714
+ },
715
+ options
716
+ );
717
+ }
718
+ search(params, options) {
719
+ return this.conn.call(
720
+ "matches.search",
721
+ { locale: this.defaults.locale, ...params },
722
+ options
723
+ );
724
+ }
725
+ async subscribeLive(params = {}, options) {
726
+ const merged = { locale: this.defaults.locale, ...params };
727
+ let handle;
728
+ const wsPromise = this.conn.registerSubscription(
729
+ "matches.subscribeLive",
730
+ merged,
731
+ {
732
+ onEvent: (event) => handle?._deliverEvent(event),
733
+ onClosed: (p) => handle?._deliverClosed(p.reason, p.message),
734
+ onSnapshot: (newSubId, newSnap) => handle?._deliverSnapshot(newSubId, newSnap)
735
+ },
736
+ options
737
+ );
738
+ const httpPromise = this.http.call(
739
+ "matches.getLive",
740
+ merged,
741
+ options
742
+ ).then((snapshot) => ({ kind: "http", snapshot }));
743
+ const wsRace = wsPromise.then((r) => ({
744
+ kind: "ws",
745
+ subId: r.subId,
746
+ snapshot: r.snapshot
747
+ }));
748
+ let winner;
749
+ try {
750
+ winner = await Promise.race([httpPromise, wsRace]);
751
+ } catch {
752
+ const ws = await wsPromise;
753
+ handle = new Subscription(
754
+ this.conn,
755
+ ws.subId,
756
+ ws.snapshot
757
+ );
758
+ return handle;
759
+ }
760
+ if (winner.kind === "ws") {
761
+ handle = new Subscription(
762
+ this.conn,
763
+ winner.subId,
764
+ winner.snapshot
765
+ );
766
+ return handle;
767
+ }
768
+ handle = new Subscription(
769
+ this.conn,
770
+ PENDING_SUB_ID,
771
+ winner.snapshot
772
+ );
773
+ wsPromise.then((r) => {
774
+ handle._deliverSnapshot(r.subId, r.snapshot);
775
+ }).catch((err) => {
776
+ handle._deliverClosed(
777
+ "internal_error",
778
+ err instanceof Error ? err.message : String(err)
779
+ );
780
+ });
781
+ return handle;
782
+ }
783
+ async subscribe(params, options) {
784
+ const merged = { locale: this.defaults.locale, ...params };
785
+ let handle;
786
+ const { subId, snapshot } = await this.conn.registerSubscription(
787
+ "matches.subscribe",
788
+ merged,
789
+ {
790
+ onEvent: (event) => handle?._deliverEvent(event),
791
+ onClosed: (p) => handle?._deliverClosed(p.reason, p.message),
792
+ onSnapshot: (newSubId, newSnap) => handle?._deliverSnapshot(newSubId, newSnap)
793
+ },
794
+ options
795
+ );
796
+ handle = new Subscription(this.conn, subId, snapshot);
797
+ return handle;
798
+ }
799
+ };
800
+
801
+ // src/resources/ai.ts
802
+ var Picks = class {
803
+ constructor(conn, defaults) {
804
+ this.conn = conn;
805
+ this.defaults = defaults;
806
+ }
807
+ conn;
808
+ defaults;
809
+ list(params = {}, options) {
810
+ return this.conn.call(
811
+ "ai.picks.list",
812
+ { locale: this.defaults.locale, ...params },
813
+ options
814
+ );
815
+ }
816
+ };
817
+ var Recommendations = class {
818
+ constructor(conn, defaults) {
819
+ this.conn = conn;
820
+ this.defaults = defaults;
821
+ }
822
+ conn;
823
+ defaults;
824
+ list(params = {}, options) {
825
+ return this.conn.call(
826
+ "ai.recommendations.list",
827
+ { locale: this.defaults.locale, ...params },
828
+ options
829
+ );
830
+ }
831
+ };
832
+ var Ai = class {
833
+ picks;
834
+ recommendations;
835
+ constructor(conn, defaults) {
836
+ this.picks = new Picks(conn, defaults);
837
+ this.recommendations = new Recommendations(conn, defaults);
838
+ }
839
+ };
840
+
841
+ // src/resources/tokens.ts
842
+ var Tokens = class {
843
+ constructor(conn) {
844
+ this.conn = conn;
845
+ }
846
+ conn;
847
+ /**
848
+ * Mint a short-lived JWT auth token.
849
+ *
850
+ * @param params.ttl Token lifetime in seconds. Default 600 (10 min).
851
+ * Server-enforced bounds: min 60, max 86400 (24h).
852
+ * @returns { token, expiresAt } — `token` is opaque to the customer; pass
853
+ * it to `@mrdoge/client`. `expiresAt` is ISO-8601.
854
+ */
855
+ create(params = {}, options) {
856
+ return this.conn.call("tokens.create", params, options);
857
+ }
858
+ };
859
+
860
+ // src/client.ts
861
+ var DEFAULT_BASE_URL = "wss://api.mrdoge.co/sdk/v1";
862
+ function wsToHttp(url) {
863
+ if (url.startsWith("wss://")) return "https://" + url.slice("wss://".length);
864
+ if (url.startsWith("ws://")) return "http://" + url.slice("ws://".length);
865
+ return url;
866
+ }
867
+ var MrDoge = class {
868
+ regions;
869
+ competitions;
870
+ teams;
871
+ matches;
872
+ ai;
873
+ tokens;
874
+ connection;
875
+ http;
876
+ constructor(options) {
877
+ if (!options?.apiKey) throw new Error("MrDoge: `apiKey` is required");
878
+ const config = {
879
+ ...DEFAULT_CONFIG,
880
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
881
+ apiKey: options.apiKey,
882
+ locale: options.locale,
883
+ timezone: options.timezone,
884
+ requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_CONFIG.requestTimeoutMs,
885
+ maxReconnectAttempts: options.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts,
886
+ reconnectBackoff: {
887
+ ...DEFAULT_CONFIG.reconnectBackoff,
888
+ ...options.reconnectBackoff
889
+ },
890
+ compression: options.compression ?? DEFAULT_CONFIG.compression
891
+ };
892
+ this.connection = new Connection(config);
893
+ this.http = createHttpClient({
894
+ apiKey: options.apiKey,
895
+ baseUrl: wsToHttp(config.baseUrl),
896
+ locale: options.locale,
897
+ timezone: options.timezone,
898
+ requestTimeoutMs: config.requestTimeoutMs
899
+ });
900
+ const defaults = { locale: options.locale, timezone: options.timezone };
901
+ this.regions = new Regions(this.connection, defaults);
902
+ this.competitions = new Competitions(this.connection, defaults);
903
+ this.teams = new Teams(this.connection, defaults);
904
+ this.matches = new Matches(this.connection, defaults, this.http);
905
+ this.ai = new Ai(this.connection, defaults);
906
+ this.tokens = new Tokens(this.connection);
907
+ }
908
+ /**
909
+ * Listen to connection lifecycle events.
910
+ */
911
+ on(event, fn) {
912
+ return this.connection.on(event, fn);
913
+ }
914
+ /**
915
+ * Force-open the connection now instead of waiting for the first call.
916
+ * Returns the welcome payload from the server.
917
+ */
918
+ async connect() {
919
+ return this.connection.connect();
920
+ }
921
+ /**
922
+ * Close the connection and cancel every active subscription. The client is
923
+ * unusable after `close()`.
924
+ */
925
+ async close() {
926
+ await this.connection.close();
927
+ }
928
+ };
929
+ export {
930
+ AbortError,
931
+ ConnectionError,
932
+ ConnectionLimitError,
933
+ DEFAULT_BASE_URL,
934
+ DisconnectedError,
935
+ ForbiddenError,
936
+ InternalError,
937
+ MrDoge,
938
+ MrDogeError,
939
+ NotFoundError,
940
+ ProtocolError,
941
+ RateLimitError,
942
+ Subscription,
943
+ SubscriptionLimitError,
944
+ TimeoutError,
945
+ UnauthorizedError,
946
+ UnavailableError,
947
+ ValidationError
948
+ };
949
+ //# sourceMappingURL=index.mjs.map