From 8f589d8cc46bb92a8d30fe915424d0ae978be813 Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Sat, 3 Jan 2026 13:04:07 -0600 Subject: [PATCH] feat: sync store with dexie * indicator UI --- docs/todo.md | 6 +- web/package.json | 2 + web/pnpm-lock.yaml | 17 ++ web/src/components/SyncIndicator.tsx | 63 +++++ web/src/components/layout/Header.tsx | 2 + .../components/tests/SyncIndicator.test.tsx | 76 ++++++ web/src/lib/api.ts | 10 + web/src/lib/db.ts | 84 +++++++ web/src/lib/sync-store.ts | 221 ++++++++++++++++++ web/src/lib/tests/db.test.ts | 203 ++++++++++++++++ web/src/lib/tests/sync-store.test.ts | 218 +++++++++++++++++ 11 files changed, 899 insertions(+), 3 deletions(-) create mode 100644 web/src/components/SyncIndicator.tsx create mode 100644 web/src/components/tests/SyncIndicator.test.tsx create mode 100644 web/src/lib/db.ts create mode 100644 web/src/lib/sync-store.ts create mode 100644 web/src/lib/tests/db.test.ts create mode 100644 web/src/lib/tests/sync-store.test.ts diff --git a/docs/todo.md b/docs/todo.md index 2f4949e..ac68fa2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -33,9 +33,9 @@ - [x] Bi-directional sync infrastructure - [x] Conflict resolution strategy - [x] API endpoints for sync operations -- [ ] Offline queue for pending publishes - - [ ] Frontend sync store with IndexedDB persistence -- [ ] Sync status UI indicators +- [x] Offline queue for pending publishes +- [x] Frontend sync store with IndexedDB persistence (Dexie.js) +- [x] Sync status UI indicators **Deep Linking:** diff --git a/web/package.json b/web/package.json index a18850a..1baecd9 100644 --- a/web/package.json +++ b/web/package.json @@ -32,6 +32,7 @@ "d3-force": "^3.0.0", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", + "dexie": "^4.2.1", "motion": "^12.23.26", "rehype-external-links": "^3.0.0", "rehype-sanitize": "^6.0.0", @@ -63,6 +64,7 @@ "@typescript-eslint/parser": "^8.50.1", "eslint": "^9.39.2", "eslint-plugin-solid": "^0.14.5", + "fake-indexeddb": "^6.2.5", "globals": "^16.5.0", "jsdom": "^27.4.0", "satori": "^0.18.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index aaaf1f4..ae4c7d0 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: d3-zoom: specifier: ^3.0.0 version: 3.0.0 + dexie: + specifier: ^4.2.1 + version: 4.2.1 motion: specifier: ^12.23.26 version: 12.23.26 @@ -156,6 +159,9 @@ importers: eslint-plugin-solid: specifier: ^0.14.5 version: 0.14.5(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + fake-indexeddb: + specifier: ^6.2.5 + version: 6.2.5 globals: specifier: ^16.5.0 version: 16.5.0 @@ -1403,6 +1409,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dexie@4.2.1: + resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -1508,6 +1517,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fake-indexeddb@6.2.5: + resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} + engines: {node: '>=18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3632,6 +3645,8 @@ snapshots: dependencies: dequal: 2.0.3 + dexie@4.2.1: {} + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -3777,6 +3792,8 @@ snapshots: extend@3.0.2: {} + fake-indexeddb@6.2.5: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} diff --git a/web/src/components/SyncIndicator.tsx b/web/src/components/SyncIndicator.tsx new file mode 100644 index 0000000..211646b --- /dev/null +++ b/web/src/components/SyncIndicator.tsx @@ -0,0 +1,63 @@ +import { syncStore } from "$lib/sync-store"; +import { Show } from "solid-js"; + +export function SyncIndicator() { + const stateClasses = () => { + const state = syncStore.syncState(); + switch (state) { + case "syncing": + return "text-blue-500"; + case "error": + return "text-red-500"; + case "offline": + return "text-amber-500"; + default: + return "text-green-500"; + } + }; + + const stateIcon = () => { + const state = syncStore.syncState(); + switch (state) { + case "syncing": + return "i-ri-loader-4-line animate-spin"; + case "error": + return "i-ri-error-warning-line"; + case "offline": + return "i-ri-wifi-off-line"; + default: + return "i-ri-cloud-line"; + } + }; + + const stateLabel = () => { + const state = syncStore.syncState(); + switch (state) { + case "syncing": + return "Syncing..."; + case "error": + return "Sync error"; + case "offline": + return "Offline"; + default: + return "Synced"; + } + }; + + return ( +
+ + {stateLabel()} + 0}> + + {syncStore.pendingCount()} pending + + + 0}> + + {syncStore.conflictCount()} conflicts + + +
+ ); +} diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 3df53b1..6d817be 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -2,6 +2,7 @@ import { authStore } from "$lib/store"; import { Avatar } from "$ui/Avatar"; import { A } from "@solidjs/router"; import { type Component, Show } from "solid-js"; +import { SyncIndicator } from "../SyncIndicator"; const Login: Component = () => ( @@ -29,6 +30,7 @@ export const Header: Component = () => {
}>
+ {authStore.user()?.handle}