From 47e76cfd463ee3fbcf04abe3a3d20205cfbee693 Mon Sep 17 00:00:00 2001 From: tknkaa Date: Mon, 5 Jan 2026 18:31:10 +0900 Subject: [PATCH] refactor: simplify game state management by moving from Drizzle to in-memory --- README.md | 2 - app/lib/db/schema-do.ts | 26 ---- app/routes/api.seed.ts | 2 +- app/routes/play.tsx | 7 +- drizzle-do.config.ts | 8 -- drizzle-do/0000_bright_dark_phoenix.sql | 8 -- drizzle-do/0001_awesome_brood.sql | 1 - drizzle-do/0002_chilly_changeling.sql | 11 -- drizzle-do/meta/0000_snapshot.json | 62 ---------- drizzle-do/meta/0001_snapshot.json | 69 ----------- drizzle-do/meta/0002_snapshot.json | 78 ------------ drizzle-do/meta/_journal.json | 27 ---- drizzle-do/migrations.js | 13 -- workers/app.ts | 158 +----------------------- workers/do.ts | 93 ++++++++++++++ wrangler.jsonc | 2 +- 16 files changed, 100 insertions(+), 467 deletions(-) delete mode 100644 app/lib/db/schema-do.ts delete mode 100644 drizzle-do.config.ts delete mode 100644 drizzle-do/0000_bright_dark_phoenix.sql delete mode 100644 drizzle-do/0001_awesome_brood.sql delete mode 100644 drizzle-do/0002_chilly_changeling.sql delete mode 100644 drizzle-do/meta/0000_snapshot.json delete mode 100644 drizzle-do/meta/0001_snapshot.json delete mode 100644 drizzle-do/meta/0002_snapshot.json delete mode 100644 drizzle-do/meta/_journal.json delete mode 100644 drizzle-do/migrations.js create mode 100644 workers/do.ts diff --git a/README.md b/README.md index f76326c..378df4b 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,3 @@ npm install cp .env.example .env npm run dev ``` - -- After running `npx drizzle-kit generate --config=drizzle-do.config.ts`, you need to add `?raw` to SQL file names in drizzle-do/migraions.js`. diff --git a/app/lib/db/schema-do.ts b/app/lib/db/schema-do.ts deleted file mode 100644 index 39e3c8f..0000000 --- a/app/lib/db/schema-do.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { customType, int, sqliteTable } from "drizzle-orm/sqlite-core"; -import type { Hai } from "../hai/utils"; -import { haiArray } from "./schema"; - -export const hai = customType<{ data: Hai; driverData: string }>({ - dataType() { - return "text"; - }, - toDriver(value: Hai) { - return JSON.stringify(value); - }, - fromDriver(value: string) { - return JSON.parse(value) as Hai; - }, -}); - -export const gameState = sqliteTable("game_state", { - // 1行しか使わないので、IDを固定 - id: int("id").primaryKey().default(1), - kyoku: int("kyoku").notNull(), - junme: int("junme").notNull(), - haiyama: haiArray("haiyama").notNull(), - sutehai: haiArray("sutehai").notNull(), - tehai: haiArray("tehai").notNull(), - tsumohai: hai("tsumohai"), -}); diff --git a/app/routes/api.seed.ts b/app/routes/api.seed.ts index b161ab9..32ea6ca 100644 --- a/app/routes/api.seed.ts +++ b/app/routes/api.seed.ts @@ -27,7 +27,7 @@ function createHaiyama(): Hai[] { ); } } - const minLengthOfHaiyama = 31; + const minLengthOfHaiyama = 32; const shuffledHaiyama = shuffleArray(sortedHaiyama); const trimedHaiyama = shuffledHaiyama.slice(0, minLengthOfHaiyama); return trimedHaiyama; diff --git a/app/routes/play.tsx b/app/routes/play.tsx index 50e2e4c..f5b1883 100644 --- a/app/routes/play.tsx +++ b/app/routes/play.tsx @@ -27,10 +27,9 @@ export async function loader({ const stub = getDOStub(env, userId); try { - const existingGameState = await stub.getCurrentGameState(); + const existingGameState = await stub.getGameState(); - if (existingGameState) { - // Return existing game state from Redis + if (existingGameState.junme !== 0) { return existingGameState; } @@ -50,7 +49,7 @@ export async function loader({ await stub.init(haiData); // Get the initialized game state to return - const gameState = await stub.getCurrentGameState(); + const gameState = await stub.getGameState(); if (!gameState) { throw new Error("Failed to get current game state"); } diff --git a/drizzle-do.config.ts b/drizzle-do.config.ts deleted file mode 100644 index 4c22667..0000000 --- a/drizzle-do.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -export default defineConfig({ - out: "./drizzle-do", - schema: "./app/lib/db/schema-do.ts", - dialect: "sqlite", - driver: "durable-sqlite", -}); diff --git a/drizzle-do/0000_bright_dark_phoenix.sql b/drizzle-do/0000_bright_dark_phoenix.sql deleted file mode 100644 index 2382ea5..0000000 --- a/drizzle-do/0000_bright_dark_phoenix.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE `users_table` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `name` text NOT NULL, - `age` integer NOT NULL, - `email` text NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `users_table_email_unique` ON `users_table` (`email`); \ No newline at end of file diff --git a/drizzle-do/0001_awesome_brood.sql b/drizzle-do/0001_awesome_brood.sql deleted file mode 100644 index 7ebc7bd..0000000 --- a/drizzle-do/0001_awesome_brood.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `users_table` ADD `gender` text NOT NULL; \ No newline at end of file diff --git a/drizzle-do/0002_chilly_changeling.sql b/drizzle-do/0002_chilly_changeling.sql deleted file mode 100644 index 882bb2e..0000000 --- a/drizzle-do/0002_chilly_changeling.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE `game_state` ( - `id` integer PRIMARY KEY DEFAULT 1 NOT NULL, - `kyoku` integer NOT NULL, - `junme` integer NOT NULL, - `haiyama` text NOT NULL, - `sutehai` text NOT NULL, - `tehai` text NOT NULL, - `tsumohai` text -); ---> statement-breakpoint -DROP TABLE `users_table`; \ No newline at end of file diff --git a/drizzle-do/meta/0000_snapshot.json b/drizzle-do/meta/0000_snapshot.json deleted file mode 100644 index 6b02091..0000000 --- a/drizzle-do/meta/0000_snapshot.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "d58425e2-8b31-424e-88a2-0a69d601dda8", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "users_table": { - "name": "users_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "age": { - "name": "age", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_table_email_unique": { - "name": "users_table_email_unique", - "columns": ["email"], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} diff --git a/drizzle-do/meta/0001_snapshot.json b/drizzle-do/meta/0001_snapshot.json deleted file mode 100644 index d297c0e..0000000 --- a/drizzle-do/meta/0001_snapshot.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "9306fd7c-dafe-47aa-ba50-02934629179f", - "prevId": "d58425e2-8b31-424e-88a2-0a69d601dda8", - "tables": { - "users_table": { - "name": "users_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "age": { - "name": "age", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "gender": { - "name": "gender", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "users_table_email_unique": { - "name": "users_table_email_unique", - "columns": ["email"], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} diff --git a/drizzle-do/meta/0002_snapshot.json b/drizzle-do/meta/0002_snapshot.json deleted file mode 100644 index 94d7067..0000000 --- a/drizzle-do/meta/0002_snapshot.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "b6b7a720-8fb8-406c-90d2-a9dd61d358b1", - "prevId": "9306fd7c-dafe-47aa-ba50-02934629179f", - "tables": { - "game_state": { - "name": "game_state", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "kyoku": { - "name": "kyoku", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "junme": { - "name": "junme", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "haiyama": { - "name": "haiyama", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sutehai": { - "name": "sutehai", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "tehai": { - "name": "tehai", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "tsumohai": { - "name": "tsumohai", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} diff --git a/drizzle-do/meta/_journal.json b/drizzle-do/meta/_journal.json deleted file mode 100644 index a0d1d25..0000000 --- a/drizzle-do/meta/_journal.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1766743478456, - "tag": "0000_bright_dark_phoenix", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1766746001866, - "tag": "0001_awesome_brood", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1766751071234, - "tag": "0002_chilly_changeling", - "breakpoints": true - } - ] -} diff --git a/drizzle-do/migrations.js b/drizzle-do/migrations.js deleted file mode 100644 index 7a0c70b..0000000 --- a/drizzle-do/migrations.js +++ /dev/null @@ -1,13 +0,0 @@ -import m0000 from "./0000_bright_dark_phoenix.sql?raw"; -import m0001 from "./0001_awesome_brood.sql?raw"; -import m0002 from "./0002_chilly_changeling.sql?raw"; -import journal from "./meta/_journal.json"; - -export default { - journal, - migrations: { - m0000, - m0001, - m0002, - }, -}; diff --git a/workers/app.ts b/workers/app.ts index ba89077..cf12be8 100644 --- a/workers/app.ts +++ b/workers/app.ts @@ -1,16 +1,6 @@ -import { DurableObject } from "cloudflare:workers"; -import { eq } from "drizzle-orm"; -import { - type DrizzleSqliteDODatabase, - drizzle, -} from "drizzle-orm/durable-sqlite"; -import { migrate } from "drizzle-orm/durable-sqlite/migrator"; import { createRequestHandler } from "react-router"; -import { gameState } from "../app/lib/db/schema-do"; -import type { GameState } from "../app/lib/do"; -import type { Hai } from "../app/lib/hai/utils"; -import { sortTehai } from "../app/lib/hai/utils"; -import migrations from "../drizzle-do/migrations"; + +export { MyDurableObject } from "./do"; declare module "react-router" { export interface AppLoadContext { @@ -21,150 +11,6 @@ declare module "react-router" { } } -export class MyDurableObject extends DurableObject { - storage: DurableObjectStorage; - db: DrizzleSqliteDODatabase; - constructor(ctx: DurableObjectState, env: Env) { - // Required, as we're extending the base class. - super(ctx, env); - this.storage = ctx.storage; - this.db = drizzle(this.storage, { - logger: false, - }); - - ctx.blockConcurrencyWhile(async () => { - console.log(migrations); - await this._migrate(); - }); - } - - async _migrate() { - migrate(this.db, migrations); - } - - private async getGameState(): Promise { - const result = await this.db - .select() - .from(gameState) - .where(eq(gameState.id, 1)) - .get(); - - if (!result) { - return null; - } - - return { - kyoku: result.kyoku, - junme: result.junme, - haiyama: result.haiyama, - sutehai: result.sutehai, - tehai: result.tehai, - tsumohai: result.tsumohai, - }; - } - - async init(initHaiyama: Hai[]) { - const tehai = initHaiyama.slice(0, 13); - const tsumohai = initHaiyama[13]; - const haiyama = initHaiyama.slice(14); - - await this.db - .insert(gameState) - .values({ - id: 1, - kyoku: 1, - junme: 1, - haiyama, - sutehai: [], - tehai, - tsumohai, - }) - .onConflictDoUpdate({ - target: gameState.id, - set: { - kyoku: 1, - junme: 1, - haiyama, - sutehai: [], - tehai, - tsumohai, - }, - }); - } - - async tedashi(index: number) { - const state = await this.getGameState(); - if (!state) { - throw new Error("game not found"); - } - if (!state.tsumohai) { - throw new Error("syohai"); - } - const tsumohai = state.tsumohai; - - if (index < 0 || 12 < index) { - throw new Error("index out of tehai length"); - } - const sortedTehai = sortTehai(state.tehai); - const deletedTehai = sortedTehai.filter((_, i) => i !== index); - const discardedHai = sortedTehai[index]; - - await this.db - .update(gameState) - .set({ - junme: state.junme + 1, - haiyama: state.haiyama.slice(1), - sutehai: [...state.sutehai, discardedHai], - tehai: sortTehai([...deletedTehai, tsumohai]), - tsumohai: state.haiyama[0], - }) - .where(eq(gameState.id, 1)); - } - - async tsumogiri() { - const state = await this.getGameState(); - if (!state) { - throw new Error("game not found"); - } - if (!state.tsumohai) { - throw new Error("syohai"); - } - const tsumohai = state.tsumohai; - - await this.db - .update(gameState) - .set({ - junme: state.junme + 1, - haiyama: state.haiyama.slice(1), - sutehai: [...state.sutehai, tsumohai], - tsumohai: state.haiyama[0], - }) - .where(eq(gameState.id, 1)); - } - - async jikyoku() { - const state = await this.getGameState(); - if (!state) { - throw new Error("game not found"); - } - - await this.db - .update(gameState) - .set({ - kyoku: state.kyoku + 1, - }) - .where(eq(gameState.id, 1)); - } - - async getCurrentGameState(): Promise { - return await this.getGameState(); - } - - async deleteGameState() { - await this.db.delete(gameState).where(eq(gameState.id, 1)); - } -} - const requestHandler = createRequestHandler( () => import("virtual:react-router/server-build"), import.meta.env.MODE, diff --git a/workers/do.ts b/workers/do.ts new file mode 100644 index 0000000..9d1a5bc --- /dev/null +++ b/workers/do.ts @@ -0,0 +1,93 @@ +import { DurableObject } from "cloudflare:workers"; +import type { GameState } from "../app/lib/do"; +import type { Hai } from "../app/lib/hai/utils"; +import { sortTehai } from "../app/lib/hai/utils"; + +export class MyDurableObject extends DurableObject { + private gameState: GameState; + constructor(ctx: DurableObjectState, env: Env) { + // Required, as we're extending the base class. + super(ctx, env); + this.gameState = { + kyoku: 0, + junme: 0, + haiyama: [], + sutehai: [], + tehai: [], + tsumohai: null, + }; + + ctx.blockConcurrencyWhile(async () => { + const saved = await this.ctx.storage.get("gameState"); + if (saved) this.gameState = saved; + }); + } + + async getGameState(): Promise { + return this.gameState; + } + + async init(initHaiyama: Hai[]) { + const tehai = initHaiyama.slice(0, 13); + const tsumohai = initHaiyama[13]; + const haiyama = initHaiyama.slice(14); + this.gameState.kyoku = 1; + this.gameState.junme = 1; + this.gameState.haiyama = haiyama; + this.gameState.sutehai = []; + this.gameState.tehai = tehai; + this.gameState.tsumohai = tsumohai; + await this.ctx.storage.put("gameState", this.gameState); + } + + async tedashi(index: number) { + const state = await this.getGameState(); + if (!state) { + throw new Error("game not found"); + } + if (!state.tsumohai) { + throw new Error("syohai"); + } + const tsumohai = state.tsumohai; + + if (index < 0 || 12 < index) { + throw new Error("index out of tehai length"); + } + const sortedTehai = sortTehai(state.tehai); + const remainingTehai = sortedTehai.filter((_, i) => i !== index); + const discardedHai = sortedTehai[index]; + + this.gameState.junme = state.junme + 1; + this.gameState.haiyama = state.haiyama.slice(1); + this.gameState.sutehai = [...state.sutehai, discardedHai]; + this.gameState.tehai = sortTehai([...remainingTehai, tsumohai]); + this.gameState.tsumohai = state.haiyama[0]; + await this.ctx.storage.put("gameState", this.gameState); + } + + async tsumogiri() { + const state = await this.getGameState(); + if (!state) { + throw new Error("game not found"); + } + if (!state.tsumohai) { + throw new Error("syohai"); + } + const tsumohai = state.tsumohai; + + this.gameState.junme = state.junme + 1; + this.gameState.haiyama = state.haiyama.slice(1); + this.gameState.sutehai = [...state.sutehai, tsumohai]; + this.gameState.tsumohai = state.haiyama[0]; + await this.ctx.storage.put("gameState", this.gameState); + } + + async jikyoku() { + const state = await this.getGameState(); + if (!state) { + throw new Error("game not found"); + } + this.gameState.kyoku = state.kyoku + 1; + await this.ctx.storage.put("gameState", this.gameState); + } +} diff --git a/wrangler.jsonc b/wrangler.jsonc index 5a63337..0407c0f 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -29,7 +29,7 @@ ], "migrations": [ { - "new_sqlite_classes": ["MyDurableObject"], + "new_classes": ["MyDurableObject"], "tag": "v1" } ],