data-patterns · diff
git:20260103.9f62587 to git:20260309.80c3cb2
1 added, 251 removed. Audit A to B.
---
description: Data patterns - composite IDs, streams, TTL, pipes normalization
globs: src/core/**/*
alwaysApply: false
---
# Data Pattern Rules
- Rules for data handling patterns. Based on ADR-0002, ADR-0003, ADR-0005, ADR-0006.
-
- ## Composite Post IDs (ADR-0002)
-
- Posts use composite key format: `author:postId`
-
- ### Format
- ```typescript
- // Composite ID format
- type CompositePostId = `${Pubky}:${PostId}`;
-
- // Example
- const id = "pk1abc123xyz:0000000123";
- // ^author^ :^postId^
- ```
-
- ### Utilities
- ```typescript
- // Creating composite ID
- const compositeId = `${author}:${postId}`;
-
- // Parsing composite ID
- function parsePostId(composite: string): { author: string; postId: string } {
- const [author, postId] = composite.split(':');
- return { author, postId };
- }
-
- // All tables reference posts using composite ID
- interface PostDetails {
- id: string; // "author:postId"
- author: string; // Pubky
- content: string;
- // ...
- }
- ```
-
- ### Why Composite IDs?
- - ✅ Globally unique (author + timestamp)
- - ✅ Stable for joins between Dexie tables
- - ✅ Prevents collisions after migrations
- - ✅ Retains chronological ordering
-
- ## Streams as Caches (ADR-0003)
-
- Streams are cached sequences in Dexie with ordering metadata.
-
- ### Stream Structure
- ```typescript
- interface StreamEntry {
- streamId: string; // Stream identifier
- postId: string; // Composite post ID
- order: number; // Position in stream
- cursor: string; // Server pagination cursor
- fetchedAt: number; // Timestamp for TTL
- }
- ```
-
- ### Stream Operations
- ```typescript
- // Reading from stream (fast, local)
- const posts = await LocalStreamService.getStreamPosts(streamId);
-
- // Refreshing stream (network + cache update)
- await StreamApplication.refresh(streamId);
-
- // Resuming pagination
- const nextPage = await StreamApplication.fetchNext(streamId, cursor);
- ```
-
- ### Stream Invalidation
- ```typescript
- // Check if stream needs refresh
- const isStale = await TtlService.isExpired(streamId);
-
- // Invalidate stream
- await LocalStreamService.invalidate(streamId);
- ```
-
- ## TTL Management (ADR-0005)
-
- Per-entity TTL tracking for cache freshness.
-
- ### TTL Tables
- ```typescript
- // Each domain has TTL table
- user_ttl: { pubky: string; expiresAt: number }
- post_ttl: { postId: string; expiresAt: number }
- stream_ttl: { streamId: string; expiresAt: number }
- ```
-
- ### TTL Operations
- ```typescript
- // Check if entity is fresh
- const isFresh = await TtlService.check('post', postId);
-
- // Update TTL on write
- await TtlService.touch('post', postId, TTL_DURATION);
-
- // Get expired entities
- const expired = await TtlService.getExpired('post');
- ```
-
- ### TTL on Writes
- ```typescript
- // ✅ Always update TTL when writing
- await LocalPostService.upsert(post);
- await TtlService.touch('post', post.id, POST_TTL);
-
- // ❌ Forgetting TTL update = stale cache
- await LocalPostService.upsert(post);
- // No TTL touch = won't refresh
- ```
-
- ### TTL Constants
- ```typescript
- const TTL = {
- USER: 5 * 60 * 1000, // 5 minutes
- POST: 10 * 60 * 1000, // 10 minutes
- STREAM: 2 * 60 * 1000, // 2 minutes
- };
- ```
-
- ## Pipes Normalization (ADR-0006)
-
- Pipes normalize external data to domain shapes.
-
- ### Pipe Responsibilities
- ```typescript
- // ✅ Pipes DO:
- // - Transform external shapes → domain shapes
- // - Validate input data
- // - Enforce pubky-app-specs
- // - Return normalized objects
-
- // ❌ Pipes DON'T:
- // - Perform IO (network, database)
- // - Have side effects
- // - Access stores
- ```
-
- ### Pipe Structure
- ```typescript
- // src/core/posts/pipes/post.pipe.ts
-
- export class PostPipe {
- /**
- * Normalize Nexus response to domain Post
- */
- static normalizeFromNexus(raw: NexusPost): Post {
- return {
- id: `${raw.author}:${raw.id}`,
- author: raw.author,
- content: raw.content,
- createdAt: raw.indexed_at,
- attachments: raw.attachments?.map(AttachmentPipe.normalize) ?? [],
- };
- }
-
- /**
- * Validate and normalize create input
- */
- static normalizeCreate(input: CreatePostInput): Post {
- if (!input.content?.trim()) {
- throw createCommonError(CommonErrorType.INVALID_INPUT, {
- field: 'content',
- message: 'Content is required',
- });
- }
-
- return {
- id: generatePostId(),
- content: input.content.trim(),
- // ...
- };
- }
- }
- ```
-
- ### Where to Use Pipes
- ```typescript
- // Controllers call pipes before application
- class PostController {
- static async commitCreatePost(input: CreatePostInput) {
- // 1. Normalize via pipe
- const post = PostPipe.normalizeCreate(input);
-
- // 2. Pass to application
- await PostApplication.create(post);
- }
- }
-
- // Services call pipes for external data
- class NexusPostService {
- static async fetch(id: string) {
- const raw = await queryNexus(`/v0/post/${id}`);
-
- // Normalize external shape
- return PostPipe.normalizeFromNexus(raw);
- }
- }
- ```
-
- ### Never Return Un-normalized Data
- ```typescript
- // ❌ Bad: Returning raw external shape
- return await queryNexus(`/v0/post/${id}`);
-
- // ✅ Good: Always normalize
- const raw = await queryNexus(`/v0/post/${id}`);
- return PostPipe.normalizeFromNexus(raw);
- ```
-
- ## Data Model Reference
-
- ### User Tables
- ```
- user_details - Profile data
- user_counts - Follower/following counts
- user_relations - Follow relationships
- user_ttl - Cache expiry
- ```
-
- ### Post Tables
- ```
- post_details - Post content
- post_counts - Like/reply counts
- post_relations - Author relationships
- post_tags - Tag associations
- post_ttl - Cache expiry
- ```
-
- ### Stream Tables
- ```
- post_streams - Post IDs in streams
- user_streams - User pubkys in streams
- tag_streams - Hot tags
- ```
-
- ## Quick Checklist
-
- When working with data:
-
- - [ ] Using composite IDs for posts (`author:postId`)?
- - [ ] Stream entries have ordering metadata?
- - [ ] TTL updated on every write?
- - [ ] External data normalized through pipes?
- - [ ] Pipes are pure (no IO)?
-
- ---
-
- **Reference**: `.cursor/adr/0002-composite-post-ids.md`, `.cursor/adr/0003-streams-as-caches.md`, `.cursor/adr/0005-ttl-refresh-policy.md`, `.cursor/adr/0006-pipes-normalization.md`
+ @docs/data-patterns.md