refactor match3d runtime adapters
This commit is contained in:
@@ -6,6 +6,11 @@ export {
|
||||
startLocalMatch3DRun,
|
||||
stopLocalMatch3DRun,
|
||||
} from './match3dLocalRuntime';
|
||||
export {
|
||||
createLocalMatch3DRuntimeAdapter,
|
||||
createServerMatch3DRuntimeAdapter,
|
||||
type Match3DRuntimeAdapter,
|
||||
} from './match3dRuntimeAdapter';
|
||||
export {
|
||||
clickMatch3DItem,
|
||||
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(
|
||||
run: Match3DRunSnapshot,
|
||||
request: Match3DClickItemRequest,
|
||||
): Promise<Match3DClickItemResult> {
|
||||
// 中文注释:F3 阶段用本地函数模拟后端权威确认,真实接口接入后保留同一结果语义。
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 180));
|
||||
await waitForLocalConfirmation(180);
|
||||
const timedRun = normalizeRemainingMs(run);
|
||||
if (timedRun.status !== 'Running') {
|
||||
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,
|
||||
retryUnsafeMethods: true,
|
||||
};
|
||||
type Match3DRuntimeRequestOptions = Pick<
|
||||
export type Match3DRuntimeRequestOptions = Pick<
|
||||
ApiRequestOptions,
|
||||
| 'authImpact'
|
||||
| 'skipRefresh'
|
||||
|
||||
Reference in New Issue
Block a user