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