@zhin.js/plugin-tic-tac-toe 1.0.10 → 1.0.13
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/commands/ttt/[[action]].js +2 -2
- package/commands/ttt/[[action]].ts +2 -2
- package/lib/board-view.d.ts +1 -1
- package/lib/models.d.ts +4 -0
- package/lib/models.js +2 -0
- package/lib/models.js.map +1 -1
- package/lib/runtime-store.d.ts +3 -2
- package/lib/runtime-store.js.map +1 -1
- package/lib/session-service.d.ts +20 -3
- package/lib/session-service.js +47 -17
- package/lib/session-service.js.map +1 -1
- package/package.json +6 -6
- package/plugin.js +1 -1
- package/src/board-view.ts +1 -1
- package/src/models.ts +6 -0
- package/src/runtime-store.ts +3 -3
- package/src/session-service.ts +61 -17
|
@@ -6,11 +6,11 @@ import { resolveGameServices } from "../../lib/runtime-store.js";
|
|
|
6
6
|
export default defineCommand({
|
|
7
7
|
description: 'Tic Tac Toe',
|
|
8
8
|
params: { action: { type: 'string', default: '' } },
|
|
9
|
-
async execute({ params, input, use
|
|
9
|
+
async execute({ params, input, use }) {
|
|
10
10
|
const action = normalizeTttAction(String(params.action ?? ''));
|
|
11
11
|
if (!action || action === 'help')
|
|
12
12
|
return TTT_HELP;
|
|
13
|
-
const services = resolveGameServices({ use
|
|
13
|
+
const services = resolveGameServices({ use });
|
|
14
14
|
const message = messageFromCommandInput(input);
|
|
15
15
|
return runTttCommand(services, message, action);
|
|
16
16
|
},
|
|
@@ -6,10 +6,10 @@ import { resolveGameServices } from '../../src/runtime-store.js';
|
|
|
6
6
|
export default defineCommand({
|
|
7
7
|
description: 'Tic Tac Toe',
|
|
8
8
|
params: { action: { type: 'string', default: '' } },
|
|
9
|
-
async execute({ params, input, use
|
|
9
|
+
async execute({ params, input, use }) {
|
|
10
10
|
const action = normalizeTttAction(String(params.action ?? ''));
|
|
11
11
|
if (!action || action === 'help') return TTT_HELP;
|
|
12
|
-
const services = resolveGameServices({ use
|
|
12
|
+
const services = resolveGameServices({ use });
|
|
13
13
|
const message = messageFromCommandInput(input);
|
|
14
14
|
return runTttCommand(services, message, action);
|
|
15
15
|
},
|
package/lib/board-view.d.ts
CHANGED
package/lib/models.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ declare module '@zhin.js/core' {
|
|
|
18
18
|
status: TttSessionStatus;
|
|
19
19
|
winner: number;
|
|
20
20
|
move_count: number;
|
|
21
|
+
/** 乐观锁版本(VersionedSessionService 契约) */
|
|
22
|
+
revision: number;
|
|
23
|
+
/** JSON 数组:已处理 actionId 历史(幂等去重) */
|
|
24
|
+
processed_actions: string;
|
|
21
25
|
updated_at: number;
|
|
22
26
|
created_at: number;
|
|
23
27
|
};
|
package/lib/models.js
CHANGED
|
@@ -15,6 +15,8 @@ export function defineHostTables(db) {
|
|
|
15
15
|
status: { type: 'text', default: 'active' },
|
|
16
16
|
winner: { type: 'integer', default: 0 },
|
|
17
17
|
move_count: { type: 'integer', default: 0 },
|
|
18
|
+
revision: { type: 'integer', default: 0 },
|
|
19
|
+
processed_actions: { type: 'text', default: '[]' },
|
|
18
20
|
updated_at: { type: 'integer', default: 0 },
|
|
19
21
|
created_at: { type: 'integer', default: 0 },
|
|
20
22
|
});
|
package/lib/models.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"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"}
|
package/lib/runtime-store.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type Token } from '@zhin.js/plugin-runtime';
|
|
1
2
|
import type { SessionServices } from './session-service.js';
|
|
2
3
|
/** Owner-scoped service consumed by discovered commands and middleware. */
|
|
3
|
-
export declare const gameServicesToken:
|
|
4
|
+
export declare const gameServicesToken: Token<SessionServices>;
|
|
4
5
|
export declare function resolveGameServices(context: {
|
|
5
|
-
use<T>(token:
|
|
6
|
+
use<T>(token: Token<T>): T;
|
|
6
7
|
}): SessionServices;
|
package/lib/runtime-store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-store.js","sourceRoot":"","sources":["../src/runtime-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"runtime-store.js","sourceRoot":"","sources":["../src/runtime-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAc,MAAM,yBAAyB,CAAC;AAGlE,2EAA2E;AAC3E,MAAM,CAAC,MAAM,iBAAiB,GAAG,WAAW,CAAkB,gCAAgC,CAAC,CAAC;AAEhG,MAAM,UAAU,mBAAmB,CAAC,OAAuC;IACzE,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AACxC,CAAC"}
|
package/lib/session-service.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Database, Models } from '@zhin.js/core';
|
|
2
|
-
import {
|
|
3
|
-
import type { TttSessionRow } from './models.js';
|
|
2
|
+
import { VersionedSessionService, type GameMessageLike, type SessionMutationResult } from '@zhin.js/game-kit';
|
|
3
|
+
import type { TttSessionRow, TttSessionStatus } from './models.js';
|
|
4
4
|
/** 井字棋服务使用的数据库实例(Models 经 models.ts 模块增强) */
|
|
5
5
|
export type TttDatabase = Database<unknown, Models, string>;
|
|
6
6
|
export type TttPlayerRef = {
|
|
@@ -10,6 +10,7 @@ export type TttPlayerRef = {
|
|
|
10
10
|
/** 排队行过期时间(超时未匹配的排队视为失效) */
|
|
11
11
|
export declare const QUEUE_TTL_MS: number;
|
|
12
12
|
export declare class QueueService {
|
|
13
|
+
#private;
|
|
13
14
|
private readonly db;
|
|
14
15
|
constructor(db: TttDatabase);
|
|
15
16
|
/** 清掉频道内过期排队行(ttt_queue 无 TTL,靠 join 时顺带清理) */
|
|
@@ -24,7 +25,7 @@ export declare class QueueService {
|
|
|
24
25
|
count(channel: string): Promise<number>;
|
|
25
26
|
}
|
|
26
27
|
export declare function sessionId(): string;
|
|
27
|
-
export declare class SessionService extends
|
|
28
|
+
export declare class SessionService extends VersionedSessionService<TttSessionRow> {
|
|
28
29
|
private readonly db;
|
|
29
30
|
constructor(db: TttDatabase);
|
|
30
31
|
createSession(input: {
|
|
@@ -35,6 +36,22 @@ export declare class SessionService extends BaseSessionService<TttSessionRow> {
|
|
|
35
36
|
playerOName?: string;
|
|
36
37
|
boardJson: string;
|
|
37
38
|
}): Promise<TttSessionRow>;
|
|
39
|
+
/**
|
|
40
|
+
* 落子写路径:乐观锁(expectedRevision)+ actionId 幂等 + 进程内按会话串行
|
|
41
|
+
* (VersionedSessionService.mutateSession)。仅 applied 时补记 ttt_moves 流水
|
|
42
|
+
* —— 提前插入会让冲突/重试产生孤儿行。
|
|
43
|
+
*/
|
|
44
|
+
performMove(input: {
|
|
45
|
+
readonly sessionId: string;
|
|
46
|
+
readonly playerId: string;
|
|
47
|
+
readonly cell: number;
|
|
48
|
+
readonly expectedRevision: number;
|
|
49
|
+
readonly boardJson: string;
|
|
50
|
+
readonly moveCount: number;
|
|
51
|
+
readonly nextTurn: number;
|
|
52
|
+
readonly status: TttSessionStatus;
|
|
53
|
+
readonly winner: number;
|
|
54
|
+
}): Promise<SessionMutationResult<TttSessionRow>>;
|
|
38
55
|
recordMove(sessionId: string, playerId: string, cell: number, moveIndex: number): Promise<void>;
|
|
39
56
|
addSpectator(sessionId: string, userId: string): Promise<void>;
|
|
40
57
|
listSpectators(sessionId: string): Promise<string[]>;
|
package/lib/session-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SessionActionGate, VersionedSessionService, channelKey, generateCompactId, } from '@zhin.js/game-kit';
|
|
2
2
|
import { BOT_ID } from './player-label.js';
|
|
3
3
|
/** 排队行过期时间(超时未匹配的排队视为失效) */
|
|
4
4
|
export const QUEUE_TTL_MS = 10 * 60 * 1000;
|
|
@@ -11,6 +11,8 @@ function getModel(db, name) {
|
|
|
11
11
|
}
|
|
12
12
|
export class QueueService {
|
|
13
13
|
db;
|
|
14
|
+
/** 同频道匹配串行化:并发 join→tryMatch 不得读到同一对玩家双开局。 */
|
|
15
|
+
#matchGate = new SessionActionGate();
|
|
14
16
|
constructor(db) {
|
|
15
17
|
this.db = db;
|
|
16
18
|
}
|
|
@@ -53,22 +55,26 @@ export class QueueService {
|
|
|
53
55
|
return rows.map((r) => r.user_id);
|
|
54
56
|
}
|
|
55
57
|
async tryMatch(channel) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
// 读-删-读非原子:两个并发调用者可能拿到同一对玩家。按频道串行后,
|
|
59
|
+
// 第二个调用者读到的是匹配后的队列(前两名已被删除),自然落空。
|
|
60
|
+
return this.#matchGate.run(`ttt-queue:${channel}`, async () => {
|
|
61
|
+
const q = getModel(this.db, 'ttt_queue');
|
|
62
|
+
const rows = await q.findAll({ channel_key: channel });
|
|
63
|
+
rows.sort((a, b) => a.joined_at - b.joined_at);
|
|
64
|
+
if (rows.length < 2)
|
|
65
|
+
return null;
|
|
66
|
+
// 只删除匹配到的两人,保留队列中其余的等待者
|
|
67
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[0].user_id });
|
|
68
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[1].user_id });
|
|
69
|
+
const toRef = (userId, userName) => ({
|
|
70
|
+
id: userId,
|
|
71
|
+
displayName: userName.trim() || userId,
|
|
72
|
+
});
|
|
73
|
+
return [
|
|
74
|
+
toRef(rows[0].user_id, rows[0].user_name),
|
|
75
|
+
toRef(rows[1].user_id, rows[1].user_name),
|
|
76
|
+
];
|
|
67
77
|
});
|
|
68
|
-
return [
|
|
69
|
-
toRef(rows[0].user_id, rows[0].user_name),
|
|
70
|
-
toRef(rows[1].user_id, rows[1].user_name),
|
|
71
|
-
];
|
|
72
78
|
}
|
|
73
79
|
async count(channel) {
|
|
74
80
|
const rows = await getModel(this.db, 'ttt_queue').findAll({ channel_key: channel });
|
|
@@ -78,7 +84,7 @@ export class QueueService {
|
|
|
78
84
|
export function sessionId() {
|
|
79
85
|
return generateCompactId('s');
|
|
80
86
|
}
|
|
81
|
-
export class SessionService extends
|
|
87
|
+
export class SessionService extends VersionedSessionService {
|
|
82
88
|
db;
|
|
83
89
|
constructor(db) {
|
|
84
90
|
super(db, {
|
|
@@ -137,11 +143,35 @@ export class SessionService extends BaseSessionService {
|
|
|
137
143
|
status: 'active',
|
|
138
144
|
winner: 0,
|
|
139
145
|
move_count: 0,
|
|
146
|
+
revision: 0,
|
|
147
|
+
processed_actions: '[]',
|
|
140
148
|
updated_at: now,
|
|
141
149
|
created_at: now,
|
|
142
150
|
};
|
|
143
151
|
return this.createRow(row);
|
|
144
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* 落子写路径:乐观锁(expectedRevision)+ actionId 幂等 + 进程内按会话串行
|
|
155
|
+
* (VersionedSessionService.mutateSession)。仅 applied 时补记 ttt_moves 流水
|
|
156
|
+
* —— 提前插入会让冲突/重试产生孤儿行。
|
|
157
|
+
*/
|
|
158
|
+
async performMove(input) {
|
|
159
|
+
const result = await this.mutateSession(input.sessionId, {
|
|
160
|
+
actionId: `${input.sessionId}:${input.expectedRevision}:${input.playerId}:${input.cell}`,
|
|
161
|
+
expectedRevision: input.expectedRevision,
|
|
162
|
+
apply: {
|
|
163
|
+
board: input.boardJson,
|
|
164
|
+
move_count: input.moveCount,
|
|
165
|
+
turn: input.nextTurn,
|
|
166
|
+
status: input.status,
|
|
167
|
+
winner: input.winner,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
if (result.kind === 'applied') {
|
|
171
|
+
await this.recordMove(input.sessionId, input.playerId, input.cell, input.moveCount);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
145
175
|
async recordMove(sessionId, playerId, cell, moveIndex) {
|
|
146
176
|
await getModel(this.db, 'ttt_moves').create({
|
|
147
177
|
session_id: sessionId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-service.js","sourceRoot":"","sources":["../src/session-service.ts"],"names":[],"mappings":"AACA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"session-service.js","sourceRoot":"","sources":["../src/session-service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-tic-tac-toe",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Zhin.js 跨平台井字棋 (Plugin Runtime)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@zhin.js/command": "1.0.
|
|
28
|
-
"@zhin.js/core": "1.5.
|
|
29
|
-
"@zhin.js/game-kit": "3.0.
|
|
30
|
-
"@zhin.js/middleware": "1.0.
|
|
31
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
27
|
+
"@zhin.js/command": "1.0.10",
|
|
28
|
+
"@zhin.js/core": "1.5.5",
|
|
29
|
+
"@zhin.js/game-kit": "3.0.5",
|
|
30
|
+
"@zhin.js/middleware": "1.0.8",
|
|
31
|
+
"@zhin.js/plugin-runtime": "1.1.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "^6.0.3",
|
package/plugin.js
CHANGED
|
@@ -61,7 +61,7 @@ export default defineGamePlugin({
|
|
|
61
61
|
}, session, status, terminal));
|
|
62
62
|
await Promise.all(spectators.map((userId) => outbound.send({
|
|
63
63
|
adapter: session.adapter,
|
|
64
|
-
|
|
64
|
+
endpointKey: session.endpoint,
|
|
65
65
|
conversation: { kind: 'private', id: userId },
|
|
66
66
|
content,
|
|
67
67
|
})));
|
package/src/board-view.ts
CHANGED
package/src/models.ts
CHANGED
|
@@ -20,6 +20,10 @@ declare module '@zhin.js/core' {
|
|
|
20
20
|
status: TttSessionStatus;
|
|
21
21
|
winner: number;
|
|
22
22
|
move_count: number;
|
|
23
|
+
/** 乐观锁版本(VersionedSessionService 契约) */
|
|
24
|
+
revision: number;
|
|
25
|
+
/** JSON 数组:已处理 actionId 历史(幂等去重) */
|
|
26
|
+
processed_actions: string;
|
|
23
27
|
updated_at: number;
|
|
24
28
|
created_at: number;
|
|
25
29
|
};
|
|
@@ -75,6 +79,8 @@ export function defineHostTables(
|
|
|
75
79
|
status: { type: 'text', default: 'active' },
|
|
76
80
|
winner: { type: 'integer', default: 0 },
|
|
77
81
|
move_count: { type: 'integer', default: 0 },
|
|
82
|
+
revision: { type: 'integer', default: 0 },
|
|
83
|
+
processed_actions: { type: 'text', default: '[]' },
|
|
78
84
|
updated_at: { type: 'integer', default: 0 },
|
|
79
85
|
created_at: { type: 'integer', default: 0 },
|
|
80
86
|
});
|
package/src/runtime-store.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createToken } from '@zhin.js/plugin-runtime';
|
|
1
|
+
import { createToken, type Token } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { SessionServices } from './session-service.js';
|
|
3
3
|
|
|
4
4
|
/** Owner-scoped service consumed by discovered commands and middleware. */
|
|
5
5
|
export const gameServicesToken = createToken<SessionServices>('zhin.game.tic-tac-toe.services');
|
|
6
6
|
|
|
7
|
-
export function resolveGameServices(context: { use<T>(token:
|
|
8
|
-
return context.use(gameServicesToken)
|
|
7
|
+
export function resolveGameServices(context: { use<T>(token: Token<T>): T }): SessionServices {
|
|
8
|
+
return context.use(gameServicesToken);
|
|
9
9
|
}
|
package/src/session-service.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Database, Models, RelatedModel } from '@zhin.js/core';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
SessionActionGate,
|
|
4
|
+
VersionedSessionService,
|
|
4
5
|
channelKey,
|
|
5
6
|
generateCompactId,
|
|
6
7
|
type GameMessageLike,
|
|
7
8
|
type GameSessionDatabase,
|
|
9
|
+
type SessionMutationResult,
|
|
8
10
|
} from '@zhin.js/game-kit';
|
|
9
|
-
import type { TttModelName, TttSessionRow } from './models.js';
|
|
11
|
+
import type { TttModelName, TttSessionRow, TttSessionStatus } from './models.js';
|
|
10
12
|
import { BOT_ID } from './player-label.js';
|
|
11
13
|
|
|
12
14
|
/** 井字棋服务使用的数据库实例(Models 经 models.ts 模块增强) */
|
|
@@ -28,6 +30,9 @@ function getModel<K extends TttModelName>(db: TttDatabase, name: K): TttModel<K>
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export class QueueService {
|
|
33
|
+
/** 同频道匹配串行化:并发 join→tryMatch 不得读到同一对玩家双开局。 */
|
|
34
|
+
readonly #matchGate = new SessionActionGate();
|
|
35
|
+
|
|
31
36
|
constructor(private readonly db: TttDatabase) {}
|
|
32
37
|
|
|
33
38
|
/** 清掉频道内过期排队行(ttt_queue 无 TTL,靠 join 时顺带清理) */
|
|
@@ -76,21 +81,25 @@ export class QueueService {
|
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
async tryMatch(channel: string): Promise<[TttPlayerRef, TttPlayerRef] | null> {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
// 读-删-读非原子:两个并发调用者可能拿到同一对玩家。按频道串行后,
|
|
85
|
+
// 第二个调用者读到的是匹配后的队列(前两名已被删除),自然落空。
|
|
86
|
+
return this.#matchGate.run(`ttt-queue:${channel}`, async () => {
|
|
87
|
+
const q = getModel(this.db, 'ttt_queue');
|
|
88
|
+
const rows = await q.findAll({ channel_key: channel });
|
|
89
|
+
rows.sort((a, b) => a.joined_at - b.joined_at);
|
|
90
|
+
if (rows.length < 2) return null;
|
|
91
|
+
// 只删除匹配到的两人,保留队列中其余的等待者
|
|
92
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[0]!.user_id });
|
|
93
|
+
await q.deleteWhere({ channel_key: channel, user_id: rows[1]!.user_id });
|
|
94
|
+
const toRef = (userId: string, userName: string) => ({
|
|
95
|
+
id: userId,
|
|
96
|
+
displayName: userName.trim() || userId,
|
|
97
|
+
});
|
|
98
|
+
return [
|
|
99
|
+
toRef(rows[0]!.user_id, rows[0]!.user_name),
|
|
100
|
+
toRef(rows[1]!.user_id, rows[1]!.user_name),
|
|
101
|
+
];
|
|
89
102
|
});
|
|
90
|
-
return [
|
|
91
|
-
toRef(rows[0]!.user_id, rows[0]!.user_name),
|
|
92
|
-
toRef(rows[1]!.user_id, rows[1]!.user_name),
|
|
93
|
-
];
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
async count(channel: string): Promise<number> {
|
|
@@ -103,7 +112,7 @@ export function sessionId(): string {
|
|
|
103
112
|
return generateCompactId('s');
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
export class SessionService extends
|
|
115
|
+
export class SessionService extends VersionedSessionService<TttSessionRow> {
|
|
107
116
|
constructor(private readonly db: TttDatabase) {
|
|
108
117
|
super(db as unknown as GameSessionDatabase<TttSessionRow>, {
|
|
109
118
|
gameId: 'ttt',
|
|
@@ -167,12 +176,47 @@ export class SessionService extends BaseSessionService<TttSessionRow> {
|
|
|
167
176
|
status: 'active',
|
|
168
177
|
winner: 0,
|
|
169
178
|
move_count: 0,
|
|
179
|
+
revision: 0,
|
|
180
|
+
processed_actions: '[]',
|
|
170
181
|
updated_at: now,
|
|
171
182
|
created_at: now,
|
|
172
183
|
};
|
|
173
184
|
return this.createRow(row);
|
|
174
185
|
}
|
|
175
186
|
|
|
187
|
+
/**
|
|
188
|
+
* 落子写路径:乐观锁(expectedRevision)+ actionId 幂等 + 进程内按会话串行
|
|
189
|
+
* (VersionedSessionService.mutateSession)。仅 applied 时补记 ttt_moves 流水
|
|
190
|
+
* —— 提前插入会让冲突/重试产生孤儿行。
|
|
191
|
+
*/
|
|
192
|
+
async performMove(input: {
|
|
193
|
+
readonly sessionId: string;
|
|
194
|
+
readonly playerId: string;
|
|
195
|
+
readonly cell: number;
|
|
196
|
+
readonly expectedRevision: number;
|
|
197
|
+
readonly boardJson: string;
|
|
198
|
+
readonly moveCount: number;
|
|
199
|
+
readonly nextTurn: number;
|
|
200
|
+
readonly status: TttSessionStatus;
|
|
201
|
+
readonly winner: number;
|
|
202
|
+
}): Promise<SessionMutationResult<TttSessionRow>> {
|
|
203
|
+
const result = await this.mutateSession(input.sessionId, {
|
|
204
|
+
actionId: `${input.sessionId}:${input.expectedRevision}:${input.playerId}:${input.cell}`,
|
|
205
|
+
expectedRevision: input.expectedRevision,
|
|
206
|
+
apply: {
|
|
207
|
+
board: input.boardJson,
|
|
208
|
+
move_count: input.moveCount,
|
|
209
|
+
turn: input.nextTurn,
|
|
210
|
+
status: input.status,
|
|
211
|
+
winner: input.winner,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
if (result.kind === 'applied') {
|
|
215
|
+
await this.recordMove(input.sessionId, input.playerId, input.cell, input.moveCount);
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
176
220
|
async recordMove(sessionId: string, playerId: string, cell: number, moveIndex: number): Promise<void> {
|
|
177
221
|
await getModel(this.db, 'ttt_moves').create({
|
|
178
222
|
session_id: sessionId,
|