refactor: 收口 runtime client 请求骨架
This commit is contained in:
@@ -10,14 +10,11 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import {
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
import {
|
||||
buildRuntimeGuestAuthOptions,
|
||||
buildRuntimeGuestHeaders,
|
||||
type RuntimeGuestRequestOptions,
|
||||
} from '../runtimeGuestAuth';
|
||||
import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth';
|
||||
import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest';
|
||||
|
||||
const MATCH3D_RUNTIME_API_BASE = '/api/runtime/match3d';
|
||||
const MATCH3D_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
@@ -74,39 +71,30 @@ export function startMatch3DRun(
|
||||
profileId: string,
|
||||
options: Match3DRuntimeRequestOptions = {},
|
||||
) {
|
||||
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||
const payload: StartMatch3DRunRequest = {
|
||||
profileId,
|
||||
itemTypeCountOverride: options.itemTypeCountOverride ?? null,
|
||||
};
|
||||
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/works/${encodeURIComponent(profileId)}/runs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: buildRuntimeGuestHeaders(options, {
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'启动抓大鹅玩法失败',
|
||||
{
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
...requestOptions,
|
||||
},
|
||||
);
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'works', profileId, 'runs'),
|
||||
method: 'POST',
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '启动抓大鹅玩法失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取抓大鹅运行态快照。
|
||||
*/
|
||||
export function getMatch3DRun(runId: string) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取抓大鹅运行快照失败',
|
||||
{ retry: MATCH3D_RUNTIME_READ_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId),
|
||||
fallbackMessage: '读取抓大鹅运行快照失败',
|
||||
retry: MATCH3D_RUNTIME_READ_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,19 +104,16 @@ export async function clickMatch3DItem(
|
||||
runId: string,
|
||||
payload: Match3DClickItemRequest,
|
||||
) {
|
||||
const response = await requestJson<Match3DClickResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/click`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
runId: payload.runId ?? runId,
|
||||
}),
|
||||
const response = await requestRuntimeJson<Match3DClickResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'click'),
|
||||
method: 'POST',
|
||||
jsonBody: {
|
||||
...payload,
|
||||
runId: payload.runId ?? runId,
|
||||
},
|
||||
'确认抓大鹅点击失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
fallbackMessage: '确认抓大鹅点击失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
|
||||
return mapClickConfirmation(payload, response.confirmation);
|
||||
}
|
||||
@@ -142,40 +127,37 @@ export function stopMatch3DRun(
|
||||
clientActionId: `match3d-stop-${Date.now()}`,
|
||||
},
|
||||
) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/stop`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'停止抓大鹅玩法失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'stop'),
|
||||
method: 'POST',
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '停止抓大鹅玩法失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前 run 重开一局。
|
||||
*/
|
||||
export function restartMatch3DRun(runId: string) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/restart`,
|
||||
{ method: 'POST' },
|
||||
'重新开始抓大鹅玩法失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'restart'),
|
||||
method: 'POST',
|
||||
fallbackMessage: '重新开始抓大鹅玩法失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端倒计时归零后通知后端确认失败状态。
|
||||
*/
|
||||
export function finishMatch3DTimeUp(runId: string) {
|
||||
return requestJson<Match3DRunResponse>(
|
||||
`/api/runtime/match3d/runs/${encodeURIComponent(runId)}/time-up`,
|
||||
{ method: 'POST' },
|
||||
'同步抓大鹅倒计时失败',
|
||||
{ retry: MATCH3D_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<Match3DRunResponse>({
|
||||
url: buildRuntimeApiPath(MATCH3D_RUNTIME_API_BASE, 'runs', runId, 'time-up'),
|
||||
method: 'POST',
|
||||
fallbackMessage: '同步抓大鹅倒计时失败',
|
||||
retry: MATCH3D_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
export const match3dRuntimeClient = {
|
||||
|
||||
88
src/services/runtimeRequest.test.ts
Normal file
88
src/services/runtimeRequest.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const apiClientMocks = vi.hoisted(() => ({
|
||||
requestJson: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./apiClient', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('./apiClient')>('./apiClient');
|
||||
return {
|
||||
...actual,
|
||||
requestJson: apiClientMocks.requestJson,
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
buildRuntimeApiPath,
|
||||
requestRuntimeJson,
|
||||
} from './runtimeRequest';
|
||||
|
||||
describe('runtimeRequest', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
apiClientMocks.requestJson.mockResolvedValue({ ok: true });
|
||||
});
|
||||
|
||||
it('builds encoded runtime api paths', () => {
|
||||
expect(buildRuntimeApiPath('/api/runtime/demo/', 'work/a b', 'run/1')).toBe(
|
||||
'/api/runtime/demo/work%2Fa%20b/run%2F1',
|
||||
);
|
||||
});
|
||||
|
||||
it('sends json runtime requests with guest auth and retry options', async () => {
|
||||
const retry = { maxRetries: 1, retryUnsafeMethods: true };
|
||||
|
||||
await requestRuntimeJson({
|
||||
url: '/api/runtime/demo/runs',
|
||||
method: 'POST',
|
||||
jsonBody: { profileId: 'profile-1' },
|
||||
fallbackMessage: '启动失败',
|
||||
retry,
|
||||
requestOptions: {
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
|
||||
'/api/runtime/demo/runs',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer runtime-guest-token',
|
||||
},
|
||||
body: JSON.stringify({ profileId: 'profile-1' }),
|
||||
},
|
||||
'启动失败',
|
||||
{
|
||||
retry,
|
||||
authImpact: undefined,
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: undefined,
|
||||
clearAuthOnUnauthorized: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('omits empty headers and body for plain runtime reads', async () => {
|
||||
await requestRuntimeJson({
|
||||
url: '/api/runtime/demo/runs/run-1',
|
||||
fallbackMessage: '读取失败',
|
||||
});
|
||||
|
||||
expect(apiClientMocks.requestJson).toHaveBeenCalledWith(
|
||||
'/api/runtime/demo/runs/run-1',
|
||||
{ method: 'GET' },
|
||||
'读取失败',
|
||||
{
|
||||
authImpact: undefined,
|
||||
skipAuth: undefined,
|
||||
skipRefresh: undefined,
|
||||
notifyAuthStateChange: undefined,
|
||||
clearAuthOnUnauthorized: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
62
src/services/runtimeRequest.ts
Normal file
62
src/services/runtimeRequest.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from './apiClient';
|
||||
import {
|
||||
buildRuntimeGuestAuthOptions,
|
||||
buildRuntimeGuestHeaders,
|
||||
type RuntimeGuestRequestOptions,
|
||||
} from './runtimeGuestAuth';
|
||||
|
||||
export type RuntimeJsonRequestParams = {
|
||||
url: string;
|
||||
method?: string;
|
||||
jsonBody?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
fallbackMessage: string;
|
||||
retry?: ApiRetryOptions;
|
||||
requestOptions?: RuntimeGuestRequestOptions;
|
||||
};
|
||||
|
||||
export function buildRuntimeApiPath(
|
||||
basePath: string,
|
||||
...segments: string[]
|
||||
) {
|
||||
const normalizedBasePath = basePath.endsWith('/')
|
||||
? basePath.slice(0, -1)
|
||||
: basePath;
|
||||
return [
|
||||
normalizedBasePath,
|
||||
...segments.map((segment) => encodeURIComponent(segment)),
|
||||
].join('/');
|
||||
}
|
||||
|
||||
export function requestRuntimeJson<T>(params: RuntimeJsonRequestParams) {
|
||||
const {
|
||||
fallbackMessage,
|
||||
headers = {},
|
||||
jsonBody,
|
||||
method = 'GET',
|
||||
requestOptions = {},
|
||||
retry,
|
||||
url,
|
||||
} = params;
|
||||
const hasJsonBody = jsonBody !== undefined;
|
||||
const requestHeaders = buildRuntimeGuestHeaders(requestOptions, {
|
||||
...(hasJsonBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
...headers,
|
||||
});
|
||||
const init: RequestInit = {
|
||||
method,
|
||||
...(Object.keys(requestHeaders).length > 0
|
||||
? { headers: requestHeaders }
|
||||
: {}),
|
||||
...(hasJsonBody ? { body: JSON.stringify(jsonBody) } : {}),
|
||||
};
|
||||
const authOptions = buildRuntimeGuestAuthOptions(requestOptions);
|
||||
|
||||
return requestJson<T>(url, init, fallbackMessage, {
|
||||
...(retry ? { retry } : {}),
|
||||
...authOptions,
|
||||
});
|
||||
}
|
||||
@@ -6,14 +6,11 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/squareHoleRuntime';
|
||||
import {
|
||||
type ApiRetryOptions,
|
||||
requestJson,
|
||||
} from '../apiClient';
|
||||
import {
|
||||
buildRuntimeGuestAuthOptions,
|
||||
buildRuntimeGuestHeaders,
|
||||
type RuntimeGuestRequestOptions,
|
||||
} from '../runtimeGuestAuth';
|
||||
import { type RuntimeGuestRequestOptions } from '../runtimeGuestAuth';
|
||||
import { buildRuntimeApiPath, requestRuntimeJson } from '../runtimeRequest';
|
||||
|
||||
const SQUARE_HOLE_RUNTIME_API_BASE = '/api/runtime/square-hole';
|
||||
const SQUARE_HOLE_RUNTIME_READ_RETRY: ApiRetryOptions = {
|
||||
maxRetries: 1,
|
||||
baseDelayMs: 120,
|
||||
@@ -34,34 +31,30 @@ export function startSquareHoleRun(
|
||||
profileId: string,
|
||||
options: SquareHoleRuntimeRequestOptions = {},
|
||||
) {
|
||||
const requestOptions = buildRuntimeGuestAuthOptions(options);
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/works/${encodeURIComponent(profileId)}/runs`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: buildRuntimeGuestHeaders(options, {
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify({ profileId }),
|
||||
},
|
||||
'启动方洞挑战失败',
|
||||
{
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
...requestOptions,
|
||||
},
|
||||
);
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
SQUARE_HOLE_RUNTIME_API_BASE,
|
||||
'works',
|
||||
profileId,
|
||||
'runs',
|
||||
),
|
||||
method: 'POST',
|
||||
jsonBody: { profileId },
|
||||
fallbackMessage: '启动方洞挑战失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
requestOptions: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取方洞挑战运行态快照。
|
||||
*/
|
||||
export function getSquareHoleRun(runId: string) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}`,
|
||||
{ method: 'GET' },
|
||||
'读取方洞挑战运行快照失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_READ_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(SQUARE_HOLE_RUNTIME_API_BASE, 'runs', runId),
|
||||
fallbackMessage: '读取方洞挑战运行快照失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_READ_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,19 +64,21 @@ export function dropSquareHoleShape(
|
||||
runId: string,
|
||||
payload: DropSquareHoleShapeRequest,
|
||||
) {
|
||||
return requestJson<SquareHoleDropResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/drop`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
runId: payload.runId ?? runId,
|
||||
}),
|
||||
return requestRuntimeJson<SquareHoleDropResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
SQUARE_HOLE_RUNTIME_API_BASE,
|
||||
'runs',
|
||||
runId,
|
||||
'drop',
|
||||
),
|
||||
method: 'POST',
|
||||
jsonBody: {
|
||||
...payload,
|
||||
runId: payload.runId ?? runId,
|
||||
},
|
||||
'确认方洞挑战投入失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
fallbackMessage: '确认方洞挑战投入失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,40 +90,47 @@ export function stopSquareHoleRun(
|
||||
clientActionId: `square-hole-stop-${Date.now()}`,
|
||||
},
|
||||
) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/stop`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
'停止方洞挑战失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(SQUARE_HOLE_RUNTIME_API_BASE, 'runs', runId, 'stop'),
|
||||
method: 'POST',
|
||||
jsonBody: payload,
|
||||
fallbackMessage: '停止方洞挑战失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于当前 run 重开一局。
|
||||
*/
|
||||
export function restartSquareHoleRun(runId: string) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/restart`,
|
||||
{ method: 'POST' },
|
||||
'重新开始方洞挑战失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
SQUARE_HOLE_RUNTIME_API_BASE,
|
||||
'runs',
|
||||
runId,
|
||||
'restart',
|
||||
),
|
||||
method: 'POST',
|
||||
fallbackMessage: '重新开始方洞挑战失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端倒计时归零后通知后端确认失败状态。
|
||||
*/
|
||||
export function finishSquareHoleTimeUp(runId: string) {
|
||||
return requestJson<SquareHoleRunResponse>(
|
||||
`/api/runtime/square-hole/runs/${encodeURIComponent(runId)}/time-up`,
|
||||
{ method: 'POST' },
|
||||
'同步方洞挑战倒计时失败',
|
||||
{ retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY },
|
||||
);
|
||||
return requestRuntimeJson<SquareHoleRunResponse>({
|
||||
url: buildRuntimeApiPath(
|
||||
SQUARE_HOLE_RUNTIME_API_BASE,
|
||||
'runs',
|
||||
runId,
|
||||
'time-up',
|
||||
),
|
||||
method: 'POST',
|
||||
fallbackMessage: '同步方洞挑战倒计时失败',
|
||||
retry: SQUARE_HOLE_RUNTIME_WRITE_RETRY,
|
||||
});
|
||||
}
|
||||
|
||||
export const squareHoleRuntimeClient = {
|
||||
|
||||
Reference in New Issue
Block a user