aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--docs/dev/roadmap.md65
-rw-r--r--drizzle.config.ts8
-rw-r--r--src/client/api/client.test.ts30
-rw-r--r--src/client/api/client.ts15
-rw-r--r--src/client/api/index.ts4
-rw-r--r--src/client/api/types.ts11
-rw-r--r--src/client/stores/auth.tsx3
-rw-r--r--src/server/db/index.ts8
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}`,