@ekmanss/5eplay 20260716.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ekmanss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # @ekmanss/5eplay
2
+
3
+ Typed 5EPlay CS2 match details and live updates for Node.js 22+. The collector uses 5EPlay's
4
+ JSON endpoints and MQTT-over-WebSocket feed directly; it does not launch a browser, execute page
5
+ JavaScript, scrape rendered DOM, or require a logged-in account.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @ekmanss/5eplay
11
+ ```
12
+
13
+ ## Complete match snapshot
14
+
15
+ ```ts
16
+ import { getFiveEPlayMatch } from '@ekmanss/5eplay';
17
+
18
+ const result = await getFiveEPlayMatch(
19
+ 'https://event.5eplay.com/csgo/matches/csgo_mc_2395709',
20
+ );
21
+
22
+ console.log(result.data.match);
23
+ console.log(result.data.maps);
24
+ console.log(result.data.current);
25
+ ```
26
+
27
+ The default snapshot includes every public match-data section used by the page:
28
+
29
+ - match, tournament, teams, ranks, odds, best-of, stage, and series score;
30
+ - complete map veto and every picked/decider map, including temporarily omitted upcoming maps;
31
+ - per-map scores, halves, round-result codes, live timer/bomb state, and player equipment;
32
+ - completed-map Data Overview metrics and the 5-by-5 Player Comparison kill matrix;
33
+ - complete historical logs and the current map's available log history;
34
+ - pre-match team/player/map analysis, recent matches, and head-to-head history;
35
+ - public player and big-event community rating cards.
36
+
37
+ Chat messages, login state, cookies, and account-specific actions are deliberately excluded.
38
+
39
+ `getFiveEPlayMatch()` accepts either the canonical URL or a `csgo_mc_<id>` identifier. It returns
40
+ `{ data, diagnostics }`. Independent HTTP sections are fetched concurrently; a normal capture does
41
+ not open the realtime credential endpoint.
42
+
43
+ ## Currently live matches
44
+
45
+ Use the list API for frequent checks that only need to know whether a CS2 match has started:
46
+
47
+ ```ts
48
+ import { getFiveEPlayLiveMatches } from '@ekmanss/5eplay';
49
+
50
+ const result = await getFiveEPlayLiveMatches();
51
+
52
+ if (result.data.hasLiveMatches) {
53
+ for (const match of result.data.matches) {
54
+ console.log(match.id, match.url, match.teams, match.currentMap);
55
+ }
56
+ }
57
+ ```
58
+
59
+ This method normally makes one small public list request and never fetches match details, analysis,
60
+ logs, community ratings, Markdown, or realtime credentials. The source list also contains upcoming
61
+ matches, so the collector strictly keeps series with live state (including the interval between two
62
+ maps, when `currentMap` is `null`). If the first 20 source rows are all live, it continues paging
63
+ until every live match is collected.
64
+
65
+ For serialized five-second polling without overlapping requests:
66
+
67
+ ```ts
68
+ import { setTimeout as delay } from 'node:timers/promises';
69
+ import { getFiveEPlayLiveMatches } from '@ekmanss/5eplay';
70
+
71
+ while (true) {
72
+ const { data } = await getFiveEPlayLiveMatches({ timeoutMs: 5_000 });
73
+ console.log(data.hasLiveMatches, data.matches.map((match) => match.url));
74
+ await delay(5_000);
75
+ }
76
+ ```
77
+
78
+ ## Generate a formatted Markdown report
79
+
80
+ From this repository, pass a match URL/ID and either an exact `.md` filename or a directory:
81
+
82
+ ```bash
83
+ pnpm 5eplay:md -- \
84
+ 'https://event.5eplay.com/csgo/matches/csgo_mc_2395709' \
85
+ './outputs/5eplay-report.md'
86
+ ```
87
+
88
+ ```bash
89
+ pnpm 5eplay:md -- 'csgo_mc_2395709' './outputs'
90
+ ```
91
+
92
+ The directory form writes `./outputs/csgo_mc_2395709.md`. Parent directories are created
93
+ automatically, and the completed report atomically replaces the destination file. The report
94
+ contains match/map details, analysis, complete event logs, and sanitized request diagnostics.
95
+ Community ratings are intentionally omitted and are not requested by the Markdown writer.
96
+
97
+ The human-readable report is deliberately more compact than the typed snapshot:
98
+
99
+ - the overview keeps only match status/identity/version/format, capture time, and source URL;
100
+ - completed and live maps show scores, clear half/round results, player tables, duels, highlights,
101
+ milestones, and readable logs; upcoming maps use one short placeholder;
102
+ - head-to-head and recent matches show only time, teams/opponent, score, and result;
103
+ - player power is reduced to Rating plus firepower, entry, opening, utility, sniping, clutch, and
104
+ trading scores;
105
+ - transport versions, coordinates, raw round codes, provider JSON, odds, logos, and community
106
+ ratings stay out of the Markdown report.
107
+
108
+ These presentation choices do not remove fields from `getFiveEPlayMatch()` or realtime snapshots.
109
+
110
+ Installed packages expose the same command as `5eplay-match-md` and a programmatic API:
111
+
112
+ ```ts
113
+ import { writeFiveEPlayMatchMarkdown } from '@ekmanss/5eplay';
114
+
115
+ const written = await writeFiveEPlayMatchMarkdown(
116
+ 'csgo_mc_2395709',
117
+ '/absolute/path/to/report.md',
118
+ );
119
+
120
+ console.log(written.outputPath, written.bytes);
121
+ ```
122
+
123
+ Use `renderFiveEPlayMatchMarkdown(result)` instead when the match has already been fetched and the
124
+ caller wants the Markdown string without writing it.
125
+
126
+ ## Realtime session
127
+
128
+ ```ts
129
+ import { createFiveEPlayMatchSession } from '@ekmanss/5eplay';
130
+
131
+ await using session = await createFiveEPlayMatchSession('csgo_mc_2395709');
132
+
133
+ for await (const update of session) {
134
+ if (update.type === 'state') {
135
+ console.log(update.snapshot.current?.teams);
136
+ } else if (update.type === 'log') {
137
+ console.log(update.event.kind, update.event.kill);
138
+ }
139
+ }
140
+ ```
141
+
142
+ The first yielded item is the complete HTTP snapshot. Later `state` updates contain the latest
143
+ scoreboard snapshot and `log` updates contain one typed event. `session.snapshot()` returns the
144
+ latest merged state at any time. The session uses one authorized MQTT connection for match state
145
+ and one for event logs, sends keepalives, deduplicates log versions, and reconnects with a bounded
146
+ backoff after an unexpected disconnect.
147
+
148
+ Always close a session, either with `await using`, `await session.close()`, or a `finally` block.
149
+
150
+ ## Options
151
+
152
+ ```ts
153
+ const result = await getFiveEPlayMatch(match, {
154
+ timeoutMs: 15_000,
155
+ signal: abortController.signal,
156
+ includeAnalysis: true,
157
+ includeLogs: true,
158
+ includeCommunityRatings: true,
159
+ onProgress: (event) => console.error(event.stage, event.message),
160
+ });
161
+ ```
162
+
163
+ The three `include*` flags default to `true`. Disable optional sections when only a compact live
164
+ scoreboard is required.
165
+
166
+ ## Errors
167
+
168
+ Failures throw `FiveEPlayError` with stable `code`, `operation`, `stage`, and `retryable` fields.
169
+ Realtime credentials are kept inside the MQTT connection and never appear in data, diagnostics,
170
+ progress events, or errors.
171
+
172
+ ## Documentation
173
+
174
+ - [Data contracts](docs/data-contracts.md)
175
+ - [Manual recipes](docs/recipes.md)
176
+
177
+ ## Disclaimer
178
+
179
+ This is an unofficial project and is not affiliated with or endorsed by 5EPlay. Consumers are
180
+ responsible for complying with applicable terms and using reasonable request rates.
181
+
182
+ ## License
183
+
184
+ [MIT](LICENSE) © 2026 ekmanss
@@ -0,0 +1,6 @@
1
+ export declare class AsyncQueue<T> implements AsyncIterable<T> {
2
+ #private;
3
+ push(value: T): void;
4
+ close(error?: unknown): void;
5
+ [Symbol.asyncIterator](): AsyncIterator<T>;
6
+ }
@@ -0,0 +1,44 @@
1
+ export class AsyncQueue {
2
+ #values = [];
3
+ #waiters = [];
4
+ #closed = false;
5
+ #error;
6
+ push(value) {
7
+ if (this.#closed)
8
+ return;
9
+ const waiter = this.#waiters.shift();
10
+ if (waiter)
11
+ waiter.resolve({ done: false, value });
12
+ else
13
+ this.#values.push(value);
14
+ }
15
+ close(error) {
16
+ if (this.#closed)
17
+ return;
18
+ this.#closed = true;
19
+ this.#error = error;
20
+ for (const waiter of this.#waiters.splice(0)) {
21
+ if (error === undefined)
22
+ waiter.resolve({ done: true, value: undefined });
23
+ else
24
+ waiter.reject(error);
25
+ }
26
+ }
27
+ [Symbol.asyncIterator]() {
28
+ return {
29
+ next: async () => {
30
+ const value = this.#values.shift();
31
+ if (value !== undefined)
32
+ return { done: false, value };
33
+ if (this.#closed) {
34
+ if (this.#error !== undefined)
35
+ throw this.#error;
36
+ return { done: true, value: undefined };
37
+ }
38
+ return await new Promise((resolve, reject) => {
39
+ this.#waiters.push({ resolve, reject });
40
+ });
41
+ },
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,11 @@
1
+ import { type CommunityCapture, type LogCapture } from './transform.js';
2
+ import type { FiveEPlayClientOptions, FiveEPlayMatchIdentity, FiveEPlayRequestOptions, GetFiveEPlayMatchResult } from './types.js';
3
+ export interface CapturedFiveEPlayMatch {
4
+ result: GetFiveEPlayMatchResult;
5
+ identity: FiveEPlayMatchIdentity;
6
+ detailData: unknown;
7
+ analysisData: unknown | null;
8
+ logs: Map<number, LogCapture>;
9
+ community: CommunityCapture | null;
10
+ }
11
+ export declare function captureFiveEPlayMatch(input: string, clientOptions?: FiveEPlayClientOptions, requestOptions?: FiveEPlayRequestOptions): Promise<CapturedFiveEPlayMatch>;
@@ -0,0 +1,199 @@
1
+ import { asFiveEPlayError, FiveEPlayError } from './errors.js';
2
+ import { fetchJson, responseData } from './http.js';
3
+ import { requireMatchIdentity } from './input.js';
4
+ import { buildFiveEPlayMatch, } from './transform.js';
5
+ import { integer, record, records, text } from './value.js';
6
+ const DATA_API = 'https://esports-data.5eplaycdn.com/v1/api/csgo';
7
+ const COMMUNITY_API = 'https://app.5eplay.com/api/score';
8
+ function emit(options, stage, message) {
9
+ options.onProgress?.({
10
+ operation: 'match-detail',
11
+ stage,
12
+ message,
13
+ timestamp: new Date().toISOString(),
14
+ });
15
+ }
16
+ function requestLifetime(options) {
17
+ const controller = new AbortController();
18
+ const timeoutMs = options.timeoutMs ?? 15_000;
19
+ const timeout = setTimeout(() => {
20
+ controller.abort(new FiveEPlayError(`5EPlay operation timed out after ${timeoutMs}ms`, {
21
+ code: 'TIMEOUT', operation: 'match-detail', stage: 'fetching-match', retryable: true,
22
+ }));
23
+ }, timeoutMs);
24
+ const abort = () => controller.abort(options.signal?.reason);
25
+ if (options.signal?.aborted)
26
+ abort();
27
+ else
28
+ options.signal?.addEventListener('abort', abort, { once: true });
29
+ return {
30
+ signal: controller.signal,
31
+ dispose: () => {
32
+ clearTimeout(timeout);
33
+ options.signal?.removeEventListener('abort', abort);
34
+ },
35
+ };
36
+ }
37
+ function matchApi(identity, suffix) {
38
+ return `${DATA_API}/matches/${identity.id}/${suffix}`;
39
+ }
40
+ async function optionalData(task, warnings, section) {
41
+ try {
42
+ return await task();
43
+ }
44
+ catch (error) {
45
+ warnings.push({
46
+ code: 'OPTIONAL_SECTION_UNAVAILABLE',
47
+ section,
48
+ reason: error instanceof Error ? error.message : String(error),
49
+ });
50
+ return null;
51
+ }
52
+ }
53
+ async function captureLogs(context, identity, detailData) {
54
+ const sourceMatch = record(record(detailData).match);
55
+ const activeBouts = records(sourceMatch.bouts_state).flatMap((bout) => {
56
+ const number = integer(bout.bout_num);
57
+ const status = integer(bout.status);
58
+ return number !== null && (status === 1 || status === 2) ? [{ number }] : [];
59
+ });
60
+ const entries = await Promise.all(activeBouts.map(async ({ number }) => {
61
+ const url = `${DATA_API}/match/${identity.id}/event/log?update_version=0&limit=500&bout_id=${identity.id}_${number}`;
62
+ const response = await fetchJson(context, url, {
63
+ kind: 'log', stage: 'fetching-logs', mapNumber: number,
64
+ });
65
+ const data = record(responseData(response, {
66
+ operation: 'match-detail', stage: 'fetching-logs', matchId: identity.id,
67
+ }));
68
+ return [number, {
69
+ complete: data.not_more === '1' || data.not_more === 1,
70
+ fromVersion: text(data.from_ver),
71
+ toVersion: text(data.to_ver),
72
+ rows: Array.isArray(data.list) ? data.list : [],
73
+ }];
74
+ }));
75
+ return new Map(entries);
76
+ }
77
+ async function captureCommunity(context, identity, tabsData) {
78
+ const tabs = Array.isArray(tabsData) ? tabsData : [];
79
+ const entries = await Promise.all(tabs.map(async (rawTab) => {
80
+ const tab = record(rawTab);
81
+ const tabName = text(tab.tab) ?? '';
82
+ const id = text(tab.id) ?? '';
83
+ const query = new URLSearchParams({
84
+ match_id: identity.id,
85
+ tab: tabName,
86
+ game_type: '1',
87
+ team_id: id,
88
+ });
89
+ const response = await fetchJson(context, `${COMMUNITY_API}/match_score_list?${query}`, {
90
+ kind: 'community-list', stage: 'fetching-community', tab: `${tabName}:${id}`,
91
+ });
92
+ const cards = responseData(response, {
93
+ operation: 'match-detail', stage: 'fetching-community', matchId: identity.id,
94
+ });
95
+ return [`${tabName}:${id}`, Array.isArray(cards) ? cards : []];
96
+ }));
97
+ return { tabs, cardsByTab: new Map(entries) };
98
+ }
99
+ export async function captureFiveEPlayMatch(input, clientOptions = {}, requestOptions = {}) {
100
+ const startedAt = new Date().toISOString();
101
+ emit(requestOptions, 'validating-input', 'Validating 5EPlay match input');
102
+ const identity = requireMatchIdentity(input);
103
+ const lifetime = requestLifetime(requestOptions);
104
+ const diagnostics = [];
105
+ const warnings = [];
106
+ const context = {
107
+ operation: 'match-detail',
108
+ matchId: identity.id,
109
+ signal: lifetime.signal,
110
+ fetch: clientOptions.fetch ?? globalThis.fetch,
111
+ diagnostics,
112
+ };
113
+ if (typeof context.fetch !== 'function') {
114
+ lifetime.dispose();
115
+ throw new FiveEPlayError('global fetch is unavailable', {
116
+ code: 'INTERNAL_ERROR', operation: 'match-detail', stage: 'fetching-match', retryable: false,
117
+ matchId: identity.id,
118
+ });
119
+ }
120
+ try {
121
+ emit(requestOptions, 'fetching-match', 'Fetching 5EPlay match snapshot');
122
+ const detailPromise = fetchJson(context, matchApi(identity, 'data'), {
123
+ kind: 'match', stage: 'fetching-match',
124
+ }).then((value) => responseData(value, {
125
+ operation: 'match-detail', stage: 'fetching-match', matchId: identity.id,
126
+ }));
127
+ const analysisPromise = requestOptions.includeAnalysis === false ? Promise.resolve(null)
128
+ : optionalData(async () => {
129
+ emit(requestOptions, 'fetching-analysis', 'Fetching pre-match analysis');
130
+ const value = await fetchJson(context, matchApi(identity, 'analysis_v1'), {
131
+ kind: 'analysis', stage: 'fetching-analysis',
132
+ });
133
+ return responseData(value, {
134
+ operation: 'match-detail', stage: 'fetching-analysis', matchId: identity.id,
135
+ });
136
+ }, warnings, 'analysis');
137
+ const tabsPromise = requestOptions.includeCommunityRatings === false ? Promise.resolve(null)
138
+ : optionalData(async () => {
139
+ emit(requestOptions, 'fetching-community', 'Fetching community rating tabs');
140
+ const query = new URLSearchParams({ match_id: identity.id, game_type: '1' });
141
+ const value = await fetchJson(context, `${COMMUNITY_API}/match_score_tab?${query}`, {
142
+ kind: 'community-tabs', stage: 'fetching-community',
143
+ });
144
+ return responseData(value, {
145
+ operation: 'match-detail', stage: 'fetching-community', matchId: identity.id,
146
+ });
147
+ }, warnings, 'communityRatings');
148
+ const [detailData, analysisData, tabsData] = await Promise.all([
149
+ detailPromise, analysisPromise, tabsPromise,
150
+ ]);
151
+ const sourceId = text(record(record(record(detailData).match).mc_info).id);
152
+ if (sourceId !== identity.id) {
153
+ throw new FiveEPlayError('5EPlay match payload identity does not match the request', {
154
+ code: 'INVALID_RESPONSE', operation: 'match-detail', stage: 'fetching-match',
155
+ retryable: true, matchId: identity.id, details: { sourceId },
156
+ });
157
+ }
158
+ const logsPromise = requestOptions.includeLogs === false
159
+ ? Promise.resolve(new Map())
160
+ : (emit(requestOptions, 'fetching-logs', 'Fetching complete map logs'),
161
+ captureLogs(context, identity, detailData));
162
+ const communityPromise = tabsData === null ? Promise.resolve(null)
163
+ : optionalData(() => captureCommunity(context, identity, tabsData), warnings, 'communityRatings');
164
+ const [logs, community] = await Promise.all([logsPromise, communityPromise]);
165
+ emit(requestOptions, 'building-output', 'Building typed 5EPlay match output');
166
+ const capturedAt = new Date().toISOString();
167
+ const data = buildFiveEPlayMatch({
168
+ identity, capturedAt, detailData, analysisData, logs, community,
169
+ });
170
+ const completedAt = new Date().toISOString();
171
+ const result = {
172
+ data,
173
+ diagnostics: {
174
+ schemaVersion: '1.0.0',
175
+ operation: 'match-detail',
176
+ startedAt,
177
+ completedAt,
178
+ durationMs: Math.max(0, Date.parse(completedAt) - Date.parse(startedAt)),
179
+ input: identity,
180
+ requests: diagnostics,
181
+ warnings,
182
+ },
183
+ };
184
+ emit(requestOptions, 'completed', '5EPlay match capture completed');
185
+ return { result, identity, detailData, analysisData, logs, community };
186
+ }
187
+ catch (error) {
188
+ if (lifetime.signal.aborted && lifetime.signal.reason instanceof Error) {
189
+ throw lifetime.signal.reason;
190
+ }
191
+ throw asFiveEPlayError(error, {
192
+ code: 'INTERNAL_ERROR', operation: 'match-detail', stage: 'fetching-match',
193
+ retryable: false, matchId: identity.id,
194
+ });
195
+ }
196
+ finally {
197
+ lifetime.dispose();
198
+ }
199
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { writeFiveEPlayMatchMarkdown } from './write_markdown.js';
3
+ function usage() {
4
+ return [
5
+ '用法:',
6
+ ' 5eplay-match-md <比赛URL或ID> <输出.md或输出目录>',
7
+ '',
8
+ '示例:',
9
+ " 5eplay-match-md 'https://event.5eplay.com/csgo/matches/csgo_mc_2395709' './outputs/report.md'",
10
+ " 5eplay-match-md 'csgo_mc_2395709' './outputs'",
11
+ ].join('\n');
12
+ }
13
+ const rawArguments = process.argv.slice(2);
14
+ const arguments_ = rawArguments[0] === '--' ? rawArguments.slice(1) : rawArguments;
15
+ if (arguments_.includes('--help') || arguments_.includes('-h')) {
16
+ process.stdout.write(`${usage()}\n`);
17
+ process.exit(0);
18
+ }
19
+ if (arguments_.length !== 2) {
20
+ process.stderr.write(`${usage()}\n`);
21
+ process.exit(2);
22
+ }
23
+ const [input, target] = arguments_;
24
+ try {
25
+ const written = await writeFiveEPlayMatchMarkdown(input, target, {
26
+ timeoutMs: 30_000,
27
+ onProgress: (event) => process.stderr.write(`[${event.stage}] ${event.message}\n`),
28
+ });
29
+ process.stdout.write(`${JSON.stringify({
30
+ outputPath: written.outputPath,
31
+ bytes: written.bytes,
32
+ matchId: written.result.data.match.id,
33
+ status: written.result.data.match.status,
34
+ maps: written.result.data.maps.length,
35
+ }, null, 2)}\n`);
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ process.stderr.write(`生成 5EPlay Markdown 失败:${message}\n`);
40
+ process.exitCode = 1;
41
+ }
@@ -0,0 +1,2 @@
1
+ import type { GetFiveEPlayMatchOptions, GetFiveEPlayMatchResult } from './types.js';
2
+ export declare function getFiveEPlayMatch(input: string, options?: GetFiveEPlayMatchOptions): Promise<GetFiveEPlayMatchResult>;
package/dist/client.js ADDED
@@ -0,0 +1,4 @@
1
+ import { captureFiveEPlayMatch } from './capture.js';
2
+ export async function getFiveEPlayMatch(input, options = {}) {
3
+ return (await captureFiveEPlayMatch(input, options, options)).result;
4
+ }
@@ -0,0 +1,20 @@
1
+ import type { FiveEPlayErrorCode, FiveEPlayOperation, FiveEPlayStage } from './types.js';
2
+ export interface FiveEPlayErrorOptions {
3
+ code: FiveEPlayErrorCode;
4
+ operation: FiveEPlayOperation;
5
+ stage: FiveEPlayStage;
6
+ retryable: boolean;
7
+ matchId?: string;
8
+ details?: Record<string, unknown>;
9
+ cause?: unknown;
10
+ }
11
+ export declare class FiveEPlayError extends Error {
12
+ readonly code: FiveEPlayErrorCode;
13
+ readonly operation: FiveEPlayOperation;
14
+ readonly stage: FiveEPlayStage;
15
+ readonly retryable: boolean;
16
+ readonly matchId?: string;
17
+ readonly details?: Record<string, unknown>;
18
+ constructor(message: string, options: FiveEPlayErrorOptions);
19
+ }
20
+ export declare function asFiveEPlayError(error: unknown, fallback: FiveEPlayErrorOptions): FiveEPlayError;
package/dist/errors.js ADDED
@@ -0,0 +1,42 @@
1
+ function safeCause(cause) {
2
+ if (cause === undefined)
3
+ return undefined;
4
+ const message = (cause instanceof Error ? cause.message : String(cause))
5
+ .replace(/(client_id|username|password)["'=:\s]+[^,}\s]+/gi, '$1=***')
6
+ .replace(/(https?:\/\/|wss:\/\/)[^\s/@:]+:[^\s/@]+@/gi, '$1***:***@');
7
+ return new Error(message);
8
+ }
9
+ export class FiveEPlayError extends Error {
10
+ code;
11
+ operation;
12
+ stage;
13
+ retryable;
14
+ matchId;
15
+ details;
16
+ constructor(message, options) {
17
+ super(message, { cause: safeCause(options.cause) });
18
+ this.name = 'FiveEPlayError';
19
+ this.code = options.code;
20
+ this.operation = options.operation;
21
+ this.stage = options.stage;
22
+ this.retryable = options.retryable;
23
+ this.matchId = options.matchId;
24
+ this.details = options.details;
25
+ }
26
+ }
27
+ export function asFiveEPlayError(error, fallback) {
28
+ if (error instanceof FiveEPlayError)
29
+ return error;
30
+ if (error instanceof DOMException && error.name === 'AbortError') {
31
+ return new FiveEPlayError('5EPlay operation was aborted', {
32
+ ...fallback,
33
+ code: 'ABORTED',
34
+ retryable: false,
35
+ cause: error,
36
+ });
37
+ }
38
+ return new FiveEPlayError(error instanceof Error ? error.message : String(error), {
39
+ ...fallback,
40
+ cause: error,
41
+ });
42
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { FiveEPlayOperation, FiveEPlayRequestDiagnostic, FiveEPlayStage } from './types.js';
2
+ export interface RequestContext {
3
+ operation: FiveEPlayOperation;
4
+ matchId?: string;
5
+ signal: AbortSignal;
6
+ fetch: typeof globalThis.fetch;
7
+ diagnostics: FiveEPlayRequestDiagnostic[];
8
+ }
9
+ export interface FetchJsonOptions {
10
+ kind: FiveEPlayRequestDiagnostic['kind'];
11
+ stage: FiveEPlayStage;
12
+ method?: 'GET' | 'POST';
13
+ body?: unknown;
14
+ mapNumber?: number;
15
+ tab?: string;
16
+ page?: number;
17
+ }
18
+ export declare function fetchJson(context: RequestContext, url: string, options: FetchJsonOptions): Promise<unknown>;
19
+ export declare function responseData(value: unknown, context: {
20
+ operation: FiveEPlayOperation;
21
+ stage: FiveEPlayStage;
22
+ matchId?: string;
23
+ }): unknown;