61 lines
2.8 KiB
Markdown
61 lines
2.8 KiB
Markdown
# Sync Feature — Implementation Notes
|
|
|
|
## DB: better-sqlite3 (migrated from @libsql/client)
|
|
|
|
**Why migrated:** `@libsql/client` caused SIGSEGV (exit code 139) on Azure App Service due to Rust native module incompatibility with seccomp profile.
|
|
|
|
**Key constraints of better-sqlite3:**
|
|
- All operations are **synchronous** — no `await` on db calls
|
|
- `db.transaction()` callback **must be synchronous** — `async (tx) => {}` throws `TypeError: Transaction function cannot return a promise`
|
|
- Drizzle ORM for better-sqlite3: `drizzle-orm/better-sqlite3` + `drizzle-orm/better-sqlite3/migrator`
|
|
- Result object uses `.changes` (not `.rowsAffected`) → use `(result as any).changes ?? (result as any).rowsAffected ?? 0`
|
|
|
|
**Config:**
|
|
- `drizzle.config.ts`: `url` without `file:` prefix
|
|
- Docker base: `node:22-slim` (prebuilt binaries available)
|
|
- No build tools needed in Dockerfile (prebuilts used on Linux)
|
|
|
|
**Migration:** `migrate(db, { migrationsFolder })` called synchronously at startup in `connection.ts`.
|
|
|
|
## sync-service.ts
|
|
|
|
`pushChanges` processes entities in FK order: contexts → topics → historyEntries → ratings → imageBlobs → pages → notebooks → pageNotebooks.
|
|
|
|
`upsertEntity` logic: insert if not exists, update if `clientVersion > serverVersion`, else record conflict.
|
|
|
|
**No transaction wrapper** — the old `db.transaction(async ...)` was dead code and was removed. Each entity is upserted individually.
|
|
|
|
## API Keys (M2M Auth)
|
|
|
|
Added `api_keys` table in schema.ts. Format: `ka_<base64url-32bytes>`. Only SHA-256 hash stored in DB.
|
|
|
|
Auth middleware (`auth.ts`) detects `ka_...` Bearer tokens **before** JWT path, looks up by hash, fires `lastUsedAt` update async.
|
|
|
|
Routes: `GET /api/api-keys`, `POST /api/api-keys`, `DELETE /api/api-keys/:id` (soft-delete).
|
|
|
|
Raw key returned **once** on creation — not recoverable after that.
|
|
|
|
## Browser API Key (client-side)
|
|
|
|
`browserApiKey` store in `authStore.ts` backed by `localStorage['ka-note-browser-key']`.
|
|
|
|
`getAccessToken()` returns browser key first, then falls back to MSAL.
|
|
|
|
`isAuthenticated` derived from `account OR browserApiKey`.
|
|
|
|
UI in Settings → "Browser-Authentifizierung" section.
|
|
|
|
Use case: avoids MSAL silent refresh failures (Azure AD returning HTTP 400 on refresh token).
|
|
|
|
## Deploy
|
|
|
|
`deploy.ps1` uses `docker build --no-cache` to ensure TypeScript changes are always recompiled.
|
|
|
|
Without `--no-cache`, Docker reuses the server-build layer → old compiled JS runs in production.
|
|
|
|
## ECONNRESET on POST /api/sync/push
|
|
|
|
`Error: aborted / ECONNRESET` logged as 500 is a **client-side disconnect**, not a server error. Hono logs 500 because response couldn't be sent. Causes: tab backgrounded on iOS, Azure idle timeout, browser request timeout.
|
|
|
|
Not actionable on server side unless response time is consistently >10s (indicates oversized payload, e.g. first full sync after DB reset).
|