refactor match3d runtime adapters
This commit is contained in:
@@ -6,51 +6,64 @@ import type {
|
|||||||
} from '../packages/shared/src/contracts/match3dRuntime';
|
} from '../packages/shared/src/contracts/match3dRuntime';
|
||||||
import { Match3DRuntimeShell } from './components/match3d-runtime';
|
import { Match3DRuntimeShell } from './components/match3d-runtime';
|
||||||
import {
|
import {
|
||||||
confirmLocalMatch3DClick,
|
createLocalMatch3DRuntimeAdapter,
|
||||||
resolveLocalMatch3DTimer,
|
type Match3DRuntimeAdapter,
|
||||||
startLocalMatch3DRun,
|
startLocalMatch3DRun,
|
||||||
} from './services/match3d-runtime';
|
} from './services/match3d-runtime';
|
||||||
|
|
||||||
function buildInitialRun() {
|
type LocalMatch3DRuntimeSession = {
|
||||||
|
adapter: Match3DRuntimeAdapter;
|
||||||
|
initialRun: Match3DRunSnapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveClearCountParam() {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const clearCountParam = params.get('clearCount') ?? params.get('count');
|
const clearCountParam = params.get('clearCount') ?? params.get('count');
|
||||||
const clearCount =
|
const clearCount =
|
||||||
clearCountParam === null ? 12 : Number.parseInt(clearCountParam, 10);
|
clearCountParam === null ? 12 : Number.parseInt(clearCountParam, 10);
|
||||||
return startLocalMatch3DRun(
|
return Number.isFinite(clearCount) && clearCount > 0 ? clearCount : 12;
|
||||||
Number.isFinite(clearCount) && clearCount > 0 ? clearCount : 12,
|
}
|
||||||
);
|
|
||||||
|
function buildInitialRuntimeSession(): LocalMatch3DRuntimeSession {
|
||||||
|
const initialRun = startLocalMatch3DRun(resolveClearCountParam());
|
||||||
|
return {
|
||||||
|
adapter: createLocalMatch3DRuntimeAdapter({ initialRun }),
|
||||||
|
initialRun,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Match3DPlaygroundApp() {
|
export default function Match3DPlaygroundApp() {
|
||||||
const [run, setRun] = useState<Match3DRunSnapshot>(buildInitialRun);
|
const runtimeSessionRef = useRef(buildInitialRuntimeSession());
|
||||||
const authorityRunRef = useRef(run);
|
const [run, setRun] = useState<Match3DRunSnapshot>(
|
||||||
|
runtimeSessionRef.current.initialRun,
|
||||||
|
);
|
||||||
|
|
||||||
const syncRun = useCallback((nextRun: Match3DRunSnapshot) => {
|
const syncRun = useCallback((nextRun: Match3DRunSnapshot) => {
|
||||||
setRun(nextRun);
|
setRun(nextRun);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClickItem = useCallback(async (payload: Match3DClickItemRequest) => {
|
const handleClickItem = useCallback(async (payload: Match3DClickItemRequest) => {
|
||||||
const result = await confirmLocalMatch3DClick(authorityRunRef.current, payload);
|
const runId = payload.runId ?? runtimeSessionRef.current.initialRun.runId;
|
||||||
authorityRunRef.current = result.run;
|
const result = await runtimeSessionRef.current.adapter.clickItem(runId, payload);
|
||||||
setRun(result.run);
|
setRun(result.run);
|
||||||
return result;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRestart = useCallback(() => {
|
const handleRestart = useCallback(() => {
|
||||||
const nextRun = buildInitialRun();
|
void runtimeSessionRef.current.adapter.restartRun(run.runId).then(({ run }) => {
|
||||||
authorityRunRef.current = nextRun;
|
setRun(run);
|
||||||
setRun(nextRun);
|
});
|
||||||
}, []);
|
}, [run.runId]);
|
||||||
|
|
||||||
const handleExit = useCallback(() => {
|
const handleExit = useCallback(() => {
|
||||||
window.location.assign('/');
|
window.location.assign('/');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTimeExpired = useCallback(() => {
|
const handleTimeExpired = useCallback(() => {
|
||||||
const nextRun = resolveLocalMatch3DTimer(authorityRunRef.current);
|
void runtimeSessionRef.current.adapter.finishTimeUp(run.runId).then(({ run }) => {
|
||||||
authorityRunRef.current = nextRun;
|
setRun(run);
|
||||||
setRun(nextRun);
|
});
|
||||||
}, []);
|
}, [run.runId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Match3DRuntimeShell
|
<Match3DRuntimeShell
|
||||||
|
|||||||
@@ -104,10 +104,6 @@ import {
|
|||||||
ApiClientError,
|
ApiClientError,
|
||||||
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||||
} from '../../services/apiClient';
|
} from '../../services/apiClient';
|
||||||
import {
|
|
||||||
fetchCreationEntryConfig,
|
|
||||||
type CreationEntryConfig,
|
|
||||||
} from '../../services/creationEntryConfigService';
|
|
||||||
import {
|
import {
|
||||||
getPublicAuthUserByCode,
|
getPublicAuthUserByCode,
|
||||||
getPublicAuthUserById,
|
getPublicAuthUserById,
|
||||||
@@ -132,6 +128,10 @@ import {
|
|||||||
deleteBigFishWork,
|
deleteBigFishWork,
|
||||||
listBigFishWorks,
|
listBigFishWorks,
|
||||||
} from '../../services/big-fish-works';
|
} from '../../services/big-fish-works';
|
||||||
|
import {
|
||||||
|
type CreationEntryConfig,
|
||||||
|
fetchCreationEntryConfig,
|
||||||
|
} from '../../services/creationEntryConfigService';
|
||||||
import {
|
import {
|
||||||
cancelCreativeAgentSession,
|
cancelCreativeAgentSession,
|
||||||
confirmCreativePuzzleTemplate,
|
confirmCreativePuzzleTemplate,
|
||||||
@@ -144,13 +144,7 @@ import {
|
|||||||
shouldRestoreCustomWorldAgentUiState,
|
shouldRestoreCustomWorldAgentUiState,
|
||||||
} from '../../services/customWorldAgentUiState';
|
} from '../../services/customWorldAgentUiState';
|
||||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||||
import {
|
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
|
||||||
clickMatch3DItem,
|
|
||||||
finishMatch3DTimeUp,
|
|
||||||
restartMatch3DRun,
|
|
||||||
startMatch3DRun,
|
|
||||||
stopMatch3DRun,
|
|
||||||
} from '../../services/match3d-runtime';
|
|
||||||
import {
|
import {
|
||||||
deleteMatch3DWork,
|
deleteMatch3DWork,
|
||||||
getMatch3DWorkDetail,
|
getMatch3DWorkDetail,
|
||||||
@@ -2599,6 +2593,10 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const match3dRuntimeAdapter = useMemo(
|
||||||
|
() => createServerMatch3DRuntimeAdapter(),
|
||||||
|
[],
|
||||||
|
);
|
||||||
const match3dFlow = usePlatformCreationAgentFlowController<
|
const match3dFlow = usePlatformCreationAgentFlowController<
|
||||||
Match3DAgentSessionSnapshot,
|
Match3DAgentSessionSnapshot,
|
||||||
CreateMatch3DSessionRequest,
|
CreateMatch3DSessionRequest,
|
||||||
@@ -4376,11 +4374,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { run } = options.embedded
|
const { run } = options.embedded
|
||||||
? await startMatch3DRun(
|
? await match3dRuntimeAdapter.startRun(
|
||||||
profile.profileId,
|
profile.profileId,
|
||||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||||
)
|
)
|
||||||
: await startMatch3DRun(profile.profileId);
|
: await match3dRuntimeAdapter.startRun(profile.profileId);
|
||||||
setMatch3DRun(run);
|
setMatch3DRun(run);
|
||||||
setMatch3DRuntimeReturnStage(returnStage);
|
setMatch3DRuntimeReturnStage(returnStage);
|
||||||
if (!options.embedded) {
|
if (!options.embedded) {
|
||||||
@@ -4412,6 +4410,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
[
|
[
|
||||||
isMatch3DBusy,
|
isMatch3DBusy,
|
||||||
match3dFlow,
|
match3dFlow,
|
||||||
|
match3dRuntimeAdapter,
|
||||||
resolveMatch3DErrorMessage,
|
resolveMatch3DErrorMessage,
|
||||||
setMatch3DError,
|
setMatch3DError,
|
||||||
setSelectionStage,
|
setSelectionStage,
|
||||||
@@ -6671,7 +6670,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
match3dFlow.setIsBusy(true);
|
match3dFlow.setIsBusy(true);
|
||||||
setMatch3DError(null);
|
setMatch3DError(null);
|
||||||
void restartMatch3DRun(match3dRun.runId)
|
void match3dRuntimeAdapter.restartRun(match3dRun.runId)
|
||||||
.then(({ run }) => {
|
.then(({ run }) => {
|
||||||
setMatch3DRun(run);
|
setMatch3DRun(run);
|
||||||
})
|
})
|
||||||
@@ -6693,14 +6692,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
if (!runId) {
|
if (!runId) {
|
||||||
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
|
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
|
||||||
}
|
}
|
||||||
return clickMatch3DItem(runId, payload);
|
return match3dRuntimeAdapter.clickItem(runId, payload);
|
||||||
}}
|
}}
|
||||||
onTimeExpired={() => {
|
onTimeExpired={() => {
|
||||||
if (!match3dRun?.runId) {
|
if (!match3dRun?.runId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void finishMatch3DTimeUp(match3dRun.runId)
|
void match3dRuntimeAdapter.finishTimeUp(match3dRun.runId)
|
||||||
.then(({ run }) => {
|
.then(({ run }) => {
|
||||||
setMatch3DRun(run);
|
setMatch3DRun(run);
|
||||||
})
|
})
|
||||||
@@ -6868,6 +6867,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
match3dError,
|
match3dError,
|
||||||
match3dFlow,
|
match3dFlow,
|
||||||
match3dRun,
|
match3dRun,
|
||||||
|
match3dRuntimeAdapter,
|
||||||
platformBootstrap.platformTab,
|
platformBootstrap.platformTab,
|
||||||
platformThemeClass,
|
platformThemeClass,
|
||||||
puzzleError,
|
puzzleError,
|
||||||
@@ -8560,7 +8560,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
error={match3dError}
|
error={match3dError}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
if (match3dRun?.runId && match3dRun.status === 'running') {
|
if (match3dRun?.runId && match3dRun.status === 'running') {
|
||||||
void stopMatch3DRun(match3dRun.runId).catch(
|
void match3dRuntimeAdapter.stopRun(match3dRun.runId).catch(
|
||||||
() => undefined,
|
() => undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8573,7 +8573,7 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
match3dFlow.setIsBusy(true);
|
match3dFlow.setIsBusy(true);
|
||||||
setMatch3DError(null);
|
setMatch3DError(null);
|
||||||
void restartMatch3DRun(match3dRun.runId)
|
void match3dRuntimeAdapter.restartRun(match3dRun.runId)
|
||||||
.then(({ run }) => {
|
.then(({ run }) => {
|
||||||
setMatch3DRun(run);
|
setMatch3DRun(run);
|
||||||
})
|
})
|
||||||
@@ -8597,14 +8597,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
new Error('抓大鹅运行态缺少 runId。'),
|
new Error('抓大鹅运行态缺少 runId。'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return clickMatch3DItem(runId, payload);
|
return match3dRuntimeAdapter.clickItem(runId, payload);
|
||||||
}}
|
}}
|
||||||
onTimeExpired={() => {
|
onTimeExpired={() => {
|
||||||
if (!match3dRun?.runId) {
|
if (!match3dRun?.runId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void finishMatch3DTimeUp(match3dRun.runId)
|
void match3dRuntimeAdapter.finishTimeUp(match3dRun.runId)
|
||||||
.then(({ run }) => {
|
.then(({ run }) => {
|
||||||
setMatch3DRun(run);
|
setMatch3DRun(run);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||||
import {
|
import {
|
||||||
clickMatch3DItem,
|
clickMatch3DItem,
|
||||||
|
createServerMatch3DRuntimeAdapter,
|
||||||
finishMatch3DTimeUp,
|
finishMatch3DTimeUp,
|
||||||
restartMatch3DRun,
|
restartMatch3DRun,
|
||||||
startMatch3DRun,
|
startMatch3DRun,
|
||||||
@@ -348,14 +349,35 @@ vi.mock('../../services/match3d-works', () => ({
|
|||||||
listMatch3DWorks: vi.fn(),
|
listMatch3DWorks: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../services/match3d-runtime', () => ({
|
const match3dRuntimeServiceMocks = vi.hoisted(() => ({
|
||||||
clickMatch3DItem: vi.fn(),
|
clickMatch3DItem: vi.fn(),
|
||||||
|
createServerMatch3DRuntimeAdapter: vi.fn(),
|
||||||
finishMatch3DTimeUp: vi.fn(),
|
finishMatch3DTimeUp: vi.fn(),
|
||||||
restartMatch3DRun: vi.fn(),
|
restartMatch3DRun: vi.fn(),
|
||||||
startMatch3DRun: vi.fn(),
|
startMatch3DRun: vi.fn(),
|
||||||
stopMatch3DRun: vi.fn(),
|
stopMatch3DRun: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const match3dServerRuntimeAdapterMock = vi.hoisted(() => ({
|
||||||
|
clickItem: vi.fn(),
|
||||||
|
finishTimeUp: vi.fn(),
|
||||||
|
getRun: vi.fn(),
|
||||||
|
restartRun: vi.fn(),
|
||||||
|
startRun: vi.fn(),
|
||||||
|
stopRun: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../services/match3d-runtime', async () => {
|
||||||
|
const actual = await vi.importActual<
|
||||||
|
typeof import('../../services/match3d-runtime')
|
||||||
|
>('../../services/match3d-runtime');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
...match3dRuntimeServiceMocks,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock('../../services/square-hole-creation', () => ({
|
vi.mock('../../services/square-hole-creation', () => ({
|
||||||
squareHoleCreationClient: {
|
squareHoleCreationClient: {
|
||||||
createSession: vi.fn(),
|
createSession: vi.fn(),
|
||||||
@@ -1453,6 +1475,24 @@ function TestWrapper({
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
|
vi.mocked(createServerMatch3DRuntimeAdapter).mockReturnValue(
|
||||||
|
match3dServerRuntimeAdapterMock,
|
||||||
|
);
|
||||||
|
match3dServerRuntimeAdapterMock.startRun.mockRejectedValue(
|
||||||
|
new Error('未启动抓大鹅运行态'),
|
||||||
|
);
|
||||||
|
match3dServerRuntimeAdapterMock.clickItem.mockRejectedValue(
|
||||||
|
new Error('未执行抓大鹅点击'),
|
||||||
|
);
|
||||||
|
match3dServerRuntimeAdapterMock.restartRun.mockRejectedValue(
|
||||||
|
new Error('未重新开始抓大鹅运行态'),
|
||||||
|
);
|
||||||
|
match3dServerRuntimeAdapterMock.finishTimeUp.mockResolvedValue({
|
||||||
|
run: buildMockMatch3DRun('match3d-profile-time-up'),
|
||||||
|
});
|
||||||
|
match3dServerRuntimeAdapterMock.stopRun.mockResolvedValue({
|
||||||
|
run: buildMockMatch3DRun('match3d-profile-stopped'),
|
||||||
|
});
|
||||||
window.history.replaceState(null, '', '/');
|
window.history.replaceState(null, '', '/');
|
||||||
window.sessionStorage.clear();
|
window.sessionStorage.clear();
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
@@ -4465,7 +4505,7 @@ test('public code search opens a published Match3D work by M3 code and starts ru
|
|||||||
vi.mocked(listMatch3DGallery).mockResolvedValue({
|
vi.mocked(listMatch3DGallery).mockResolvedValue({
|
||||||
items: [match3dWork],
|
items: [match3dWork],
|
||||||
});
|
});
|
||||||
vi.mocked(startMatch3DRun).mockResolvedValue({
|
match3dServerRuntimeAdapterMock.startRun.mockResolvedValue({
|
||||||
run: buildMockMatch3DRun(match3dWork.profileId),
|
run: buildMockMatch3DRun(match3dWork.profileId),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4483,7 +4523,9 @@ test('public code search opens a published Match3D work by M3 code and starts ru
|
|||||||
await user.click(screen.getByRole('button', { name: '启动' }));
|
await user.click(screen.getByRole('button', { name: '启动' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(startMatch3DRun).toHaveBeenCalledWith('match3d-profile-public-1');
|
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||||
|
'match3d-profile-public-1',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText('抓大鹅运行态:match3d-run-match3d-profile-public-1'),
|
await screen.findByText('抓大鹅运行态:match3d-run-match3d-profile-public-1'),
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ export {
|
|||||||
startLocalMatch3DRun,
|
startLocalMatch3DRun,
|
||||||
stopLocalMatch3DRun,
|
stopLocalMatch3DRun,
|
||||||
} from './match3dLocalRuntime';
|
} from './match3dLocalRuntime';
|
||||||
|
export {
|
||||||
|
createLocalMatch3DRuntimeAdapter,
|
||||||
|
createServerMatch3DRuntimeAdapter,
|
||||||
|
type Match3DRuntimeAdapter,
|
||||||
|
} from './match3dRuntimeAdapter';
|
||||||
export {
|
export {
|
||||||
clickMatch3DItem,
|
clickMatch3DItem,
|
||||||
finishMatch3DTimeUp,
|
finishMatch3DTimeUp,
|
||||||
|
|||||||
@@ -481,12 +481,17 @@ export function buildLocalMatch3DOptimisticRun(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForLocalConfirmation(delayMs: number) {
|
||||||
|
const scheduler = globalThis.setTimeout;
|
||||||
|
return new Promise((resolve) => scheduler(resolve, delayMs));
|
||||||
|
}
|
||||||
|
|
||||||
export async function confirmLocalMatch3DClick(
|
export async function confirmLocalMatch3DClick(
|
||||||
run: Match3DRunSnapshot,
|
run: Match3DRunSnapshot,
|
||||||
request: Match3DClickItemRequest,
|
request: Match3DClickItemRequest,
|
||||||
): Promise<Match3DClickItemResult> {
|
): Promise<Match3DClickItemResult> {
|
||||||
// 中文注释:F3 阶段用本地函数模拟后端权威确认,真实接口接入后保留同一结果语义。
|
// 中文注释:F3 阶段用本地函数模拟后端权威确认,真实接口接入后保留同一结果语义。
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, 180));
|
await waitForLocalConfirmation(180);
|
||||||
const timedRun = normalizeRemainingMs(run);
|
const timedRun = normalizeRemainingMs(run);
|
||||||
if (timedRun.status !== 'Running') {
|
if (timedRun.status !== 'Running') {
|
||||||
return {
|
return {
|
||||||
|
|||||||
144
src/services/match3d-runtime/match3dRuntimeAdapter.test.ts
Normal file
144
src/services/match3d-runtime/match3dRuntimeAdapter.test.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Match3DClickItemRequest,
|
||||||
|
Match3DRunResponse,
|
||||||
|
Match3DRunSnapshot,
|
||||||
|
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||||
|
import {
|
||||||
|
createLocalMatch3DRuntimeAdapter,
|
||||||
|
createServerMatch3DRuntimeAdapter,
|
||||||
|
type Match3DRuntimeAdapter,
|
||||||
|
startLocalMatch3DRun,
|
||||||
|
} from './index';
|
||||||
|
|
||||||
|
function buildMockRun(runId: string): Match3DRunSnapshot {
|
||||||
|
return {
|
||||||
|
runId,
|
||||||
|
profileId: 'server-profile-1',
|
||||||
|
ownerUserId: 'server-owner-1',
|
||||||
|
status: 'Running',
|
||||||
|
snapshotVersion: 1,
|
||||||
|
startedAtMs: 1_700_000_000_000,
|
||||||
|
durationLimitMs: 30_000,
|
||||||
|
serverNowMs: 1_700_000_000_000,
|
||||||
|
remainingMs: 30_000,
|
||||||
|
clearCount: 3,
|
||||||
|
totalItemCount: 0,
|
||||||
|
clearedItemCount: 0,
|
||||||
|
boardVersion: 1,
|
||||||
|
items: [],
|
||||||
|
traySlots: [],
|
||||||
|
failureReason: null,
|
||||||
|
lastConfirmedActionId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('server Match3D runtime adapter forwards the full runtime seam lazily', async () => {
|
||||||
|
const startResponse: Match3DRunResponse = { run: buildMockRun('server-run-start') };
|
||||||
|
const getResponse: Match3DRunResponse = { run: buildMockRun('server-run-get') };
|
||||||
|
const restartResponse: Match3DRunResponse = { run: buildMockRun('server-run-restart') };
|
||||||
|
const stopResponse: Match3DRunResponse = {
|
||||||
|
run: { ...buildMockRun('server-run-stop'), status: 'Stopped' },
|
||||||
|
};
|
||||||
|
const finishResponse: Match3DRunResponse = {
|
||||||
|
run: { ...buildMockRun('server-run-finish'), status: 'Timeout' },
|
||||||
|
};
|
||||||
|
const clickPayload: Match3DClickItemRequest = {
|
||||||
|
runId: 'server-run-start',
|
||||||
|
itemInstanceId: 'item-1',
|
||||||
|
clientActionId: 'action-1',
|
||||||
|
clientEventId: 'event-1',
|
||||||
|
clickedAtMs: 1_700_000_000_001,
|
||||||
|
clientSnapshotVersion: 1,
|
||||||
|
};
|
||||||
|
const dependencies = {
|
||||||
|
clickItem: vi.fn().mockResolvedValue({
|
||||||
|
status: 'Accepted' as const,
|
||||||
|
run: buildMockRun('server-run-click'),
|
||||||
|
}),
|
||||||
|
finishTimeUp: vi.fn().mockResolvedValue(finishResponse),
|
||||||
|
getRun: vi.fn().mockResolvedValue(getResponse),
|
||||||
|
restartRun: vi.fn().mockResolvedValue(restartResponse),
|
||||||
|
startRun: vi.fn().mockResolvedValue(startResponse),
|
||||||
|
stopRun: vi.fn().mockResolvedValue(stopResponse),
|
||||||
|
};
|
||||||
|
const adapter = createServerMatch3DRuntimeAdapter(dependencies);
|
||||||
|
|
||||||
|
expect(await adapter.startRun('server-profile-1', { skipRefresh: true })).toBe(
|
||||||
|
startResponse,
|
||||||
|
);
|
||||||
|
expect(await adapter.getRun('server-run-start')).toBe(getResponse);
|
||||||
|
expect(await adapter.clickItem('server-run-start', clickPayload)).toEqual({
|
||||||
|
status: 'Accepted',
|
||||||
|
run: buildMockRun('server-run-click'),
|
||||||
|
});
|
||||||
|
expect(await adapter.restartRun('server-run-start')).toBe(restartResponse);
|
||||||
|
expect(await adapter.stopRun('server-run-restart')).toBe(stopResponse);
|
||||||
|
expect(await adapter.finishTimeUp('server-run-start')).toBe(finishResponse);
|
||||||
|
|
||||||
|
expect(dependencies.startRun).toHaveBeenCalledWith('server-profile-1', {
|
||||||
|
skipRefresh: true,
|
||||||
|
});
|
||||||
|
expect(dependencies.getRun).toHaveBeenCalledWith('server-run-start');
|
||||||
|
expect(dependencies.clickItem).toHaveBeenCalledWith(
|
||||||
|
'server-run-start',
|
||||||
|
clickPayload,
|
||||||
|
);
|
||||||
|
expect(dependencies.restartRun).toHaveBeenCalledWith('server-run-start');
|
||||||
|
expect(dependencies.stopRun).toHaveBeenCalledWith('server-run-restart');
|
||||||
|
expect(dependencies.finishTimeUp).toHaveBeenCalledWith('server-run-start');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local Match3D runtime adapter exposes the same runtime seam as the server client', async () => {
|
||||||
|
const adapter = createLocalMatch3DRuntimeAdapter({ clearCount: 1 });
|
||||||
|
const started = await adapter.startRun('ignored-local-profile');
|
||||||
|
const clickableItem = started.run.items.find((item) => item.clickable);
|
||||||
|
|
||||||
|
expect(started.run.profileId).toBe('local-match3d-profile');
|
||||||
|
expect(clickableItem).toBeTruthy();
|
||||||
|
|
||||||
|
const clickResult = await adapter.clickItem(started.run.runId, {
|
||||||
|
runId: started.run.runId,
|
||||||
|
itemInstanceId: clickableItem!.itemInstanceId,
|
||||||
|
clientActionId: 'local-click-1',
|
||||||
|
clientEventId: 'local-event-1',
|
||||||
|
clickedAtMs: started.run.serverNowMs ?? Date.now(),
|
||||||
|
clientSnapshotVersion: started.run.snapshotVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(clickResult.status).toBe('Accepted');
|
||||||
|
expect(clickResult.run.snapshotVersion).toBe(started.run.snapshotVersion + 1);
|
||||||
|
|
||||||
|
const restarted = await adapter.restartRun(started.run.runId);
|
||||||
|
expect(restarted.run.runId).not.toBe(started.run.runId);
|
||||||
|
|
||||||
|
const stopped = await adapter.stopRun(restarted.run.runId);
|
||||||
|
expect(stopped.run.status).toBe('Stopped');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local Match3D runtime adapter keeps authority run local to the adapter', async () => {
|
||||||
|
const adapter = createLocalMatch3DRuntimeAdapter({ initialRun: startLocalMatch3DRun(1) });
|
||||||
|
const first = await adapter.getRun('unused-run-id');
|
||||||
|
const timedOut = await adapter.finishTimeUp(first.run.runId);
|
||||||
|
|
||||||
|
expect(timedOut.run.status).toBe('Running');
|
||||||
|
expect(timedOut.run.runId).toBe(first.run.runId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('server and local Match3D runtime adapters share the same runtime seam', () => {
|
||||||
|
const adapters: Match3DRuntimeAdapter[] = [
|
||||||
|
createLocalMatch3DRuntimeAdapter({ clearCount: 1 }),
|
||||||
|
createServerMatch3DRuntimeAdapter(),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(adapters).toHaveLength(2);
|
||||||
|
for (const adapter of adapters) {
|
||||||
|
expect(typeof adapter.startRun).toBe('function');
|
||||||
|
expect(typeof adapter.getRun).toBe('function');
|
||||||
|
expect(typeof adapter.clickItem).toBe('function');
|
||||||
|
expect(typeof adapter.restartRun).toBe('function');
|
||||||
|
expect(typeof adapter.stopRun).toBe('function');
|
||||||
|
expect(typeof adapter.finishTimeUp).toBe('function');
|
||||||
|
}
|
||||||
|
});
|
||||||
106
src/services/match3d-runtime/match3dRuntimeAdapter.ts
Normal file
106
src/services/match3d-runtime/match3dRuntimeAdapter.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import type {
|
||||||
|
Match3DClickItemRequest,
|
||||||
|
Match3DClickItemResult,
|
||||||
|
Match3DRunResponse,
|
||||||
|
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||||
|
import {
|
||||||
|
confirmLocalMatch3DClick,
|
||||||
|
resolveLocalMatch3DTimer,
|
||||||
|
startLocalMatch3DRun,
|
||||||
|
stopLocalMatch3DRun,
|
||||||
|
} from './match3dLocalRuntime';
|
||||||
|
import {
|
||||||
|
clickMatch3DItem,
|
||||||
|
finishMatch3DTimeUp,
|
||||||
|
getMatch3DRun,
|
||||||
|
type Match3DRuntimeRequestOptions,
|
||||||
|
restartMatch3DRun,
|
||||||
|
startMatch3DRun,
|
||||||
|
stopMatch3DRun,
|
||||||
|
} from './match3dRuntimeClient';
|
||||||
|
|
||||||
|
export type Match3DRuntimeAdapter = {
|
||||||
|
startRun: (
|
||||||
|
profileId: string,
|
||||||
|
options?: Match3DRuntimeRequestOptions,
|
||||||
|
) => Promise<Match3DRunResponse>;
|
||||||
|
getRun: (runId: string) => Promise<Match3DRunResponse>;
|
||||||
|
clickItem: (
|
||||||
|
runId: string,
|
||||||
|
payload: Match3DClickItemRequest,
|
||||||
|
) => Promise<Match3DClickItemResult>;
|
||||||
|
restartRun: (runId: string) => Promise<Match3DRunResponse>;
|
||||||
|
stopRun: (runId: string) => Promise<Match3DRunResponse>;
|
||||||
|
finishTimeUp: (runId: string) => Promise<Match3DRunResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LocalMatch3DRuntimeAdapterOptions = {
|
||||||
|
clearCount?: number;
|
||||||
|
initialRun?: Match3DRunResponse['run'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServerMatch3DRuntimeAdapterDependencies = {
|
||||||
|
clickItem: typeof clickMatch3DItem;
|
||||||
|
finishTimeUp: typeof finishMatch3DTimeUp;
|
||||||
|
getRun: typeof getMatch3DRun;
|
||||||
|
restartRun: typeof restartMatch3DRun;
|
||||||
|
startRun: typeof startMatch3DRun;
|
||||||
|
stopRun: typeof stopMatch3DRun;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultServerMatch3DRuntimeAdapterDependencies: ServerMatch3DRuntimeAdapterDependencies = {
|
||||||
|
clickItem: clickMatch3DItem,
|
||||||
|
finishTimeUp: finishMatch3DTimeUp,
|
||||||
|
getRun: getMatch3DRun,
|
||||||
|
restartRun: restartMatch3DRun,
|
||||||
|
startRun: startMatch3DRun,
|
||||||
|
stopRun: stopMatch3DRun,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createServerMatch3DRuntimeAdapter(
|
||||||
|
dependencies: ServerMatch3DRuntimeAdapterDependencies =
|
||||||
|
defaultServerMatch3DRuntimeAdapterDependencies,
|
||||||
|
): Match3DRuntimeAdapter {
|
||||||
|
return {
|
||||||
|
clickItem: (runId, payload) => dependencies.clickItem(runId, payload),
|
||||||
|
finishTimeUp: (runId) => dependencies.finishTimeUp(runId),
|
||||||
|
getRun: (runId) => dependencies.getRun(runId),
|
||||||
|
restartRun: (runId) => dependencies.restartRun(runId),
|
||||||
|
startRun: (profileId, options) => dependencies.startRun(profileId, options),
|
||||||
|
stopRun: (runId) => dependencies.stopRun(runId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLocalMatch3DRuntimeAdapter(
|
||||||
|
options: LocalMatch3DRuntimeAdapterOptions = {},
|
||||||
|
): Match3DRuntimeAdapter {
|
||||||
|
let authorityRun = options.initialRun ?? startLocalMatch3DRun(options.clearCount);
|
||||||
|
|
||||||
|
return {
|
||||||
|
async startRun() {
|
||||||
|
authorityRun = startLocalMatch3DRun(options.clearCount);
|
||||||
|
return { run: authorityRun };
|
||||||
|
},
|
||||||
|
async getRun() {
|
||||||
|
authorityRun = resolveLocalMatch3DTimer(authorityRun);
|
||||||
|
return { run: authorityRun };
|
||||||
|
},
|
||||||
|
async clickItem(_runId, payload) {
|
||||||
|
const result = await confirmLocalMatch3DClick(authorityRun, payload);
|
||||||
|
authorityRun = result.run;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
async restartRun() {
|
||||||
|
authorityRun = startLocalMatch3DRun(options.clearCount);
|
||||||
|
return { run: authorityRun };
|
||||||
|
},
|
||||||
|
async stopRun() {
|
||||||
|
authorityRun = stopLocalMatch3DRun(authorityRun);
|
||||||
|
return { run: authorityRun };
|
||||||
|
},
|
||||||
|
async finishTimeUp() {
|
||||||
|
authorityRun = resolveLocalMatch3DTimer(authorityRun);
|
||||||
|
return { run: authorityRun };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ const MATCH3D_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
|||||||
maxDelayMs: 360,
|
maxDelayMs: 360,
|
||||||
retryUnsafeMethods: true,
|
retryUnsafeMethods: true,
|
||||||
};
|
};
|
||||||
type Match3DRuntimeRequestOptions = Pick<
|
export type Match3DRuntimeRequestOptions = Pick<
|
||||||
ApiRequestOptions,
|
ApiRequestOptions,
|
||||||
| 'authImpact'
|
| 'authImpact'
|
||||||
| 'skipRefresh'
|
| 'skipRefresh'
|
||||||
|
|||||||
Reference in New Issue
Block a user