podcast manager

devenv on the new machine

+319 -17
-1
.devenv/devenv.json
··· 1 - {"inputs":{"nixpkgs":{"url":"github:cachix/devenv-nixpkgs/rolling"}}}
-1
.devenv/flake.json
··· 1 - {"nixpkgs":{"url":"github:cachix/devenv-nixpkgs/rolling"}}
-1
.devenv/gc/shell
··· 1 - shell-5-link
.devenv/imports.txt

This is a binary file and will not be displayed.

-7
.devenv/input-paths.txt
··· 1 - /home/jonathan/code/skypod-node/.devenv/flake.json 2 - /home/jonathan/code/skypod-node/.devenv.flake.nix 3 - /home/jonathan/code/skypod-node/.env 4 - /home/jonathan/code/skypod-node/devenv.local.nix 5 - /home/jonathan/code/skypod-node/devenv.lock 6 - /home/jonathan/code/skypod-node/devenv.nix 7 - /home/jonathan/code/skypod-node/devenv.yaml
-1
.devenv/load-exports
··· 1 -
.devenv/nix-eval-cache.db

This is a binary file and will not be displayed.

.devenv/nix-eval-cache.db-shm

This is a binary file and will not be displayed.

-1
.devenv/profile
··· 1 - /nix/store/wmh3hwran6m1ln7n4ci6794xyrrgcs6w-devenv-profile
-1
.devenv/run
··· 1 - /run/user/1000/devenv-d48b567
.devenv/tasks.db

This is a binary file and will not be displayed.

.devenv/tasks.db-shm

This is a binary file and will not be displayed.

.devenv/tasks.db-wal

This is a binary file and will not be displayed.

+8
.envrc
··· 1 + #!/usr/bin/env bash 2 + 3 + export DIRENV_WARN_TIMEOUT=20s 4 + eval "$(devenv direnvrc)" 5 + 6 + # To silence all output, use `--quiet`. 7 + # Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true 8 + use devenv --quiet
+12
.gitignore
··· 30 30 coverage/ 31 31 .jest-cache/ 32 32 /.jestcache/ 33 + 34 + # Devenv 35 + .devenv* 36 + .devenv/ 37 + devenv.local.nix 38 + devenv.local.yaml 39 + 40 + # direnv 41 + .direnv 42 + 43 + # pre-commit 44 + .pre-commit-config.yaml
+155
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Project Overview 6 + 7 + **Skypod** is an offline-first, peer-to-peer RSS and podcast Progressive Web App (PWA). The application enables users to manage subscriptions and listening history across multiple devices with direct peer-to-peer synchronization (no central server for data). 8 + 9 + **Key Technologies:** TypeScript, Preact, Vite, Dexie.js (IndexedDB), Express, WebRTC (simple-peer), WebSockets, Zod 10 + 11 + ## Development Commands 12 + 13 + ### Starting Development 14 + ```bash 15 + npm run dev 16 + ``` 17 + Starts all development services concurrently using wireit: 18 + - Vite dev server at `http://127.0.0.1:4000` (frontend) 19 + - Backend server at `http://127.0.0.1:4001` (WebSocket + API) 20 + - Live type-checking and linting in watch mode 21 + 22 + ### Running Tests 23 + ```bash 24 + npm run test # Run all tests once 25 + npm run start:tests # Run tests in watch mode 26 + ``` 27 + Tests use Jest with ts-jest. Test files are named `*.spec.ts` or `*.spec.tsx`. 28 + 29 + To run a single test file: 30 + ```bash 31 + npx jest src/path/to/file.spec.ts 32 + ``` 33 + 34 + ### Linting and Type-Checking 35 + ```bash 36 + npm run lint # Run ESLint 37 + npm run types # Run TypeScript type-checking 38 + npm run build # Build production frontend 39 + ``` 40 + 41 + ### Production 42 + ```bash 43 + npm run start:prod # Build and run production server 44 + ``` 45 + 46 + ### Git Hooks 47 + Pre-commit hook runs type-checking and linting automatically. Enable with: 48 + ```bash 49 + git config core.hooksPath .githooks 50 + ``` 51 + 52 + ## Architecture 53 + 54 + ### Module Structure 55 + 56 + The codebase is organized into four main directories with path aliases: 57 + 58 + - **`#client/*`** (`src/client/`) - Preact frontend application 59 + - **`#server/*`** (`src/server/`) - Node.js Express backend 60 + - **`#common/*`** (`src/common/`) - Shared code (protocol, crypto, utilities) 61 + - **`#skypod/*`** (`src/skypod/`) - Domain-specific schemas and actions 62 + 63 + These path aliases are configured in both `package.json` (imports) and `tsconfig.json` (paths). Always use these aliases for absolute imports. 64 + 65 + ### Frontend Client (`src/client/`) 66 + 67 + Built with Preact and Vite. Key subdirectories: 68 + 69 + - **`realm/`** - Core P2P connection management 70 + - `service-connection.ts` - Main WebSocket connection to signaling server 71 + - `service-connection-peer.ts` - WebRTC peer connection management 72 + - `service-connection-sync.ts` - Sync protocol coordinator 73 + - `context-connection.tsx` - React context for connection state 74 + - `context-identity.tsx` - React context for user identity 75 + - **`root/`** - Database setup (Dexie/IndexedDB) 76 + - **`skypod/`** - Application-specific components and workers 77 + - **`components/`** - Reusable UI components 78 + 79 + ### Backend Server (`src/server/`) 80 + 81 + Lightweight Express server with two primary functions: 82 + 83 + 1. **WebSocket Signaling Server** (`routes-socket/`) - Enables WebRTC connection establishment between peers 84 + 2. **API Proxy** (`routes-api/`) - Fetches and parses external RSS feeds 85 + 86 + The server maintains realm state (connected peers) but does NOT store user data - all data lives on clients. 87 + 88 + ### Common Library (`src/common/`) 89 + 90 + Shared isomorphic code: 91 + 92 + - **`protocol/`** - P2P protocol definitions 93 + - `logical-clock.ts` - Hybrid Logical Clock (HLC) implementation for causal ordering 94 + - `messages.ts` - Zod schemas for all WebSocket/WebRTC messages 95 + - `brands.ts` - Branded types for identity IDs 96 + - **`crypto/`** - JWT and JWK utilities for authentication 97 + - **`async/`** - Utilities (semaphore, blocking queue, sleep, etc.) 98 + - **`schema/`** - Common Zod schemas 99 + - `socket.ts` - Shared WebSocket message handling 100 + - `errors.ts` - Custom error types (ProtocolError) 101 + 102 + ### Domain Layer (`src/skypod/`) 103 + 104 + Application-specific schemas and action definitions for the podcast/RSS domain. 105 + 106 + ## Synchronization Protocol 107 + 108 + The P2P sync protocol is the core architectural component. It uses a **Hybrid Logical Clock (HLC)** for causal ordering of events and follows a specific pattern: 109 + 110 + ### PULL for Catch-up 111 + When a new client connects or comes online after being offline, it PULLS the complete action history from a single deterministically chosen `syncPartner`. This efficiently brings the client up to date. 112 + 113 + ### PUSH for Updates 114 + All clients PUSH their own new or offline-generated changes to all connected peers. This is NOT a simple broadcast - each client sends a *tailored* set of missing actions to each peer based on a handshake where "knowledge vectors" (last known timestamps) are exchanged. 115 + 116 + ### Key Components 117 + - **Hybrid Logical Clock** (`src/common/protocol/logical-clock.ts`) - Provides total ordering across distributed events even with clock skew 118 + - **Message Schemas** (`src/common/protocol/messages.ts`) - Defines all protocol messages using Zod 119 + - **Connection Services** (`src/client/realm/service-connection-*.ts`) - Implements the sync protocol 120 + 121 + When working on sync-related code, understand that ordering is critical - the HLC timestamp format is `lc:seconds:counter:identid` where each component is used for deterministic ordering. 122 + 123 + ## Build System 124 + 125 + Uses **wireit** for dependency-aware task orchestration. All build tasks are defined in `package.json` under the `wireit` key. 126 + 127 + - Tasks automatically track file dependencies and outputs 128 + - Service tasks (prefixed with `run:`) run continuously in watch mode 129 + - Compound tasks like `start:dev` orchestrate multiple services 130 + 131 + ## Testing Conventions 132 + 133 + - Test files use the pattern `*.spec.ts` or `*.spec.tsx` 134 + - Tests use `@jest/globals` imports for Jest functions 135 + - Use `#common/*`, `#client/*`, `#server/*` aliases in tests (configured in `jest.config.js`) 136 + - Tests support ESM and handle Preact/JSX via ts-jest 137 + 138 + ## Code Style 139 + 140 + - **ESLint + Prettier** enforce consistent style 141 + - **Strict TypeScript** with `strict: true` and `noImplicitAny: true` 142 + - **Zod** for runtime validation and schema definitions 143 + - **JSX** configured for Preact (`jsxImportSource: "preact"`) 144 + 145 + ## Important Context for AI Assistance 146 + 147 + 1. **Path Aliases Are Critical** - Always use `#common/*`, `#client/*`, `#server/*`, `#skypod/*` for imports, never relative paths across module boundaries 148 + 149 + 2. **Sync Protocol Is Complex** - The HLC-based synchronization is the most intricate part of the codebase. Changes to sync logic require careful consideration of causal ordering and distributed consistency 150 + 151 + 3. **Client Is Offline-First** - All user data lives in IndexedDB via Dexie. The server is stateless regarding user data (only maintains ephemeral realm/peer state) 152 + 153 + 4. **WebRTC for P2P** - Peers communicate directly via WebRTC data channels (using simple-peer). The WebSocket connection to the server is only used for signaling and as a fallback broadcast mechanism 154 + 155 + 5. **Zod Schemas Define Protocol** - All message formats are defined as Zod schemas in `src/common/protocol/`. These schemas are the single source of truth for protocol messages
+103
devenv.lock
··· 1 + { 2 + "nodes": { 3 + "devenv": { 4 + "locked": { 5 + "dir": "src/modules", 6 + "lastModified": 1760560333, 7 + "owner": "cachix", 8 + "repo": "devenv", 9 + "rev": "0a4043938f540027e562c5a0feebbe6be872c3ea", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "dir": "src/modules", 14 + "owner": "cachix", 15 + "repo": "devenv", 16 + "type": "github" 17 + } 18 + }, 19 + "flake-compat": { 20 + "flake": false, 21 + "locked": { 22 + "lastModified": 1747046372, 23 + "owner": "edolstra", 24 + "repo": "flake-compat", 25 + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 26 + "type": "github" 27 + }, 28 + "original": { 29 + "owner": "edolstra", 30 + "repo": "flake-compat", 31 + "type": "github" 32 + } 33 + }, 34 + "git-hooks": { 35 + "inputs": { 36 + "flake-compat": "flake-compat", 37 + "gitignore": "gitignore", 38 + "nixpkgs": [ 39 + "nixpkgs" 40 + ] 41 + }, 42 + "locked": { 43 + "lastModified": 1760392170, 44 + "owner": "cachix", 45 + "repo": "git-hooks.nix", 46 + "rev": "46d55f0aeb1d567a78223e69729734f3dca25a85", 47 + "type": "github" 48 + }, 49 + "original": { 50 + "owner": "cachix", 51 + "repo": "git-hooks.nix", 52 + "type": "github" 53 + } 54 + }, 55 + "gitignore": { 56 + "inputs": { 57 + "nixpkgs": [ 58 + "git-hooks", 59 + "nixpkgs" 60 + ] 61 + }, 62 + "locked": { 63 + "lastModified": 1709087332, 64 + "owner": "hercules-ci", 65 + "repo": "gitignore.nix", 66 + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 67 + "type": "github" 68 + }, 69 + "original": { 70 + "owner": "hercules-ci", 71 + "repo": "gitignore.nix", 72 + "type": "github" 73 + } 74 + }, 75 + "nixpkgs": { 76 + "locked": { 77 + "lastModified": 1758532697, 78 + "owner": "cachix", 79 + "repo": "devenv-nixpkgs", 80 + "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f", 81 + "type": "github" 82 + }, 83 + "original": { 84 + "owner": "cachix", 85 + "ref": "rolling", 86 + "repo": "devenv-nixpkgs", 87 + "type": "github" 88 + } 89 + }, 90 + "root": { 91 + "inputs": { 92 + "devenv": "devenv", 93 + "git-hooks": "git-hooks", 94 + "nixpkgs": "nixpkgs", 95 + "pre-commit-hooks": [ 96 + "git-hooks" 97 + ] 98 + } 99 + } 100 + }, 101 + "root": "root", 102 + "version": 7 103 + }
+8
devenv.nix
··· 1 + { pkgs, lib, config, inputs, ... }: 2 + 3 + { 4 + languages.javascript = { 5 + enable = true; 6 + npm.enable = true; 7 + }; 8 + }
+15
devenv.yaml
··· 1 + # yaml-language-server: $schema=https://devenv.sh/devenv.schema.json 2 + inputs: 3 + nixpkgs: 4 + url: github:cachix/devenv-nixpkgs/rolling 5 + 6 + # If you're using non-OSS software, you can set allowUnfree to true. 7 + # allowUnfree: true 8 + 9 + # If you're willing to use a package that's vulnerable 10 + # permittedInsecurePackages: 11 + # - "openssl-1.1.1w" 12 + 13 + # If you have more than one devenv you can merge them 14 + #imports: 15 + # - ./backend
+17 -3
package-lock.json
··· 59 59 "typescript": "^5.8.3", 60 60 "typescript-eslint": "^8.34.1", 61 61 "typescript-eslint-language-service": "^5.0.5", 62 + "typescript-language-server": "^5.0.1", 62 63 "vite": "^6.3.5", 63 64 "vite-plugin-checker": "^0.9.3", 64 65 "vite-plugin-node-polyfills": "^0.23.0", ··· 3611 3612 } 3612 3613 }, 3613 3614 "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { 3614 - "version": "7.7.2", 3615 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", 3616 - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", 3615 + "version": "7.7.3", 3616 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 3617 + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 3617 3618 "dev": true, 3618 3619 "license": "ISC", 3619 3620 "bin": { ··· 16455 16456 "@typescript-eslint/parser": ">= 5.0.0", 16456 16457 "eslint": ">= 8.0.0", 16457 16458 "typescript": ">= 4.0.0" 16459 + } 16460 + }, 16461 + "node_modules/typescript-language-server": { 16462 + "version": "5.0.1", 16463 + "resolved": "https://registry.npmjs.org/typescript-language-server/-/typescript-language-server-5.0.1.tgz", 16464 + "integrity": "sha512-+qwfddk3LvEbMRlcdDJya58A6pn0tO9nuBp6hRHfBwwGBm8KTkkf3V9iQLM2lgIEUc3vnuMmfzFG/dCFF/whvg==", 16465 + "dev": true, 16466 + "license": "Apache-2.0", 16467 + "bin": { 16468 + "typescript-language-server": "lib/cli.mjs" 16469 + }, 16470 + "engines": { 16471 + "node": ">=20" 16458 16472 } 16459 16473 }, 16460 16474 "node_modules/typeson": {
+1
package.json
··· 72 72 "typescript": "^5.8.3", 73 73 "typescript-eslint": "^8.34.1", 74 74 "typescript-eslint-language-service": "^5.0.5", 75 + "typescript-language-server": "^5.0.1", 75 76 "vite": "^6.3.5", 76 77 "vite-plugin-checker": "^0.9.3", 77 78 "vite-plugin-node-polyfills": "^0.23.0",