diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-31 19:51:21 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-31 19:51:21 +0900 |
| commit | c915c21d47a2b417979b20e9e9d9b6ff30a03c0d (patch) | |
| tree | 643ba4e499bae8a541d77f299837361a75890b10 | |
| parent | 73ee02825f57d971f6f660fc5277d4aa268702ff (diff) | |
| download | kioku-c915c21d47a2b417979b20e9e9d9b6ff30a03c0d.tar.gz kioku-c915c21d47a2b417979b20e9e9d9b6ff30a03c0d.tar.zst kioku-c915c21d47a2b417979b20e9e9d9b6ff30a03c0d.zip | |
refactor(client): use Hono InferResponseType for API response types
Replace manually defined AuthResponse and User types with Hono's
InferResponseType to automatically derive types from server definitions.
This ensures client types stay in sync with server responses.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | docs/dev/roadmap.md | 65 | ||||
| -rw-r--r-- | drizzle.config.ts | 8 | ||||
| -rw-r--r-- | src/client/api/client.test.ts | 30 | ||||
| -rw-r--r-- | src/client/api/client.ts | 15 | ||||
| -rw-r--r-- | src/client/api/index.ts | 4 | ||||
| -rw-r--r-- | src/client/api/types.ts | 11 | ||||
| -rw-r--r-- | src/client/stores/auth.tsx | 3 | ||||
| -rw-r--r-- | src/server/db/index.ts | 8 |
8 files changed, 87 insertions, 57 deletions
diff --git a/docs/dev/roadmap.md b/docs/dev/roadmap.md index 022e09e..b6cc6f9 100644 --- a/docs/dev/roadmap.md +++ b/docs/dev/roadmap.md @@ -1,48 +1,39 @@ # Kioku Development Roadmap -## Issue #3: Introduce CRDT Library for Conflict Resolution +## Use Hono RPC Type Inference for API Response Types -Replace the current Last-Write-Wins (LWW) conflict resolution with Automerge CRDT for better offline sync. +Replace manually defined API response types with Hono's `InferResponseType` for automatic type derivation from server. -**Decisions:** -- Library: Automerge -- Text conflicts: LWW Register (simple, predictable) -- Migration: Clean migration (no backward compatibility) +**Background:** +- Hono's `hc` client can automatically infer response types from server definitions +- Manual type definitions (`AuthResponse`, `User`) duplicate server types and can become out of sync -### Phase 1: Add Automerge and Core Types +**Scope:** +- `AuthResponse` → Use `InferResponseType` +- `User` → Derive from `AuthResponse["user"]` +- `ApiError` → Keep (error responses are separate from success types) +- `Tokens` → Keep (client-internal type) -- [x] Install dependencies: `@automerge/automerge`, `@automerge/automerge-repo`, `@automerge/automerge-repo-storage-indexeddb` -- [x] Create `src/client/sync/crdt/types.ts` - Automerge document type definitions -- [x] Create `src/client/sync/crdt/document-manager.ts` - Automerge document lifecycle management -- [x] Create `src/client/sync/crdt/index.ts` - Module exports +### Phase 1: Update API Client Types -### Phase 2: Create CRDT Repository Layer +- [x] Modify `src/client/api/client.ts`: + - Import `InferResponseType` from `hono/client` + - Define `LoginResponse` type using `InferResponseType<typeof rpc.api.auth.login.$post>` + - Update `login` method to use inferred type +- [x] Modify `src/client/api/types.ts`: + - Remove `AuthResponse` and `User` interfaces + - Keep `ApiError` and `Tokens` +- [x] Modify `src/client/api/index.ts`: + - Export inferred types from `client.ts` instead of `types.ts` -- [x] Create `src/client/sync/crdt/repositories.ts` - CRDT-aware repository wrappers -- [x] Create `src/client/sync/crdt/sync-state.ts` - Sync state serialization +### Phase 2: Update Consumers -### Phase 3: Modify Sync Protocol +- [x] Modify `src/client/stores/auth.tsx`: + - Import `User` type from new location (derived from login response) + - Update `AuthState` interface -- [x] Modify `src/client/sync/push.ts` - Add crdtChanges to push payload -- [x] Modify `src/client/sync/pull.ts` - Handle crdtChanges in pull response -- [x] Modify `src/client/sync/conflict.ts` - Replace LWW with Automerge merge -- [x] Modify `src/client/sync/manager.ts` - Integrate CRDT sync flow +### Phase 3: Update Tests -### Phase 4: Server-Side CRDT Support - -- [x] Install server dependency: `@automerge/automerge` -- [x] Create `src/server/db/schema-crdt.ts` - CRDT document storage schema -- [x] Create database migration for crdt_documents table -- [x] Modify `src/server/routes/sync.ts` - Handle CRDT changes in API -- [x] Modify `src/server/repositories/sync.ts` - Store/merge CRDT documents - -### Phase 5: Migration - -- [x] Create `src/client/sync/crdt/migration.ts` - One-time migration script -- [x] Server data migration - Not needed (no existing production data) - -### Phase 6: Testing and Cleanup - -- [x] Add unit tests for CRDT operations -- [x] Add integration tests for concurrent edit scenarios -- [x] Remove legacy LWW code after validation +- [x] Update `src/client/api/client.test.ts` if needed +- [x] Update `src/client/stores/auth.test.tsx` if needed +- [x] Verify all tests pass diff --git a/drizzle.config.ts b/drizzle.config.ts index 0520eb6..698a49a 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,6 +1,12 @@ import { defineConfig } from "drizzle-kit"; -const { POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_HOST, POSTGRES_PORT } = process.env; +const { + POSTGRES_USER, + POSTGRES_PASSWORD, + POSTGRES_DB, + POSTGRES_HOST, + POSTGRES_PORT, +} = process.env; export default defineConfig({ out: "./drizzle", diff --git a/src/client/api/client.test.ts b/src/client/api/client.test.ts index 5489547..27c3a0a 100644 --- a/src/client/api/client.test.ts +++ b/src/client/api/client.test.ts @@ -13,8 +13,10 @@ import { import { ApiClient, ApiClientError, + type LoginResponse, localStorageTokenStorage, type TokenStorage, + type User, } from "./client"; function createMockTokenStorage(): TokenStorage & { @@ -209,3 +211,31 @@ describe("localStorageTokenStorage", () => { expect(localStorageTokenStorage.getTokens()).toBeNull(); }); }); + +describe("InferResponseType types", () => { + it("LoginResponse has expected properties", () => { + // This test verifies the inferred types have the correct structure + // The type assertions will fail at compile time if the types are wrong + const mockResponse: LoginResponse = { + accessToken: "access-token", + refreshToken: "refresh-token", + user: { id: "123", username: "testuser" }, + }; + + expect(mockResponse.accessToken).toBe("access-token"); + expect(mockResponse.refreshToken).toBe("refresh-token"); + expect(mockResponse.user.id).toBe("123"); + expect(mockResponse.user.username).toBe("testuser"); + }); + + it("User type is correctly derived from LoginResponse", () => { + // Verify User type has expected structure + const user: User = { + id: "user-1", + username: "testuser", + }; + + expect(user.id).toBe("user-1"); + expect(user.username).toBe("testuser"); + }); +}); diff --git a/src/client/api/client.ts b/src/client/api/client.ts index 7741942..7607eb6 100644 --- a/src/client/api/client.ts +++ b/src/client/api/client.ts @@ -1,6 +1,13 @@ -import { hc } from "hono/client"; +import { hc, type InferResponseType } from "hono/client"; import type { AppType } from "../../server/index.js"; -import type { ApiError, AuthResponse, Tokens } from "./types"; +import type { ApiError, Tokens } from "./types"; + +// Create a temporary client just for type inference +const _rpc = hc<AppType>(""); + +// Infer response types from server definitions +export type LoginResponse = InferResponseType<typeof _rpc.api.auth.login.$post>; +export type User = LoginResponse["user"]; export class ApiClientError extends Error { constructor( @@ -120,12 +127,12 @@ export class ApiClient { } } - async login(username: string, password: string): Promise<AuthResponse> { + async login(username: string, password: string): Promise<LoginResponse> { const res = await this.rpc.api.auth.login.$post({ json: { username, password }, }); - const data = await this.handleResponse<AuthResponse>(res); + const data = await this.handleResponse<LoginResponse>(res); this.tokenStorage.setTokens({ accessToken: data.accessToken, diff --git a/src/client/api/index.ts b/src/client/api/index.ts index fb26b70..63d0a40 100644 --- a/src/client/api/index.ts +++ b/src/client/api/index.ts @@ -3,7 +3,9 @@ export { ApiClientError, type ApiClientOptions, apiClient, + type LoginResponse, localStorageTokenStorage, type TokenStorage, + type User, } from "./client"; -export type { ApiError, AuthResponse, Tokens, User } from "./types"; +export type { ApiError, Tokens } from "./types"; diff --git a/src/client/api/types.ts b/src/client/api/types.ts index eaf69eb..70918fe 100644 --- a/src/client/api/types.ts +++ b/src/client/api/types.ts @@ -1,14 +1,3 @@ -export interface User { - id: string; - username: string; -} - -export interface AuthResponse { - accessToken: string; - refreshToken: string; - user: User; -} - export interface ApiError { error: { message: string; diff --git a/src/client/stores/auth.tsx b/src/client/stores/auth.tsx index 58e9d40..b34717b 100644 --- a/src/client/stores/auth.tsx +++ b/src/client/stores/auth.tsx @@ -7,8 +7,7 @@ import { useMemo, useState, } from "react"; -import { ApiClientError, apiClient } from "../api/client"; -import type { User } from "../api/types"; +import { ApiClientError, apiClient, type User } from "../api/client"; export interface AuthState { user: User | null; diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 4826710..2a152a3 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -2,7 +2,13 @@ import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "./schema.js"; import * as schemaCrdt from "./schema-crdt.js"; -const { POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, POSTGRES_HOST, POSTGRES_PORT } = process.env; +const { + POSTGRES_USER, + POSTGRES_PASSWORD, + POSTGRES_DB, + POSTGRES_HOST, + POSTGRES_PORT, +} = process.env; export const db = drizzle( `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`, |
