From f785907202c42fde6fb1d71ef9802120db7162e2 Mon Sep 17 00:00:00 2001 From: tknkaa Date: Fri, 26 Dec 2025 17:57:24 +0900 Subject: [PATCH 1/5] seed local d1 --- app/lib/auth.ts | 6 +- app/lib/db/index.ts | 15 +---- app/lib/db/schema.ts | 121 +++++++++++++++++++++++------------------ app/lib/db/seed.ts | 57 ------------------- app/lib/hai/utils.ts | 33 ----------- app/routes/api.seed.ts | 49 +++++++++++++++++ app/routes/play.tsx | 14 +---- bun.lock | 72 ++++++++++++++++++++++++ docker-compose.yml | 37 ------------- drizzle.config.ts | 22 +++++++- package.json | 1 + wrangler.jsonc | 7 +++ 12 files changed, 223 insertions(+), 211 deletions(-) delete mode 100644 app/lib/db/seed.ts create mode 100644 app/routes/api.seed.ts delete mode 100644 docker-compose.yml diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 90d7d50..8107cdf 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -4,10 +4,10 @@ import { anonymous } from "better-auth/plugins"; import * as schema from "../lib/db/schema"; import { getDB } from "./db"; -export function getAuth(env?: Env) { +export function getAuth(env: Env) { const auth = betterAuth({ database: drizzleAdapter(getDB(env), { - provider: "pg", + provider: "sqlite", schema: schema, }), emailAndPassword: { @@ -28,5 +28,3 @@ export function getAuth(env?: Env) { }); return auth; } -// This is for @better-auth/cli -// export const auth = getAuth(); diff --git a/app/lib/db/index.ts b/app/lib/db/index.ts index fbfa5f5..6d05c95 100644 --- a/app/lib/db/index.ts +++ b/app/lib/db/index.ts @@ -1,15 +1,6 @@ -import { drizzle as drizzleNeon } from "drizzle-orm/neon-http"; -import { drizzle as drizzlePg } from "drizzle-orm/node-postgres"; +import { drizzle } from "drizzle-orm/d1"; -export function getDB(env?: Env) { - // better-auth/cli を実行するとき - if (!env) { - const db = drizzlePg(process.env.DATABASE_URL); - return db; - } - const db = - env.NODE_ENV === "development" - ? drizzlePg(env.DATABASE_URL) - : drizzleNeon(env.DATABASE_URL); +export function getDB(env: Env) { + const db = drizzle(env.DB); return db; } diff --git a/app/lib/db/schema.ts b/app/lib/db/schema.ts index 68c1c8d..cf8eb36 100644 --- a/app/lib/db/schema.ts +++ b/app/lib/db/schema.ts @@ -1,41 +1,38 @@ +import type { string } from "better-auth"; import { sql } from "drizzle-orm"; import { - boolean, check, + customType, index, integer, - pgEnum, - pgTable, primaryKey, - serial, + sqliteTable, text, - timestamp, -} from "drizzle-orm/pg-core"; +} from "drizzle-orm/sqlite-core"; +import type { Hai } from "../hai/utils"; -export const haiyama = pgTable("haiyama", { - id: text("id").primaryKey(), +const haiArray = 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 haiKindEnum = pgEnum("hai_kind", [ - "manzu", - "pinzu", - "souzu", - "jihai", -]); - -export const hai = pgTable("hai", { - id: serial("id").primaryKey(), - haiyamaId: text("haiyama_id") - .notNull() - .references(() => haiyama.id, { onDelete: "cascade" }), - kind: haiKindEnum("kind").notNull(), // "manzu" | "pinzu" | "souzu" | "jihai" - value: text("value").notNull(), // 1~9 or "ton" ~ "tyun" - order: integer("order").notNull(), // 0~17 - index: integer("index").notNull(), // haiToIndex +export const haiyama = sqliteTable("haiyama", { + id: text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + // D1だと1クエリあたり100パラメータまでなので、あえて正規化していない + tiles: haiArray("tiles").notNull(), }); // relation between user and haiyama -export const kyoku = pgTable( +export const kyoku = sqliteTable( "kyoku", { userId: text("user_id") @@ -44,7 +41,7 @@ export const kyoku = pgTable( haiyamaId: text("haiyama_id") .notNull() .references(() => haiyama.id, { onDelete: "cascade" }), - didAgari: boolean("did_agari").notNull(), + didAgari: integer("did_agari", { mode: "boolean" }).notNull(), agariJunme: integer("agari_junme"), }, (table) => [ @@ -59,28 +56,35 @@ export const kyoku = pgTable( ); // better-auth -export const user = pgTable("user", { +export const user = sqliteTable("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), - emailVerified: boolean("email_verified").default(false).notNull(), - image: text("image"), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at") - .defaultNow() - .$onUpdate(() => /* @__PURE__ */ new Date()) + emailVerified: integer("email_verified", { mode: "boolean" }) + .default(false) .notNull(), - isAnonymous: boolean("is_anonymous"), + image: text("image"), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), + isAnonymous: integer("is_anonymous", { mode: "boolean" }), }); -export const session = pgTable("session", { +export const session = sqliteTable("session", { id: text("id").primaryKey(), - expiresAt: timestamp("expires_at").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), token: text("token").notNull().unique(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at") - .$onUpdate(() => /* @__PURE__ */ new Date()) - .notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") @@ -88,7 +92,7 @@ export const session = pgTable("session", { .references(() => user.id, { onDelete: "cascade" }), }); -export const account = pgTable("account", { +export const account = sqliteTable("account", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), @@ -98,24 +102,33 @@ export const account = pgTable("account", { accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), - accessTokenExpiresAt: timestamp("access_token_expires_at"), - refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + accessTokenExpiresAt: integer("access_token_expires_at", { + mode: "timestamp", + }), + refreshTokenExpiresAt: integer("refresh_token_expires_at", { + mode: "timestamp", + }), scope: text("scope"), password: text("password"), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at") - .$onUpdate(() => /* @__PURE__ */ new Date()) - .notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), }); -export const verification = pgTable("verification", { +export const verification = sqliteTable("verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), - expiresAt: timestamp("expires_at").notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at") - .defaultNow() - .$onUpdate(() => /* @__PURE__ */ new Date()) - .notNull(), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), }); diff --git a/app/lib/db/seed.ts b/app/lib/db/seed.ts deleted file mode 100644 index 866bec4..0000000 --- a/app/lib/db/seed.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { drizzle } from "drizzle-orm/node-postgres"; -import { Client } from "pg"; -import { v4 as uuidv4 } from "uuid"; -import type { Hai, SuhaiKind } from "../hai"; -import { constructHai, haiToDBHai } from "../hai"; -import { hai, haiyama } from "./schema"; - -function shuffleArray(array: T[]): T[] { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - const temp = array[i]; - if (temp !== undefined && array[j] !== undefined) { - array[i] = array[j]; - array[j] = temp; - } - } - return array; -} - -function createHaiyama(): Hai[] { - const kinds: SuhaiKind[] = ["manzu", "pinzu", "souzu"]; - let sortedHaiyama: Hai[] = []; - - for (const kind of kinds) { - for (let value = 1; value < 10; value++) { - sortedHaiyama = sortedHaiyama.concat( - new Array(4).fill(constructHai(kind, value)), - ); - } - } - return shuffleArray(sortedHaiyama); -} - -async function main() { - const client = new Client({ - connectionString: process.env.DATABASE_URL, - }); - await client.connect(); - const db = drizzle(client); - - const newHaiyamaId = uuidv4(); - const haiyamaData = createHaiyama(); - - await db.insert(haiyama).values({ id: newHaiyamaId }); - - const haiData = haiyamaData.map((h, i) => haiToDBHai(h, newHaiyamaId, i)); - - await db.insert(hai).values(haiData); - - console.log("Seeding complete"); - await client.end(); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/app/lib/hai/utils.ts b/app/lib/hai/utils.ts index 78fbbf2..0b17199 100644 --- a/app/lib/hai/utils.ts +++ b/app/lib/hai/utils.ts @@ -78,36 +78,3 @@ export function constructHai(kind: HaiKind, value: number | JihaiValue): Hai { export function sortTehai(haiArray: Hai[]): Hai[] { return haiArray.sort((a, b) => haiToIndex(a) - haiToIndex(b)); } - -// To store hai in DB -export type DBHai = { - haiyamaId: string; - kind: HaiKind; - value: string; - order: number; - index: number; -}; - -export function haiToDBHai(hai: Hai, haiyamaId: string, order: number): DBHai { - return { - haiyamaId, - kind: hai.kind, - value: String(hai.value), - order, - index: haiToIndex(hai), - }; -} - -export function dbHaiToHai(dbHai: DBHai): Hai { - if (dbHai.kind === "jihai") { - return { - kind: dbHai.kind, - value: dbHai.value as JihaiValue, - }; - } else { - return { - kind: dbHai.kind as SuhaiKind, - value: Number(dbHai.value), - }; - } -} diff --git a/app/routes/api.seed.ts b/app/routes/api.seed.ts new file mode 100644 index 0000000..b161ab9 --- /dev/null +++ b/app/routes/api.seed.ts @@ -0,0 +1,49 @@ +import { getDB } from "../lib/db"; +import { haiyama } from "../lib/db/schema"; +import type { Hai, SuhaiKind } from "../lib/hai/utils"; +import { constructHai } from "../lib/hai/utils"; +import type { Route } from "./+types/api.seed"; + +function shuffleArray(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = array[i]; + if (temp !== undefined && array[j] !== undefined) { + array[i] = array[j]; + array[j] = temp; + } + } + return array; +} + +function createHaiyama(): Hai[] { + const kinds: SuhaiKind[] = ["manzu", "pinzu", "souzu"]; + let sortedHaiyama: Hai[] = []; + + for (const kind of kinds) { + for (let value = 1; value < 10; value++) { + sortedHaiyama = sortedHaiyama.concat( + new Array(4).fill(constructHai(kind, value)), + ); + } + } + const minLengthOfHaiyama = 31; + const shuffledHaiyama = shuffleArray(sortedHaiyama); + const trimedHaiyama = shuffledHaiyama.slice(0, minLengthOfHaiyama); + return trimedHaiyama; +} + +async function seed(env: Env) { + const db = getDB(env); + + const haiyamaData = createHaiyama(); + + await db.insert(haiyama).values({ tiles: haiyamaData }); + + console.log("Seeding complete"); +} + +export async function loader({ context }: Route.LoaderArgs) { + const { env } = context.cloudflare; + await seed(env); +} diff --git a/app/routes/play.tsx b/app/routes/play.tsx index bab529f..bdf6cf3 100644 --- a/app/routes/play.tsx +++ b/app/routes/play.tsx @@ -2,9 +2,9 @@ import { sql } from "drizzle-orm"; import { Form } from "react-router"; import { getAuth } from "~/lib/auth"; import { getDB } from "~/lib/db"; -import { hai, haiyama } from "~/lib/db/schema"; +import { haiyama } from "~/lib/db/schema"; import judgeAgari from "~/lib/hai/judgeAgari"; -import { dbHaiToHai, sortTehai } from "~/lib/hai/utils"; +import { sortTehai } from "~/lib/hai/utils"; import { type GameState, getRedisClient, init } from "~/lib/redis"; import type { Route } from "./+types/play"; @@ -47,15 +47,7 @@ export async function loader({ throw new Response("No haiyama found", { status: 404 }); } - const selectedHaiyama = randomHaiyama[0]; - const rawHaiData = await db - .select() - .from(hai) - .where(sql`${hai.haiyamaId} = ${selectedHaiyama.id}`) - .orderBy(hai.order); - - const haiData = rawHaiData.map((hai) => dbHaiToHai(hai)); - + const haiData = randomHaiyama[0].tiles; // Initialize game state in Redis await init(redisClient, userId, haiData); diff --git a/bun.lock b/bun.lock index 9c7b170..662150a 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "hitori-mahjong", @@ -8,6 +9,7 @@ "@react-router/fs-routes": "^7.9.4", "@vitejs/plugin-rsc": "^0.5.7", "better-auth": "^1.3.33", + "better-sqlite3": "^12.5.0", "drizzle-orm": "^0.44.7", "isbot": "^5.1.31", "pg": "^8.16.3", @@ -467,18 +469,28 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ=="], "better-auth": ["better-auth@1.3.33", "", { "dependencies": { "@better-auth/core": "1.3.33", "@better-auth/telemetry": "1.3.33", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-4jKI/rbpOh/Qu4puM6sMFvqzmp2sXiZdqqcIYI7fvdb27jBjVylRG7b4nhSHUbl4zXAgsb/FSMi2Jx2sy/5EbA=="], "better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="], + "better-sqlite3": ["better-sqlite3@12.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -487,6 +499,8 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], @@ -513,8 +527,12 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -529,6 +547,8 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], @@ -555,6 +575,8 @@ "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -563,8 +585,12 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -575,6 +601,8 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], @@ -589,6 +617,12 @@ "hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -685,20 +719,30 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "miniflare": ["miniflare@4.20251011.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251011.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-Qbw1Z8HTYM1adWl6FAtzhrj34/6dPRDPwdYOx21dkae8a/EaxbMzRIPbb4HKVGMVvtqbK1FaRCgDLVLolNzGHg=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + "node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="], + "node-releases": ["node-releases@2.0.26", "", {}, "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA=="], "normalize-package-data": ["normalize-package-data@5.0.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q=="], @@ -713,6 +757,8 @@ "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -755,6 +801,8 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="], @@ -763,12 +811,16 @@ "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], @@ -781,6 +833,8 @@ "react-server-dom-webpack": ["react-server-dom-webpack@19.2.3", "", { "dependencies": { "acorn-loose": "^8.3.0", "neo-async": "^2.6.1", "webpack-sources": "^3.2.0" }, "peerDependencies": { "react": "^19.2.3", "react-dom": "^19.2.3", "webpack": "^5.59.0" } }, "sha512-ifo7aqqdNJyV6U2zuvvWX4rRQ51pbleuUFNG7ZYhIuSuWZzQPbfmYv11GNsyJm/3uGNbt8buJ9wmoISn/uOAfw=="], + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "redis": ["redis@5.9.0", "", { "dependencies": { "@redis/bloom": "5.9.0", "@redis/client": "5.9.0", "@redis/json": "5.9.0", "@redis/search": "5.9.0", "@redis/time-series": "5.9.0" } }, "sha512-E8dQVLSyH6UE/C9darFuwq4usOPrqfZ1864kI4RFbr5Oj9ioB9qPF0oJMwX7s8mf6sPYrz84x/Dx1PGF3/0EaQ=="], @@ -817,6 +871,10 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -841,10 +899,14 @@ "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -853,6 +915,10 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "terser": ["terser@5.44.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw=="], "terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="], @@ -865,6 +931,8 @@ "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "turbo-stream": ["turbo-stream@3.1.0", "", {}, "sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -881,6 +949,8 @@ "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "valibot": ["valibot@1.1.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw=="], @@ -913,6 +983,8 @@ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ca47c35..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -services: - db: - image: postgres:17 - container_name: mypostgres - ports: - - 5432:5432 - volumes: - - db-store:/var/lib/postgresql/data - environment: - POSTGRES_USER: "user" - POSTGRES_PASSWORD: "postgres" - networks: - - mynetwork - - redis: - image: redis:7-alpine - container_name: myredis - command: ["redis-server", "--appendonly", "yes", "--requirepass", "devpass"] - ports: - - 6379:6379 - volumes: - - redis-data:/data - healthcheck: - test: ["CMD", "redis-cli", "-a", "devpass", "PING"] - interval: 5s - timeout: 3s - retries: 10 - networks: - - mynetwork - -networks: - mynetwork: - driver: bridge - -volumes: - db-store: - redis-data: diff --git a/drizzle.config.ts b/drizzle.config.ts index ee8b052..43c211f 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,10 +1,26 @@ import { defineConfig } from "drizzle-kit"; -export default defineConfig({ +const remoteConfig = defineConfig({ + schema: "./app/lib/db/schema.ts", out: "./drizzle", + dialect: "sqlite", + driver: "d1-http", + dbCredentials: { + accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, + databaseId: process.env.CLOUDFLARE_DATABASE_ID!, + token: process.env.CLOUDFLARE_D1_TOKEN!, + }, +}); + +const localConfig = defineConfig({ schema: "./app/lib/db/schema.ts", - dialect: "postgresql", + out: "./drizzle", + dialect: "sqlite", dbCredentials: { - url: process.env.DATABASE_URL!, + url: "./.wrangler/state/v3/d1/miniflare-D1DatabaseObject/25eabb7c53441872a4a4abb47c949949922819653bd1d5148f978d7d7562a7bf.sqlite ", }, }); + +export default process.env.NODE_ENV === "development" + ? localConfig + : remoteConfig; diff --git a/package.json b/package.json index 0095790..f266251 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@react-router/fs-routes": "^7.9.4", "@vitejs/plugin-rsc": "^0.5.7", "better-auth": "^1.3.33", + "better-sqlite3": "^12.5.0", "drizzle-orm": "^0.44.7", "isbot": "^5.1.31", "pg": "^8.16.3", diff --git a/wrangler.jsonc b/wrangler.jsonc index e672406..336318b 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -19,6 +19,13 @@ "pattern": "mahjong.utcode.net", "custom_domain": true } + ], + "d1_databases": [ + { + "binding": "DB", + "database_name": "db", + "database_id": "3d47de4a-ae78-48e0-9aea-fab19d6c2fc9" + } ] /** * Smart Placement From b55b0bb317fbb4eba8ca33fcc8fb1a737441fefc Mon Sep 17 00:00:00 2001 From: tknkaa Date: Fri, 26 Dec 2025 20:04:52 +0900 Subject: [PATCH 2/5] set up durable object --- README.md | 2 + app/lib/db/schema-do.ts | 9 ++++ app/routes/api.do.ts | 17 ++++++ drizzle-do.config.ts | 8 +++ drizzle-do/0000_bright_dark_phoenix.sql | 8 +++ drizzle-do/0001_awesome_brood.sql | 1 + drizzle-do/meta/0000_snapshot.json | 62 ++++++++++++++++++++++ drizzle-do/meta/0001_snapshot.json | 69 +++++++++++++++++++++++++ drizzle-do/meta/_journal.json | 20 +++++++ drizzle-do/migrations.js | 11 ++++ tsconfig.cloudflare.json | 3 +- workers/app.ts | 34 ++++++++++++ wrangler.jsonc | 21 ++++++++ 13 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 app/lib/db/schema-do.ts create mode 100644 app/routes/api.do.ts create mode 100644 drizzle-do.config.ts create mode 100644 drizzle-do/0000_bright_dark_phoenix.sql create mode 100644 drizzle-do/0001_awesome_brood.sql create mode 100644 drizzle-do/meta/0000_snapshot.json create mode 100644 drizzle-do/meta/0001_snapshot.json create mode 100644 drizzle-do/meta/_journal.json create mode 100644 drizzle-do/migrations.js diff --git a/README.md b/README.md index 4d1d371..5a16484 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,5 @@ cp .env.example .env docker compose up bun dev ``` + +After running `bunx 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 new file mode 100644 index 0000000..41f899f --- /dev/null +++ b/app/lib/db/schema-do.ts @@ -0,0 +1,9 @@ +import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const usersTable = sqliteTable("users_table", { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int().notNull(), + email: text().notNull().unique(), + gender: text().notNull(), +}); diff --git a/app/routes/api.do.ts b/app/routes/api.do.ts new file mode 100644 index 0000000..65f7653 --- /dev/null +++ b/app/routes/api.do.ts @@ -0,0 +1,17 @@ +import type { Route } from "./+types/api.do"; + +export async function loader({ context }: Route.LoaderArgs) { + const { env } = context.cloudflare; + const id = env.DO.idFromName("user-id-999"); + const stub = env.DO.get(id); + const userAll = await stub.insertAndList({ + name: "John", + age: 30, + email: "john@example.com", + gender: "male", + }); + console.log( + "New user created. Getting all usersfrom the database: ", + userAll, + ); +} diff --git a/drizzle-do.config.ts b/drizzle-do.config.ts new file mode 100644 index 0000000..4c22667 --- /dev/null +++ b/drizzle-do.config.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..2382ea5 --- /dev/null +++ b/drizzle-do/0000_bright_dark_phoenix.sql @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..7ebc7bd --- /dev/null +++ b/drizzle-do/0001_awesome_brood.sql @@ -0,0 +1 @@ +ALTER TABLE `users_table` ADD `gender` text NOT NULL; \ No newline at end of file diff --git a/drizzle-do/meta/0000_snapshot.json b/drizzle-do/meta/0000_snapshot.json new file mode 100644 index 0000000..6b02091 --- /dev/null +++ b/drizzle-do/meta/0000_snapshot.json @@ -0,0 +1,62 @@ +{ + "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 new file mode 100644 index 0000000..d297c0e --- /dev/null +++ b/drizzle-do/meta/0001_snapshot.json @@ -0,0 +1,69 @@ +{ + "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/_journal.json b/drizzle-do/meta/_journal.json new file mode 100644 index 0000000..8d60991 --- /dev/null +++ b/drizzle-do/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "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 + } + ] +} diff --git a/drizzle-do/migrations.js b/drizzle-do/migrations.js new file mode 100644 index 0000000..73254a1 --- /dev/null +++ b/drizzle-do/migrations.js @@ -0,0 +1,11 @@ +import journal from "./meta/_journal.json"; +import m0000 from "./0000_bright_dark_phoenix.sql?raw"; +import m0001 from "./0001_awesome_brood.sql?raw"; + +export default { + journal, + migrations: { + m0000, + m0001, + }, +}; diff --git a/tsconfig.cloudflare.json b/tsconfig.cloudflare.json index 9bfce28..f038128 100644 --- a/tsconfig.cloudflare.json +++ b/tsconfig.cloudflare.json @@ -6,7 +6,8 @@ "app/**/.server/**/*", "app/**/.client/**/*", "workers/**/*", - "worker-configuration.d.ts" + "worker-configuration.d.ts", + "drizzle-do/**/*" ], "compilerOptions": { "composite": true, diff --git a/workers/app.ts b/workers/app.ts index c564b9b..54be763 100644 --- a/workers/app.ts +++ b/workers/app.ts @@ -1,4 +1,12 @@ +import { DurableObject } from "cloudflare:workers"; +import { + type DrizzleSqliteDODatabase, + drizzle, +} from "drizzle-orm/durable-sqlite"; +import { migrate } from "drizzle-orm/durable-sqlite/migrator"; import { createRequestHandler } from "react-router"; +import { usersTable } from "~/lib/db/schema-do"; +import migrations from "../drizzle-do/migrations"; declare module "react-router" { export interface AppLoadContext { @@ -9,6 +17,32 @@ 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); + } + async insertAndList(user: typeof usersTable.$inferInsert) { + await this.db.insert(usersTable).values(user); + return this.db.select().from(usersTable); + } +} + const requestHandler = createRequestHandler( () => import("virtual:react-router/server-build"), import.meta.env.MODE, diff --git a/wrangler.jsonc b/wrangler.jsonc index 336318b..5a63337 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -26,6 +26,27 @@ "database_name": "db", "database_id": "3d47de4a-ae78-48e0-9aea-fab19d6c2fc9" } + ], + "migrations": [ + { + "new_sqlite_classes": ["MyDurableObject"], + "tag": "v1" + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "MyDurableObject", + "name": "DO" + } + ] + }, + "rules": [ + { + "type": "Text", + "globs": ["**/*.sql"], + "fallthrough": true + } ] /** * Smart Placement From 178ed56c616e5e875ddbd16f1aaf858a8b2ac8d0 Mon Sep 17 00:00:00 2001 From: tknkaa Date: Fri, 26 Dec 2025 20:06:31 +0900 Subject: [PATCH 3/5] format --- drizzle-do/migrations.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/drizzle-do/migrations.js b/drizzle-do/migrations.js index 73254a1..45243ee 100644 --- a/drizzle-do/migrations.js +++ b/drizzle-do/migrations.js @@ -1,11 +1,11 @@ -import journal from "./meta/_journal.json"; import m0000 from "./0000_bright_dark_phoenix.sql?raw"; import m0001 from "./0001_awesome_brood.sql?raw"; +import journal from "./meta/_journal.json"; export default { - journal, - migrations: { - m0000, - m0001, - }, + journal, + migrations: { + m0000, + m0001, + }, }; From 681fddfea3c40c5fa6c32b757de1474b1eb4086d Mon Sep 17 00:00:00 2001 From: tknkaa Date: Fri, 26 Dec 2025 21:15:02 +0900 Subject: [PATCH 4/5] migrate to durable object --- app/lib/db/schema-do.ts | 31 ++++-- app/lib/db/schema.ts | 3 +- app/lib/do.ts | 16 +++ app/lib/redis.ts | 140 -------------------------- app/routes/api.do.ts | 17 ---- app/routes/play.tedashi.ts | 11 +- app/routes/play.tsumogiri.ts | 11 +- app/routes/play.tsx | 23 ++--- drizzle-do/0002_chilly_changeling.sql | 11 ++ drizzle-do/meta/0002_snapshot.json | 78 ++++++++++++++ drizzle-do/meta/_journal.json | 7 ++ drizzle-do/migrations.js | 23 +++-- package.json | 1 - workers/app.ts | 130 +++++++++++++++++++++++- 14 files changed, 292 insertions(+), 210 deletions(-) create mode 100644 app/lib/do.ts delete mode 100644 app/lib/redis.ts delete mode 100644 app/routes/api.do.ts create mode 100644 drizzle-do/0002_chilly_changeling.sql create mode 100644 drizzle-do/meta/0002_snapshot.json diff --git a/app/lib/db/schema-do.ts b/app/lib/db/schema-do.ts index 41f899f..39e3c8f 100644 --- a/app/lib/db/schema-do.ts +++ b/app/lib/db/schema-do.ts @@ -1,9 +1,26 @@ -import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { customType, int, sqliteTable } from "drizzle-orm/sqlite-core"; +import type { Hai } from "../hai/utils"; +import { haiArray } from "./schema"; -export const usersTable = sqliteTable("users_table", { - id: int().primaryKey({ autoIncrement: true }), - name: text().notNull(), - age: int().notNull(), - email: text().notNull().unique(), - gender: text().notNull(), +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/lib/db/schema.ts b/app/lib/db/schema.ts index cf8eb36..483d212 100644 --- a/app/lib/db/schema.ts +++ b/app/lib/db/schema.ts @@ -1,4 +1,3 @@ -import type { string } from "better-auth"; import { sql } from "drizzle-orm"; import { check, @@ -11,7 +10,7 @@ import { } from "drizzle-orm/sqlite-core"; import type { Hai } from "../hai/utils"; -const haiArray = customType<{ data: Hai[]; driverData: string }>({ +export const haiArray = customType<{ data: Hai[]; driverData: string }>({ dataType() { return "text"; }, diff --git a/app/lib/do.ts b/app/lib/do.ts new file mode 100644 index 0000000..67794a0 --- /dev/null +++ b/app/lib/do.ts @@ -0,0 +1,16 @@ +import type { Hai } from "./hai/utils"; + +export interface GameState { + kyoku: number; + junme: number; + haiyama: Hai[]; + sutehai: Hai[]; + tehai: Hai[]; + tsumohai: Hai | null; +} + +export default function getDOStub(env: Env, userId: string) { + const id = env.DO.idFromName(userId); + const stub = env.DO.get(id); + return stub; +} diff --git a/app/lib/redis.ts b/app/lib/redis.ts deleted file mode 100644 index 0df3ca0..0000000 --- a/app/lib/redis.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { createClient } from "redis"; -import type { Hai } from "./hai/utils"; -import { sortTehai } from "./hai/utils"; - -export function getRedisClient(env: Env) { - const client = createClient({ - url: env.REDIS_URL, - }); - return client; -} - -export interface GameState { - kyoku: number; - junme: number; - haiyama: Hai[]; - sutehai: Hai[]; - tehai: Hai[]; - tsumohai: Hai | null; -} - -const getGameState = async ( - client: ReturnType, - userId: string, -): Promise => { - const gameStateJSON = await client.get(`user:${userId}:game`); - if (!gameStateJSON) { - return null; - } - return JSON.parse(gameStateJSON); -}; - -const setGameState = async ( - client: ReturnType, - userId: string, - gameState: GameState, -) => { - await client.set(`user:${userId}:game`, JSON.stringify(gameState)); -}; - -export const init = async ( - client: ReturnType, - userId: string, - initHaiyama: Hai[], -) => { - const tehai = initHaiyama.slice(0, 13); - const tsumohai = initHaiyama[13]; - const haiyama = initHaiyama.slice(14); - const initialGameState: GameState = { - kyoku: 1, - junme: 1, - haiyama, - sutehai: [], - tehai, - tsumohai, - }; - await setGameState(client, userId, initialGameState); -}; - -export const tedashi = async ( - client: ReturnType, - userId: string, - index: number, -) => { - const state = await getGameState(client, userId); - 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]; - - const newGameState: GameState = { - ...state, - junme: state.junme + 1, - haiyama: state.haiyama.slice(1), - sutehai: [...state.sutehai, discardedHai], - tehai: sortTehai([...deletedTehai, tsumohai]), - tsumohai: state.haiyama[0], - }; - await setGameState(client, userId, newGameState); -}; - -export const tsumogiri = async ( - client: ReturnType, - userId: string, -) => { - const state = await getGameState(client, userId); - if (!state) { - throw new Error("game not found"); - } - if (!state.tsumohai) { - throw new Error("syohai"); - } - const tsumohai = state.tsumohai; - const newGameState: GameState = { - ...state, - junme: state.junme + 1, - haiyama: state.haiyama.slice(1), - sutehai: [...state.sutehai, tsumohai], - tsumohai: state.haiyama[0], - }; - await setGameState(client, userId, newGameState); -}; - -export const jikyoku = async ( - client: ReturnType, - userId: string, -) => { - const state = await getGameState(client, userId); - if (!state) { - throw new Error("game not found"); - } - const newGameState: GameState = { - ...state, - kyoku: state.kyoku + 1, - }; - await setGameState(client, userId, newGameState); -}; - -export const getCurrentGameState = async ( - client: ReturnType, - userId: string, -): Promise => { - return await getGameState(client, userId); -}; - -export const deleteGameState = async ( - client: ReturnType, - userId: string, -) => { - await client.del(`user:${userId}:game`); -}; diff --git a/app/routes/api.do.ts b/app/routes/api.do.ts deleted file mode 100644 index 65f7653..0000000 --- a/app/routes/api.do.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Route } from "./+types/api.do"; - -export async function loader({ context }: Route.LoaderArgs) { - const { env } = context.cloudflare; - const id = env.DO.idFromName("user-id-999"); - const stub = env.DO.get(id); - const userAll = await stub.insertAndList({ - name: "John", - age: 30, - email: "john@example.com", - gender: "male", - }); - console.log( - "New user created. Getting all usersfrom the database: ", - userAll, - ); -} diff --git a/app/routes/play.tedashi.ts b/app/routes/play.tedashi.ts index ca817a6..468265f 100644 --- a/app/routes/play.tedashi.ts +++ b/app/routes/play.tedashi.ts @@ -1,6 +1,6 @@ import { redirect } from "react-router"; import { getAuth } from "~/lib/auth"; -import { type GameState, getRedisClient, tedashi } from "~/lib/redis"; +import getDOStub from "~/lib/do"; import type { Route } from "./+types/play.tedashi"; export async function action({ context, request }: Route.ActionArgs) { @@ -20,18 +20,13 @@ export async function action({ context, request }: Route.ActionArgs) { throw new Response("Invalid index", { status: 400 }); } - const redisClient = getRedisClient(env); - await redisClient.connect(); + const stub = getDOStub(env, userId); try { - await tedashi(redisClient, userId, index); - const gameStateJSON = await redisClient.get(`user:${userId}:game`); - const gameState = gameStateJSON ? JSON.parse(gameStateJSON) : null; + await stub.tedashi(index); - await redisClient.quit(); return redirect("/play"); } catch (error) { - await redisClient.quit(); const errorMessage = error instanceof Error ? error.message : String(error); throw new Response(errorMessage, { status: 400 }); } diff --git a/app/routes/play.tsumogiri.ts b/app/routes/play.tsumogiri.ts index 77da62e..5f54969 100644 --- a/app/routes/play.tsumogiri.ts +++ b/app/routes/play.tsumogiri.ts @@ -1,6 +1,6 @@ import { redirect } from "react-router"; import { getAuth } from "~/lib/auth"; -import { type GameState, getRedisClient, tsumogiri } from "~/lib/redis"; +import getDOStub from "~/lib/do"; import type { Route } from "./+types/play.tsumogiri"; export async function action({ context, request }: Route.ActionArgs) { @@ -13,18 +13,13 @@ export async function action({ context, request }: Route.ActionArgs) { } const userId = session.user.id; - const redisClient = getRedisClient(env); - await redisClient.connect(); + const stub = getDOStub(env, userId); try { - await tsumogiri(redisClient, userId); - const gameStateJSON = await redisClient.get(`user:${userId}:game`); - const gameState = gameStateJSON ? JSON.parse(gameStateJSON) : null; + await stub.tsumogiri(); - await redisClient.quit(); return redirect("/play"); } catch (error) { - await redisClient.quit(); const errorMessage = error instanceof Error ? error.message : String(error); throw new Response(errorMessage, { status: 400 }); } diff --git a/app/routes/play.tsx b/app/routes/play.tsx index bdf6cf3..50e2e4c 100644 --- a/app/routes/play.tsx +++ b/app/routes/play.tsx @@ -3,9 +3,10 @@ import { Form } from "react-router"; import { getAuth } from "~/lib/auth"; import { getDB } from "~/lib/db"; import { haiyama } from "~/lib/db/schema"; +import type { GameState } from "~/lib/do"; +import getDOStub from "~/lib/do"; import judgeAgari from "~/lib/hai/judgeAgari"; import { sortTehai } from "~/lib/hai/utils"; -import { type GameState, getRedisClient, init } from "~/lib/redis"; import type { Route } from "./+types/play"; export async function loader({ @@ -23,16 +24,14 @@ export async function loader({ const userId = session.user.id; // Check if game state already exists in Redis - const redisClient = getRedisClient(env); - await redisClient.connect(); + const stub = getDOStub(env, userId); try { - const existingGameState = await redisClient.get(`user:${userId}:game`); + const existingGameState = await stub.getCurrentGameState(); if (existingGameState) { // Return existing game state from Redis - await redisClient.quit(); - return JSON.parse(existingGameState); + return existingGameState; } // No existing game state, so initialize from PostgreSQL @@ -43,22 +42,20 @@ export async function loader({ .limit(1); if (randomHaiyama.length === 0) { - await redisClient.quit(); throw new Response("No haiyama found", { status: 404 }); } const haiData = randomHaiyama[0].tiles; // Initialize game state in Redis - await init(redisClient, userId, haiData); + await stub.init(haiData); // Get the initialized game state to return - const gameStateJSON = await redisClient.get(`user:${userId}:game`); - const gameState = gameStateJSON ? JSON.parse(gameStateJSON) : null; - - await redisClient.quit(); + const gameState = await stub.getCurrentGameState(); + if (!gameState) { + throw new Error("Failed to get current game state"); + } return gameState; } catch (error) { - await redisClient.quit(); throw error instanceof Error ? error : new Error(String(error)); } } diff --git a/drizzle-do/0002_chilly_changeling.sql b/drizzle-do/0002_chilly_changeling.sql new file mode 100644 index 0000000..882bb2e --- /dev/null +++ b/drizzle-do/0002_chilly_changeling.sql @@ -0,0 +1,11 @@ +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/0002_snapshot.json b/drizzle-do/meta/0002_snapshot.json new file mode 100644 index 0000000..94d7067 --- /dev/null +++ b/drizzle-do/meta/0002_snapshot.json @@ -0,0 +1,78 @@ +{ + "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 index 8d60991..a0d1d25 100644 --- a/drizzle-do/meta/_journal.json +++ b/drizzle-do/meta/_journal.json @@ -15,6 +15,13 @@ "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 index 45243ee..3de2f40 100644 --- a/drizzle-do/migrations.js +++ b/drizzle-do/migrations.js @@ -1,11 +1,14 @@ -import m0000 from "./0000_bright_dark_phoenix.sql?raw"; -import m0001 from "./0001_awesome_brood.sql?raw"; -import journal from "./meta/_journal.json"; +import journal from './meta/_journal.json'; +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'; -export default { - journal, - migrations: { - m0000, - m0001, - }, -}; + export default { + journal, + migrations: { + m0000, +m0001, +m0002 + } + } + \ No newline at end of file diff --git a/package.json b/package.json index f266251..aeb944d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "build": "react-router build", "cf-typegen": "wrangler types", "deploy": "bun run build && wrangler deploy", - "db:seed": "bun run app/lib/db/seed.ts", "dev": "react-router dev", "postinstall": "npm run cf-typegen", "preview": "bun run build && vite preview", diff --git a/workers/app.ts b/workers/app.ts index 54be763..ba89077 100644 --- a/workers/app.ts +++ b/workers/app.ts @@ -1,11 +1,15 @@ 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 { usersTable } from "~/lib/db/schema-do"; +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"; declare module "react-router" { @@ -37,9 +41,127 @@ export class MyDurableObject extends DurableObject { async _migrate() { migrate(this.db, migrations); } - async insertAndList(user: typeof usersTable.$inferInsert) { - await this.db.insert(usersTable).values(user); - return this.db.select().from(usersTable); + + 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)); } } From 38b14bfc37a0a8b77a51f8d885c7f67cdcd53860 Mon Sep 17 00:00:00 2001 From: tknkaa Date: Fri, 26 Dec 2025 21:15:18 +0900 Subject: [PATCH 5/5] format --- drizzle-do/migrations.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/drizzle-do/migrations.js b/drizzle-do/migrations.js index 3de2f40..7a0c70b 100644 --- a/drizzle-do/migrations.js +++ b/drizzle-do/migrations.js @@ -1,14 +1,13 @@ -import journal from './meta/_journal.json'; -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 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 - } - } - \ No newline at end of file +export default { + journal, + migrations: { + m0000, + m0001, + m0002, + }, +};