-66
GEMINI.md
-66
GEMINI.md
···
1
-
# Gemini Code Assistant Context
2
-
3
-
This document provides an overview of the "skypod" project to be used as context for AI-assisted development.
4
-
5
-
## Project Overview
6
-
7
-
**Skypod** is an offline-first, peer-to-peer RSS and podcast Progressive Web App (PWA). The primary goal is to allow a user to manage their subscriptions and listening history across multiple devices, with data synchronizing directly between them.
8
-
9
-
### Architecture
10
-
11
-
The application is a full-stack TypeScript project with three main components:
12
-
13
-
1. **Frontend Client:** A Preact-based single-page application built with Vite. It is responsible for all UI, managing local data in IndexedDB via **Dexie.js**, and handling the peer-to-peer communication.
14
-
2. **Backend Server:** A lightweight Node.js server using Express. Its primary roles are to act as a proxy for fetching and parsing external RSS feeds and to run a WebSocket-based signaling server for establishing WebRTC connections between clients.
15
-
3. **P2P Sync Protocol:** The core of the application is a custom peer-to-peer synchronization protocol. It uses WebRTC (via `simple-peer`) for direct communication between a user's devices. Data consistency is achieved by syncing an immutable log of actions, with ordering guaranteed by a **Hybrid Logical Clock (HLC)** implementation.
16
-
17
-
### Key Technologies
18
-
19
-
- **Language:** TypeScript
20
-
- **Frontend:** Preact, Vite, Dexie.js
21
-
- **Backend:** Node.js, Express, `ws` for WebSockets
22
-
- **P2P & Sync:** WebRTC (`simple-peer`), Hybrid Logical Clocks
23
-
- **Schema & Validation:** Zod
24
-
- **Build & Tooling:** `npm` with `wireit`, ESLint, Prettier, Jest
25
-
26
-
## Building and Running
27
-
28
-
The project uses `npm` and `wireit` for robust script and dependency management.
29
-
30
-
- **Installation:**
31
-
```bash
32
-
npm install
33
-
```
34
-
35
-
- **Development:**
36
-
```bash
37
-
npm run dev
38
-
```
39
-
This is the primary development command. It uses `wireit` to concurrently run the Vite dev server, the backend server with `tsx` for live reloading, and watch modes for linting and type-checking. The frontend is available at `http://127.0.0.1:4000` and the backend at `http://127.0.0.1:4001`.
40
-
41
-
- **Production:**
42
-
```bash
43
-
npm run start:prod
44
-
```
45
-
This command builds the production frontend assets with Vite and starts the backend server.
46
-
47
-
- **Testing:**
48
-
```bash
49
-
npm run test
50
-
```
51
-
Runs the entire Jest test suite once.
52
-
53
-
- **Linting & Type-Checking:**
54
-
```bash
55
-
npm run lint
56
-
npm run types
57
-
```
58
-
59
-
## Development Conventions
60
-
61
-
- **Code Style:** The project enforces a strict code style using Prettier and ESLint. Configurations can be found in `prettier.config.js` and `eslint.config.js`.
62
-
- **Git Hooks:** A `pre-commit` hook is provided in `.githooks/` to automatically run linting and type-checking. Enable it with `git config core.hooksPath .githooks`.
63
-
- **Path Aliases:** The codebase uses import aliases like `#common/*` and `#client/*` for clean, absolute-style imports. These are defined in `package.json` and `tsconfig.json`.
64
-
- **P2P Sync Model:** The synchronization protocol follows a specific hybrid model:
65
-
- **PULL for Catch-up:** A new client PULLS the full action history from a single, deterministically chosen `syncPartner` to efficiently get up to date.
66
-
- **PUSH for Updates:** All clients PUSH their own new or offline-generated changes to all connected peers. This is not a simple broadcast; the client sends a *tailored* set of missing actions to each peer based on a handshake where "knowledge vectors" are exchanged.
+80
src/client/components/debug-nuke.tsx
+80
src/client/components/debug-nuke.tsx
···
1
+
import {useState} from 'preact/hooks'
2
+
3
+
export const DebugNuke: preact.FunctionComponent = () => {
4
+
const [confirming, setConfirming] = useState(false)
5
+
6
+
const handleNuke = () => {
7
+
if (!confirming) {
8
+
setConfirming(true)
9
+
return
10
+
}
11
+
12
+
try {
13
+
window.indexedDB.deleteDatabase('skypod')
14
+
window.location.reload()
15
+
} catch (error) {
16
+
console.error('Failed to nuke database:', error)
17
+
alert(`Failed to delete database: ${error}`)
18
+
setConfirming(false)
19
+
}
20
+
}
21
+
22
+
const handleCancel = () => {
23
+
setConfirming(false)
24
+
}
25
+
26
+
return (
27
+
<div style={{position: 'fixed', bottom: '10px', right: '10px', zIndex: 1000}}>
28
+
{!confirming ? (
29
+
<button
30
+
onClick={handleNuke}
31
+
style={{
32
+
padding: '8px 16px',
33
+
backgroundColor: '#dc3545',
34
+
color: 'white',
35
+
border: 'none',
36
+
borderRadius: '4px',
37
+
cursor: 'pointer',
38
+
fontSize: '14px',
39
+
fontWeight: 'bold',
40
+
}}
41
+
>
42
+
Nuke DB
43
+
</button>
44
+
) : (
45
+
<div style={{display: 'flex', gap: '8px'}}>
46
+
<button
47
+
onClick={handleNuke}
48
+
style={{
49
+
padding: '8px 16px',
50
+
backgroundColor: '#dc3545',
51
+
color: 'white',
52
+
border: '2px solid #fff',
53
+
borderRadius: '4px',
54
+
cursor: 'pointer',
55
+
fontSize: '14px',
56
+
fontWeight: 'bold',
57
+
animation: 'pulse 1s infinite',
58
+
}}
59
+
>
60
+
CONFIRM DELETE
61
+
</button>
62
+
<button
63
+
onClick={handleCancel}
64
+
style={{
65
+
padding: '8px 16px',
66
+
backgroundColor: '#6c757d',
67
+
color: 'white',
68
+
border: 'none',
69
+
borderRadius: '4px',
70
+
cursor: 'pointer',
71
+
fontSize: '14px',
72
+
}}
73
+
>
74
+
Cancel
75
+
</button>
76
+
</div>
77
+
)}
78
+
</div>
79
+
)
80
+
}
+2
src/client/page-app.tsx
+2
src/client/page-app.tsx
···
7
7
import {DatabaseProvider} from '#client/root/context-database.js'
8
8
import {SkypodProvider} from '#client/skypod/context'
9
9
10
+
import {DebugNuke} from './components/debug-nuke'
10
11
import {Messenger} from './components/messenger'
11
12
import {PeerList} from './components/peer-list'
12
13
···
29
30
<RealmConnectionManager />
30
31
<PeerList />
31
32
<Messenger />
33
+
<DebugNuke />
32
34
</SkypodProvider>
33
35
</RealmConnectionProvider>
34
36
</RealmIdentityProvider>
+2
src/client/realm/service-connection.ts
+2
src/client/realm/service-connection.ts
+2
-2
src/server/realm-storage.ts
+2
-2
src/server/realm-storage.ts
···
164
164
165
165
try {
166
166
// Iterate through all actions (leveldb stores by clock which is sortable)
167
-
for await (const [, value] of this.#db.values({reverse: true})) {
167
+
for await (const value of this.#db.values({reverse: true})) {
168
168
const stored = storedActionSchema.parse(JSON.parse(value))
169
169
if (!states[stored.actor]) {
170
170
states[stored.actor] = stored.clock
···
180
180
async buildSyncDelta(clocks: Record<IdentID, LCTimestamp | null>): Promise<StoredAction[]> {
181
181
const results: StoredAction[] = []
182
182
try {
183
-
for await (const [, value] of this.#db.values()) {
183
+
for await (const value of this.#db.values()) {
184
184
const stored = storedActionSchema.parse(JSON.parse(value))
185
185
const knownClock = clocks[stored.actor]
186
186