See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: collapse packages/atproto and packages/clickhouse into apps/web/app/lib/

Move the two workspace packages into the Adonis monolith under app/lib/.
Adds #lib/* subpath import alias in apps/web/package.json. Rewrites all
25 import sites from @skystar/atproto / @skystar/clickhouse to #lib/atproto/index
and #lib/clickhouse/index. Removes tsconfig references/paths, removes the
workspace entries from root package.json, removes Dockerfile package build
steps, and updates the spec doc to reflect the single-app layout.

169 tests still pass. Build, lint, and docker compose build all exit 0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+83 -207
-4
Dockerfile
··· 4 4 # Copy workspace manifests + lockfile for layer caching 5 5 COPY package.json package-lock.json ./ 6 6 COPY apps/web/package.json ./apps/web/ 7 - COPY packages/atproto/package.json ./packages/atproto/ 8 - COPY packages/clickhouse/package.json ./packages/clickhouse/ 9 7 # Use npm ci for deterministic installs; lockfile was generated on Linux so 10 8 # platform-native deps (better-sqlite3, @swc/core) resolve correctly here. 11 9 RUN apk add --no-cache python3 make g++ && npm ci ··· 16 14 # that `npm ci` works in the runtime stage. In a monorepo the lockfile lives at 17 15 # the root, so symlink it into the workspace before building. 18 16 RUN cp package-lock.json apps/web/package-lock.json 19 - RUN cd packages/atproto && npm run build 20 - RUN cd packages/clickhouse && npm run build 21 17 RUN cd apps/web && node ace build 22 18 23 19 FROM node:24-alpine AS runtime
+2 -2
apps/web/app/controllers/profile_controller.ts
··· 1 1 import { inject } from '@adonisjs/core' 2 2 import type { HttpContext } from '@adonisjs/core/http' 3 3 import { HandleResolver, InvalidHandleError, HandleNotFoundError } from '#services/handle_resolver' 4 - import { BlueskyRateLimitedError } from '@skystar/atproto' 5 - import { ClickHouseStore } from '@skystar/clickhouse' 4 + import { BlueskyRateLimitedError } from '#lib/atproto/index' 5 + import { ClickHouseStore } from '#lib/clickhouse/index' 6 6 import User from '#models/user' 7 7 import BackfillJobRow from '#models/backfill_job' 8 8 import BackfillJob from '#jobs/backfill_job'
+2 -2
apps/web/app/jobs/backfill_job.ts
··· 1 1 import { inject } from '@adonisjs/core' 2 2 import { Job } from '@adonisjs/queue' 3 - import { AtprotoClient, parseGetPostsResponse } from '@skystar/atproto' 4 - import { ClickHouseStore } from '@skystar/clickhouse' 3 + import { AtprotoClient, parseGetPostsResponse } from '#lib/atproto/index' 4 + import { ClickHouseStore } from '#lib/clickhouse/index' 5 5 import User from '#models/user' 6 6 import BackfillJobRow from '#models/backfill_job' 7 7
+1 -1
apps/web/app/services/handle_resolver.ts
··· 1 1 import { inject } from '@adonisjs/core' 2 - import { AtprotoClient, BlueskyRateLimitedError } from '@skystar/atproto' 2 + import { AtprotoClient, BlueskyRateLimitedError } from '#lib/atproto/index' 3 3 4 4 // --------------------------------------------------------------------------- 5 5 // Custom errors
+5 -5
apps/web/app/services/jetstream_consumer.ts
··· 1 - import type { EngagementEventRow } from '@skystar/clickhouse' 2 - import type { ClickHouseStore } from '@skystar/clickhouse' 3 - import type { PostSnapshot } from '@skystar/clickhouse' 4 - import { parseJetstreamEvent, parseAtUri } from '@skystar/atproto' 5 - import type { PostEvent } from '@skystar/atproto' 1 + import type { EngagementEventRow } from '#lib/clickhouse/index' 2 + import type { ClickHouseStore } from '#lib/clickhouse/index' 3 + import type { PostSnapshot } from '#lib/clickhouse/index' 4 + import { parseJetstreamEvent, parseAtUri } from '#lib/atproto/index' 5 + import type { PostEvent } from '#lib/atproto/index' 6 6 7 7 // --------------------------------------------------------------------------- 8 8 // WebSocket-like interface for dependency injection in tests
+1 -1
apps/web/commands/jetstream_consume.ts
··· 1 1 import { BaseCommand } from '@adonisjs/core/ace' 2 2 import db from '@adonisjs/lucid/services/db' 3 - import { ClickHouseStore } from '@skystar/clickhouse' 3 + import { ClickHouseStore } from '#lib/clickhouse/index' 4 4 import { JetstreamConsumer } from '#services/jetstream_consumer' 5 5 import { readCursor, writeCursor } from '#services/jetstream_cursor_io' 6 6 import User from '#models/user'
+2 -1
apps/web/package.json
··· 34 34 "#tests/*": "./tests/*.js", 35 35 "#start/*": "./start/*.js", 36 36 "#config/*": "./config/*.js", 37 - "#jobs/*": "./app/jobs/*.js" 37 + "#jobs/*": "./app/jobs/*.js", 38 + "#lib/*": "./app/lib/*.js" 38 39 }, 39 40 "devDependencies": { 40 41 "@adonisjs/assembler": "^8.3.0",
+1 -1
apps/web/providers/atproto_provider.ts
··· 1 1 import type { ApplicationService } from '@adonisjs/core/types' 2 - import { AtprotoClient, createAtprotoClient } from '@skystar/atproto' 2 + import { AtprotoClient, createAtprotoClient } from '#lib/atproto/index' 3 3 4 4 /** 5 5 * Registers AtprotoClient as a container singleton.
+1 -1
apps/web/providers/clickhouse_provider.ts
··· 1 1 import type { ApplicationService } from '@adonisjs/core/types' 2 - import { ClickHouseStore } from '@skystar/clickhouse' 2 + import { ClickHouseStore } from '#lib/clickhouse/index' 3 3 4 4 /** 5 5 * Registers ClickHouseStore as a container singleton.
+1 -1
apps/web/start/health.ts
··· 14 14 import { DbCheck } from '@adonisjs/lucid/database' 15 15 import { Result, BaseCheck } from '@adonisjs/core/health' 16 16 import type { HealthCheckResult } from '@adonisjs/core/types/health' 17 - import { ClickHouseStore } from '@skystar/clickhouse' 17 + import { ClickHouseStore } from '#lib/clickhouse/index' 18 18 import app from '@adonisjs/core/services/app' 19 19 20 20 /**
+1 -1
apps/web/tests/functional/profile_controller.spec.ts
··· 11 11 import { fileURLToPath } from 'node:url' 12 12 import { join } from 'node:path' 13 13 import testUtils from '@adonisjs/core/services/test_utils' 14 - import { ClickHouseStore } from '@skystar/clickhouse' 14 + import { ClickHouseStore } from '#lib/clickhouse/index' 15 15 import User from '#models/user' 16 16 17 17 // ---------------------------------------------------------------------------
+2 -2
apps/web/tests/functional/profile_controller_dispatch.spec.ts
··· 18 18 import BackfillJobRow from '#models/backfill_job' 19 19 import BackfillJob from '#jobs/backfill_job' 20 20 import { HandleResolver, HandleNotFoundError } from '#services/handle_resolver' 21 - import { BlueskyRateLimitedError } from '@skystar/atproto' 21 + import { BlueskyRateLimitedError } from '#lib/atproto/index' 22 22 23 23 // --------------------------------------------------------------------------- 24 24 // Helpers ··· 291 291 292 292 // The controller will try to call ClickHouseStore.getTopPosts — provide a stub 293 293 // that returns empty array so the profile page renders without ClickHouse 294 - const { ClickHouseStore } = await import('@skystar/clickhouse') 294 + const { ClickHouseStore } = await import('#lib/clickhouse/index') 295 295 swap(ClickHouseStore, { 296 296 async getTopPosts() { 297 297 return []
+1 -1
apps/web/tests/unit/atproto/at_uri.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 - import { parseAtUri } from '@skystar/atproto' 2 + import { parseAtUri } from '#lib/atproto/index' 3 3 4 4 test.group('parseAtUri', () => { 5 5 test('parses a did:plc AT-URI with post collection', ({ assert }) => {
+1 -1
apps/web/tests/unit/atproto/client.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 - import { AtprotoClient, BlueskyRateLimitedError } from '@skystar/atproto' 2 + import { AtprotoClient, BlueskyRateLimitedError } from '#lib/atproto/index' 3 3 4 4 // --------------------------------------------------------------------------- 5 5 // Mock Agent factory
+1 -1
apps/web/tests/unit/atproto/get_posts.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 - import { parseGetPostsResponse } from '@skystar/atproto' 2 + import { parseGetPostsResponse } from '#lib/atproto/index' 3 3 4 4 // --------------------------------------------------------------------------- 5 5 // Fixtures — shaped like the AppView app.bsky.feed.getPosts response
+1 -1
apps/web/tests/unit/atproto/jetstream.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 - import { parseJetstreamEvent } from '@skystar/atproto' 2 + import { parseJetstreamEvent } from '#lib/atproto/index' 3 3 4 4 // --------------------------------------------------------------------------- 5 5 // Real-world Jetstream event fixtures
+1 -1
apps/web/tests/unit/clickhouse_store.spec.ts
··· 12 12 import { randomBytes } from 'node:crypto' 13 13 import { fileURLToPath } from 'node:url' 14 14 import { join } from 'node:path' 15 - import { ClickHouseStore } from '@skystar/clickhouse' 15 + import { ClickHouseStore } from '#lib/clickhouse/index' 16 16 import { aLikeEvent, aSnapshot } from './clickhouse_store_fixtures.js' 17 17 18 18 // ---------------------------------------------------------------------------
+1 -1
apps/web/tests/unit/clickhouse_store_fixtures.ts
··· 2 2 * Test fixture builders for ClickHouseStore tests. 3 3 * All defaults are minimal valid values; override what your test cares about. 4 4 */ 5 - import type { EngagementEventRow, PostSnapshot } from '@skystar/clickhouse' 5 + import type { EngagementEventRow, PostSnapshot } from '#lib/clickhouse/index' 6 6 7 7 // --------------------------------------------------------------------------- 8 8 // PostSnapshot builder
+2 -2
apps/web/tests/unit/handle_resolver.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 2 import { HandleResolver, InvalidHandleError, HandleNotFoundError } from '#services/handle_resolver' 3 - import { BlueskyRateLimitedError } from '@skystar/atproto' 3 + import { BlueskyRateLimitedError } from '#lib/atproto/index' 4 4 5 5 // --------------------------------------------------------------------------- 6 6 // Minimal AtprotoClient mock ··· 16 16 if (config.resolveHandleError) throw config.resolveHandleError 17 17 return config.resolveHandleResult! 18 18 }, 19 - } as unknown as import('@skystar/atproto').AtprotoClient 19 + } as unknown as import('#lib/atproto/index').AtprotoClient 20 20 } 21 21 22 22 // ---------------------------------------------------------------------------
+3 -3
apps/web/tests/unit/jobs/backfill_job.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 2 import testUtils from '@adonisjs/core/services/test_utils' 3 - import type { PostSnapshot } from '@skystar/atproto' 4 - import type { AtprotoClient } from '@skystar/atproto' 5 - import type { ClickHouseStore } from '@skystar/clickhouse' 3 + import type { PostSnapshot } from '#lib/atproto/index' 4 + import type { AtprotoClient } from '#lib/atproto/index' 5 + import type { ClickHouseStore } from '#lib/clickhouse/index' 6 6 import User from '#models/user' 7 7 import BackfillJobRow from '#models/backfill_job' 8 8 import BackfillJob from '#jobs/backfill_job'
+3 -3
apps/web/tests/unit/services/jetstream_consumer.spec.ts
··· 5 5 * injected as fakes so these tests run without any external services. 6 6 */ 7 7 import { test } from '@japa/runner' 8 - import type { ClickHouseStore } from '@skystar/clickhouse' 9 - import type { EngagementEventRow } from '@skystar/clickhouse' 10 - import type { PostSnapshot } from '@skystar/clickhouse' 8 + import type { ClickHouseStore } from '#lib/clickhouse/index' 9 + import type { EngagementEventRow } from '#lib/clickhouse/index' 10 + import type { PostSnapshot } from '#lib/clickhouse/index' 11 11 import { 12 12 JetstreamConsumer, 13 13 type WebSocketLike,
+2 -10
apps/web/tsconfig.json
··· 2 2 "extends": "@adonisjs/tsconfig/tsconfig.app.json", 3 3 "compilerOptions": { 4 4 "rootDir": "./", 5 - "outDir": "./build", 6 - "paths": { 7 - "@skystar/atproto": ["../../packages/atproto/src/index.ts"], 8 - "@skystar/clickhouse": ["../../packages/clickhouse/src/index.ts"] 9 - } 10 - }, 11 - "references": [ 12 - { "path": "../../packages/atproto" }, 13 - { "path": "../../packages/clickhouse" } 14 - ] 5 + "outDir": "./build" 6 + } 15 7 }
+13 -21
docs/superpowers/specs/2026-04-11-skystar-bluesky-design.md
··· 90 90 - **Templating:** Edge (Adonis's first-party SSR template engine). 91 91 - **Read-side ORM:** Lucid (Adonis's first-party Knex-based ORM), used only 92 92 for SQLite. 93 - - **Engagement store:** ClickHouse, accessed via `@clickhouse/client` from a 94 - shared `packages/clickhouse` package. No ORM. 93 + - **Engagement store:** ClickHouse, accessed via `@clickhouse/client`. No ORM. 95 94 - **Metadata store:** SQLite (file-backed, WAL mode), via Lucid. 96 95 - **Jetstream worker:** Adonis Ace command (`node ace jetstream:consume`) 97 96 with `staysAlive = true`. Same project, same image, same code as the web ··· 99 98 - **Background jobs:** `@adonisjs/queue` (the official AdonisJS queue 100 99 package) with the **database adapter on SQLite**. Used for backfill jobs 101 100 only. Run via `node ace queue:work` as a third process. 102 - - **Atproto client:** `@atproto/api` (the official TypeScript SDK), wrapped 103 - in `packages/atproto` for centralized rate-limit handling and parsing. 101 + - **Atproto client:** `@atproto/api` (the official TypeScript SDK), with rate-limit handling and parsing in `apps/web/app/lib/atproto/`. 104 102 - **Containerization:** Single `Dockerfile` (Node 24 alpine multi-stage with 105 103 tini), single `docker-compose.yml` with four services: `clickhouse`, 106 104 `web`, `jetstream-worker`, `queue-worker`. ··· 154 152 │ │ │ │ │ 155 153 │ │ reads │ writes │ reads+writes│ 156 154 │ ▼ ▼ ▼ │ 157 - │ ┌────────────────────────────────────────────────────────┐ │ 158 - │ │ shared TS package: packages/clickhouse │ │ 159 - │ │ (@clickhouse/client wrapper, schema, query funcs) │ │ 160 - │ └────────────────────────┬───────────────────────────────┘ │ 155 + │ (via app/lib/clickhouse/) │ 161 156 │ ▼ │ 162 157 │ ┌──────────────────┐ ┌──────────────────────┐ │ 163 158 │ │ ClickHouse │ │ SQLite (Lucid) │ │ ··· 184 179 │ └── web/ # Adonis project root 185 180 │ ├── app/ 186 181 │ │ ├── controllers/ # ProfileController, SearchController 187 - │ │ ├── models/ # User, BackfillJob row (Lucid) 188 182 │ │ ├── jobs/ # BackfillJob (@adonisjs/queue Job) 189 - │ │ └── services/ # HandleResolver, BackfillRunner 183 + │ │ ├── lib/ # atproto/ and clickhouse/ helpers 184 + │ │ ├── models/ # User, BackfillJob row (Lucid) 185 + │ │ └── services/ # HandleResolver, JetstreamConsumer 190 186 │ ├── commands/ 191 187 │ │ └── jetstream_consume.ts # Ace staysAlive worker 192 188 │ ├── database/ 193 189 │ │ ├── migrations/ # SQLite migrations (Adonis) 194 190 │ │ └── clickhouse/ # ordered .sql files for CH migrations 195 - │ ├── resources/views/ # Edge templates 196 - │ └── start/ # routes.ts, kernel, etc. 197 - ├── packages/ 198 - │ ├── clickhouse/ # CH client wrapper, schema, queries 199 - │ └── atproto/ # @atproto/api wrapper, parsing, rate limits 191 + │ └── resources/views/ # Edge templates 200 192 ├── docker-compose.yml 201 193 └── Dockerfile 202 194 ``` 203 195 204 - Both packages are imported by the Adonis app (controllers, services, the 205 - Ace command) via TypeScript path aliases. There is one published image. 196 + There is one published image. 206 197 207 198 ### Key invariants 208 199 209 200 1. The web and worker processes never communicate directly. All coordination 210 201 is via SQLite and ClickHouse. 211 - 2. The shared `packages/clickhouse` package is the only place that knows 212 - ClickHouse SQL. Everywhere else uses its query functions. 202 + 2. The `apps/web/app/lib/clickhouse/` directory is the only place that knows 203 + ClickHouse SQL. Every controller, job, and service imports from `#lib/clickhouse` 204 + — there's no inline SQL anywhere else. 213 205 3. Every per-request operation is a constant number of SQL/ClickHouse 214 206 queries, regardless of how many users are tracked. 215 207 ··· 705 697 706 698 ### Unit tests (Japa) 707 699 708 - - `packages/clickhouse/queries.ts` — top-25 query builder, fixtures seeded 700 + - `app/lib/clickhouse/store.ts` — top-25 query builder, fixtures seeded 709 701 into a real ClickHouse. Tests cover: all-time and `?days=` ordering, 710 702 per-post watermark math, ties broken by `post_created_at`, posts with 711 703 zero engagement excluded, tombstoned posts excluded. 712 - - `packages/atproto/parse.ts` — pure functions for AT-URI parsing, 704 + - `app/lib/atproto/parsers/` — pure functions for AT-URI parsing, 713 705 Jetstream event JSON → internal shape, `getPosts` response → internal 714 706 shape. 715 707 - `apps/web/app/services/handle_resolver.ts` — handle normalization,
+3 -11
package-lock.json
··· 8 8 "name": "skystar", 9 9 "version": "0.0.0", 10 10 "workspaces": [ 11 - "apps/web", 12 - "packages/atproto", 13 - "packages/clickhouse" 11 + "apps/web" 14 12 ], 15 13 "devDependencies": { 16 14 "pino-pretty": "^13.1.3" ··· 2644 2642 "funding": { 2645 2643 "url": "https://github.com/sponsors/sindresorhus" 2646 2644 } 2647 - }, 2648 - "node_modules/@skystar/atproto": { 2649 - "resolved": "packages/atproto", 2650 - "link": true 2651 - }, 2652 - "node_modules/@skystar/clickhouse": { 2653 - "resolved": "packages/clickhouse", 2654 - "link": true 2655 2645 }, 2656 2646 "node_modules/@speed-highlight/core": { 2657 2647 "version": "1.2.15", ··· 9019 9009 "packages/atproto": { 9020 9010 "name": "@skystar/atproto", 9021 9011 "version": "0.0.0", 9012 + "extraneous": true, 9022 9013 "dependencies": { 9023 9014 "@atproto/api": "^0.19.8" 9024 9015 }, ··· 9029 9020 "packages/clickhouse": { 9030 9021 "name": "@skystar/clickhouse", 9031 9022 "version": "0.0.0", 9023 + "extraneous": true, 9032 9024 "dependencies": { 9033 9025 "@clickhouse/client": "^1.10.1", 9034 9026 "@skystar/atproto": "*"
+1 -3
package.json
··· 3 3 "version": "0.0.0", 4 4 "private": true, 5 5 "workspaces": [ 6 - "apps/web", 7 - "packages/atproto", 8 - "packages/clickhouse" 6 + "apps/web" 9 7 ], 10 8 "engines": { 11 9 "node": ">=24.0.0"
-24
packages/atproto/package.json
··· 1 - { 2 - "name": "@skystar/atproto", 3 - "version": "0.0.0", 4 - "private": true, 5 - "type": "module", 6 - "scripts": { 7 - "build": "tsc --build", 8 - "clean": "rm -rf dist tsconfig.tsbuildinfo" 9 - }, 10 - "exports": { 11 - ".": { 12 - "import": "./dist/index.js", 13 - "types": "./dist/index.d.ts" 14 - } 15 - }, 16 - "main": "./dist/index.js", 17 - "types": "./dist/index.d.ts", 18 - "engines": { 19 - "node": ">=24.0.0" 20 - }, 21 - "dependencies": { 22 - "@atproto/api": "^0.19.8" 23 - } 24 - }
+7 -6
packages/atproto/src/client.ts apps/web/app/lib/atproto/client.ts
··· 54 54 constructor( 55 55 message: string, 56 56 readonly attempts: number, 57 - readonly cause?: unknown, 57 + readonly cause?: unknown 58 58 ) { 59 59 super(message, { cause }) 60 60 this.name = 'BlueskyRateLimitedError' ··· 78 78 const RATE_LIMIT_REMAINING_THRESHOLD = 10 79 79 80 80 /** Default sleep implementation using setTimeout */ 81 - const defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms)) 81 + const defaultSleep = (ms: number): Promise<void> => 82 + new Promise((resolve) => setTimeout(resolve, ms)) 82 83 83 84 function is429(err: unknown): boolean { 84 85 if (err instanceof Error) { ··· 178 179 179 180 if (!remainingStr || !resetStr) return 180 181 181 - const remaining = parseInt(remainingStr, 10) 182 - const resetEpochSeconds = parseInt(resetStr, 10) 182 + const remaining = Number.parseInt(remainingStr, 10) 183 + const resetEpochSeconds = Number.parseInt(resetStr, 10) 183 184 184 - if (isNaN(remaining) || isNaN(resetEpochSeconds)) return 185 + if (Number.isNaN(remaining) || Number.isNaN(resetEpochSeconds)) return 185 186 if (remaining >= RATE_LIMIT_REMAINING_THRESHOLD) return 186 187 187 188 const resetMs = resetEpochSeconds * 1000 ··· 215 216 lastError instanceof Error ? lastError.message : String(lastError) 216 217 }`, 217 218 MAX_RETRIES + 1, 218 - lastError, 219 + lastError 219 220 ) 220 221 } 221 222
packages/atproto/src/index.ts apps/web/app/lib/atproto/index.ts
+1 -3
packages/atproto/src/parsers/at_uri.ts apps/web/app/lib/atproto/parsers/at_uri.ts
··· 27 27 const parts = withoutScheme.split('/') 28 28 29 29 if (parts.length < 3) { 30 - throw new Error( 31 - `Invalid AT-URI: must have authority, collection, and rkey — got: ${uri}` 32 - ) 30 + throw new Error(`Invalid AT-URI: must have authority, collection, and rkey — got: ${uri}`) 33 31 } 34 32 35 33 const [authority, collection, rkey] = parts
packages/atproto/src/parsers/get_posts.ts apps/web/app/lib/atproto/parsers/get_posts.ts
+1 -4
packages/atproto/src/parsers/jetstream.ts apps/web/app/lib/atproto/parsers/jetstream.ts
··· 37 37 return typeof v === 'boolean' ? v : undefined 38 38 } 39 39 40 - function getObject( 41 - obj: Record<string, unknown>, 42 - key: string 43 - ): Record<string, unknown> | undefined { 40 + function getObject(obj: Record<string, unknown>, key: string): Record<string, unknown> | undefined { 44 41 const v = obj[key] 45 42 return isObject(v) ? v : undefined 46 43 }
packages/atproto/src/types.ts apps/web/app/lib/atproto/types.ts
-17
packages/atproto/tsconfig.json
··· 1 - { 2 - "compilerOptions": { 3 - "target": "ES2022", 4 - "module": "NodeNext", 5 - "moduleResolution": "NodeNext", 6 - "outDir": "./dist", 7 - "rootDir": "./src", 8 - "strict": true, 9 - "composite": true, 10 - "declaration": true, 11 - "declarationMap": true, 12 - "sourceMap": true, 13 - "skipLibCheck": true 14 - }, 15 - "include": ["src/**/*"], 16 - "exclude": ["node_modules", "dist"] 17 - }
-25
packages/clickhouse/package.json
··· 1 - { 2 - "name": "@skystar/clickhouse", 3 - "version": "0.0.0", 4 - "private": true, 5 - "type": "module", 6 - "scripts": { 7 - "build": "tsc --build", 8 - "clean": "rm -rf dist tsconfig.tsbuildinfo" 9 - }, 10 - "exports": { 11 - ".": { 12 - "import": "./dist/index.js", 13 - "types": "./dist/index.d.ts" 14 - } 15 - }, 16 - "main": "./dist/index.js", 17 - "types": "./dist/index.d.ts", 18 - "engines": { 19 - "node": ">=24.0.0" 20 - }, 21 - "dependencies": { 22 - "@clickhouse/client": "^1.10.1", 23 - "@skystar/atproto": "*" 24 - } 25 - }
+1 -1
packages/clickhouse/src/index.ts apps/web/app/lib/clickhouse/index.ts
··· 6 6 EngagementEventRow, 7 7 ClickHouseConfig, 8 8 } from './types.js' 9 - export type { PostSnapshot } from '@skystar/atproto' 9 + export type { PostSnapshot } from '#lib/atproto/index'
+21 -29
packages/clickhouse/src/store.ts apps/web/app/lib/clickhouse/store.ts
··· 1 1 import { createClient } from '@clickhouse/client' 2 2 import type { ClickHouseClient } from '@clickhouse/client' 3 - import type { PostSnapshot } from '@skystar/atproto' 3 + import type { PostSnapshot } from '#lib/atproto/index' 4 4 import type { 5 5 ClickHouseConfig, 6 6 EngagementEventRow, ··· 59 59 LIMIT 25` 60 60 61 61 /** likes, all-time */ 62 - const QUERY_LIKES_ALL: string = 63 - `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE}${GROUP_BY} 62 + const QUERY_LIKES_ALL: string = `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE}${GROUP_BY} 64 63 ORDER BY likes DESC, s.post_created_at DESC${LIMIT}` 65 64 66 65 /** likes, windowed by N days */ 67 - const QUERY_LIKES_WINDOWED: string = 68 - `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE} 66 + const QUERY_LIKES_WINDOWED: string = `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE} 69 67 AND s.post_created_at >= now() - INTERVAL {daysWindow: Int32} DAY${GROUP_BY} 70 68 ORDER BY likes DESC, s.post_created_at DESC${LIMIT}` 71 69 72 70 /** reposts, all-time */ 73 - const QUERY_REPOSTS_ALL: string = 74 - `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE}${GROUP_BY} 71 + const QUERY_REPOSTS_ALL: string = `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE}${GROUP_BY} 75 72 ORDER BY reposts DESC, s.post_created_at DESC${LIMIT}` 76 73 77 74 /** reposts, windowed by N days */ 78 - const QUERY_REPOSTS_WINDOWED: string = 79 - `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE} 75 + const QUERY_REPOSTS_WINDOWED: string = `SELECT${SELECT_COLUMNS}${FROM_JOIN}${WHERE_BASE} 80 76 AND s.post_created_at >= now() - INTERVAL {daysWindow: Int32} DAY${GROUP_BY} 81 77 ORDER BY reposts DESC, s.post_created_at DESC${LIMIT}` 82 78 ··· 162 158 const windowed = query.daysWindow !== undefined 163 159 const sql = pickQuery(query.kind, windowed) 164 160 165 - const query_params: Record<string, unknown> = { 161 + const queryParams: Record<string, unknown> = { 166 162 authorDid: query.authorDid, 167 163 } 168 164 if (windowed) { 169 - query_params['daysWindow'] = query.daysWindow 165 + queryParams['daysWindow'] = query.daysWindow 170 166 } 171 167 172 168 let resultSet ··· 174 170 resultSet = await this.client.query({ 175 171 query: sql, 176 172 format: 'JSONEachRow', 177 - query_params, 173 + query_params: queryParams, 178 174 }) 179 175 } catch (err) { 180 - throw new Error( 181 - `ClickHouseStore.getTopPosts failed for author ${query.authorDid}`, 182 - { cause: err }, 183 - ) 176 + throw new Error(`ClickHouseStore.getTopPosts failed for author ${query.authorDid}`, { 177 + cause: err, 178 + }) 184 179 } 185 180 186 181 const rows = await resultSet.json<TopPostsRow>() ··· 224 219 format: 'JSONEachRow', 225 220 }) 226 221 } catch (err) { 227 - throw new Error( 228 - `ClickHouseStore.insertPostSnapshots failed (${snapshots.length} rows)`, 229 - { cause: err }, 230 - ) 222 + throw new Error(`ClickHouseStore.insertPostSnapshots failed (${snapshots.length} rows)`, { 223 + cause: err, 224 + }) 231 225 } 232 226 } 233 227 ··· 258 252 format: 'JSONEachRow', 259 253 }) 260 254 } catch (err) { 261 - throw new Error( 262 - `ClickHouseStore.insertEngagementEvents failed (${events.length} rows)`, 263 - { cause: err }, 264 - ) 255 + throw new Error(`ClickHouseStore.insertEngagementEvents failed (${events.length} rows)`, { 256 + cause: err, 257 + }) 265 258 } 266 259 } 267 260 ··· 302 295 } catch (err) { 303 296 throw new Error( 304 297 `ClickHouseStore.tombstonePost failed for ${postUri} (author ${postAuthorDid})`, 305 - { cause: err }, 298 + { cause: err } 306 299 ) 307 300 } 308 301 } ··· 348 341 query_params: { authorDid }, 349 342 }) 350 343 } catch (err) { 351 - throw new Error( 352 - `ClickHouseStore.tombstoneUserSnapshots failed for author ${authorDid}`, 353 - { cause: err }, 354 - ) 344 + throw new Error(`ClickHouseStore.tombstoneUserSnapshots failed for author ${authorDid}`, { 345 + cause: err, 346 + }) 355 347 } 356 348 } 357 349 }
packages/clickhouse/src/types.ts apps/web/app/lib/clickhouse/types.ts
-17
packages/clickhouse/tsconfig.json
··· 1 - { 2 - "compilerOptions": { 3 - "target": "ES2022", 4 - "module": "NodeNext", 5 - "moduleResolution": "NodeNext", 6 - "outDir": "./dist", 7 - "rootDir": "./src", 8 - "strict": true, 9 - "composite": true, 10 - "declaration": true, 11 - "declarationMap": true, 12 - "sourceMap": true, 13 - "skipLibCheck": true 14 - }, 15 - "include": ["src/**/*"], 16 - "exclude": ["node_modules", "dist"] 17 - }