Skip to content

Commit 354e810

Browse files
committed
docs: track the design specs and implementation plans in docs/superpowers
1 parent 18deaaa commit 354e810

8 files changed

Lines changed: 5336 additions & 2 deletions

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,3 @@ Thumbs.db
5050
.claude/worktrees
5151
.nx/polygraph
5252
.nx/self-healing
53-
# Local AI working docs
54-
docs/superpowers/
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# SSR / Hydration Support Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** `injectQuery`/`injectQueries` fetch real data during Angular SSR, transfer it via `TransferState`, and seed hydrated clients with it; every helper is SSR-safe.
6+
7+
**Architecture:** On the server platform the `ConvexClient` is created `disabled: true` (no WebSocket); a `ConvexServerQueryLoader` runs one-shot queries over `ConvexHttpClient` inside `PendingTasks` and writes results to `TransferState`. On the browser, `ConvexHydrationState` seeds helpers from `TransferState` until `ApplicationRef.whenStable()`. See spec: `docs/superpowers/specs/2026-06-10-ssr-hydration-design.md`.
8+
9+
**Tech Stack:** Angular 21 (`TransferState`, `PendingTasks`, `isPlatformServer`), convex `ConvexHttpClient`, `convexToJson`/`jsonToConvex`, Jest + jest-preset-angular.
10+
11+
---
12+
13+
### Task 0: Branch
14+
15+
- [ ] `git checkout -b feat/ssr-hydration` in `convex-angular/`.
16+
17+
### Task 1: State transfer utilities + hydration service
18+
19+
**Files:**
20+
21+
- Create: `packages/convex-angular/src/lib/ssr/state-transfer.ts`
22+
- Test: `packages/convex-angular/src/lib/ssr/state-transfer.spec.ts`
23+
- Modify: `packages/convex-angular/src/lib/providers/inject-query.ts` (extract `serializeQueryArgs` later in Task 4; the util lives in state-transfer.ts)
24+
25+
Interfaces (locked):
26+
27+
```ts
28+
export type TransferredQueryResult = { d: JsonValue } | { u: true };
29+
export function serializeQueryArgs(args: Record<string, Value>): string; // JSON.stringify(convexToJson(args))
30+
export function makeQueryStateKey(queryName: string, argsKey: string): StateKey<TransferredQueryResult>; // 'cva:' + queryName + ':' + argsKey
31+
export function wrapQueryResult(value: Value | undefined): TransferredQueryResult;
32+
export function unwrapQueryResult(t: TransferredQueryResult): Value | undefined;
33+
34+
@Injectable()
35+
export class ConvexHydrationState {
36+
// inject(TransferState), inject(ApplicationRef)
37+
// active until first whenStable() resolution
38+
consume(queryName: string, argsKey: string): { value: Value | undefined } | undefined;
39+
}
40+
```
41+
42+
- [ ] Write failing specs: key format; wrap/unwrap round-trip for plain object, BigInt (Int64), undefined; `consume` returns `{value}` when TransferState has key; returns undefined when absent; returns undefined after `ApplicationRef.whenStable()` resolved (use TestBed + `await appRef.whenStable()`).
43+
- [ ] Run `nx test convex-angular --testFile=state-transfer.spec.ts` → FAIL.
44+
- [ ] Implement `state-transfer.ts`.
45+
- [ ] Tests pass. Commit `feat(ssr): add query state transfer utilities and hydration service`.
46+
47+
### Task 2: Server query loader + provider wiring
48+
49+
**Files:**
50+
51+
- Create: `packages/convex-angular/src/lib/ssr/server-query-loader.ts`
52+
- Test: `packages/convex-angular/src/lib/ssr/server-query-loader.spec.ts`
53+
- Modify: `packages/convex-angular/src/lib/tokens/convex.ts`
54+
- Modify: `packages/convex-angular/src/index.ts` (export `ConvexSsrOptions`, `ProvideConvexOptions`)
55+
56+
Interfaces (locked):
57+
58+
```ts
59+
// tokens/convex.ts
60+
export interface ConvexSsrOptions {
61+
fetchOnServer?: boolean;
62+
authToken?: () => string | null | undefined | Promise<string | null | undefined>;
63+
}
64+
export interface ProvideConvexOptions extends ConvexClientOptions {
65+
ssr?: ConvexSsrOptions;
66+
}
67+
/** @internal */ export const CONVEX_SSR_CONFIG: InjectionToken<{ url: string; ssr: ConvexSsrOptions }>;
68+
/** @internal */ export const CONVEX_HTTP_CLIENT: InjectionToken<Pick<ConvexHttpClient, 'query' | 'setAuth'>>; // default factory: new ConvexHttpClient(url)
69+
70+
// server-query-loader.ts
71+
@Injectable()
72+
export class ConvexServerQueryLoader {
73+
readonly enabled: boolean; // ssr.fetchOnServer !== false
74+
fetch<Query extends FunctionReference<'query'>>(
75+
query: Query,
76+
args: FunctionArgs<Query>,
77+
argsKey: string,
78+
): Promise<FunctionReturnType<Query>>;
79+
}
80+
```
81+
82+
`provideConvex(url, options)`:
83+
84+
- strips `ssr` from options before constructing `ConvexClient`;
85+
- on `isPlatformServer(PLATFORM_ID)` adds `disabled: true`;
86+
- provides `CONVEX_SSR_CONFIG`, `CONVEX_HTTP_CLIENT` (factory), `ConvexServerQueryLoader`, `ConvexHydrationState`.
87+
88+
Loader behavior: dedup map keyed `queryName + ':' + argsKey` caching the promise (including rejections); auth token resolved once per instance and applied via `httpClient.setAuth(token)` when non-null; body runs inside `pendingTasks.run(...)`; on success `transferState.set(makeQueryStateKey(...), wrapQueryResult(result))`; rejection propagates to callers but never unhandled inside the pending task (catch inside, rethrow to callers from the cached promise).
89+
90+
- [ ] Write failing specs (mock `CONVEX_HTTP_CLIENT` via TestBed provider): single fetch writes TransferState; concurrent fetch same key → one HTTP call; different args → two calls; authToken applied once before first query; query rejection → caller sees error, TransferState empty; `enabled === false` when `fetchOnServer: false`.
91+
- [ ] FAIL run, implement, PASS run.
92+
- [ ] Spec for provideConvex: on server platform client is `disabled`; `ssr` key not forwarded to client options (assert via `injectConvex().disabled` with `PLATFORM_ID: 'server'`).
93+
- [ ] Commit `feat(ssr): add server query loader and platform-aware provideConvex`.
94+
95+
### Task 3: SSR-safety guards in non-query helpers
96+
97+
**Files:**
98+
99+
- Modify: `packages/convex-angular/src/lib/providers/inject-connection-state.ts` (disabled → static default `ConnectionState`)
100+
- Modify: `packages/convex-angular/src/lib/providers/inject-prewarm-query.ts` (disabled → `prewarm()` no-op)
101+
- Test: extend `inject-connection-state.spec.ts`, `inject-prewarm-query.spec.ts`
102+
103+
Default state object: `{ hasInflightRequests: false, isWebSocketConnected: false, timeOfOldestInflightRequest: null, hasEverConnected: false, connectionCount: 0, connectionRetries: 0, inflightMutations: 0, inflightActions: 0 }`.
104+
105+
- [ ] Failing specs: mock client with `disabled: true` and throwing `connectionState` → signal returns default, no subscribe call; prewarm on disabled client → no `onUpdate`, no timer.
106+
- [ ] Implement, PASS, commit `fix(ssr): make connection state and prewarm helpers safe on disabled clients`.
107+
108+
### Task 4: injectQuery server fetch + hydration seeding
109+
110+
**Files:**
111+
112+
- Modify: `packages/convex-angular/src/lib/providers/inject-query.ts`
113+
- Test: extend `packages/convex-angular/src/lib/providers/inject-query.spec.ts` (new describe blocks `SSR` and `hydration`)
114+
115+
Changes:
116+
117+
- Replace local `serializeArgs` with `serializeQueryArgs` from `../ssr/state-transfer`.
118+
- `const isServer = isPlatformServer(inject(PLATFORM_ID));`
119+
- `const serverLoader = isServer ? inject(ConvexServerQueryLoader, { optional: true }) : null;`
120+
- `const hydration = !isServer ? inject(ConvexHydrationState, { optional: true }) : null;`
121+
- Effect, non-skip path:
122+
- **server:** no subscription. If `serverLoader?.enabled`: `serverLoader.fetch(query, args, argsKey).then/catch` under generation guard → success: `data.set`, `error.set(undefined)`, `isLoading.set(false)`, `onSuccess`; failure: `error.set`, `isLoading.set(false)`, `onError`. Else: remain `pending`.
123+
- **browser:** warm-cache read only when `!convex.disabled`; if no warm cache, `hydration?.consume(queryName, argsKey)` hit → `data.set(value)`, `error.set(undefined)`, `isLoading.set(false)`; subscription registered as today.
124+
125+
- [ ] Failing specs (server platform via `{ provide: PLATFORM_ID, useValue: 'server' }`, loader mocked or real with mocked `CONVEX_HTTP_CLIENT`): success → status `'success'` + data; error → status `'error'`; `onUpdate` never called; skip works; no loader provided → stays `'pending'`; arg change mid-fetch → stale result dropped (generation guard).
126+
- [ ] Failing specs (browser + TransferState pre-set): instant `'success'` with transferred data before any `onUpdate`; live update overwrites; warm cache wins over transfer; after `whenStable` no seeding.
127+
- [ ] Implement, PASS (full file: `nx test convex-angular --testFile=inject-query.spec.ts`), commit `feat(ssr): server-side fetching and hydration seeding for injectQuery`.
128+
129+
### Task 5: injectQueries same treatment
130+
131+
**Files:**
132+
133+
- Modify: `packages/convex-angular/src/lib/providers/inject-queries.ts`
134+
- Test: extend `inject-queries.spec.ts`
135+
136+
Same branches as Task 4 applied per definition entry (each entry has its own argsKey, fetch, and seed). Guard `localQueryResult` behind `!convex.disabled`.
137+
138+
- [ ] Failing specs: server fetch resolves all entries; one entry error → its `errors` record entry set, others succeed; hydration seeds entries individually.
139+
- [ ] Implement, PASS, commit `feat(ssr): server-side fetching and hydration seeding for injectQueries`.
140+
141+
### Task 6: Full-suite verification + docs
142+
143+
**Files:**
144+
145+
- Modify: `packages/convex-angular/README.md` (new "Server-side rendering" section)
146+
- Modify: `packages/convex-angular/src/index.ts` (verify exports annotated `@public`)
147+
148+
- [ ] `nx test convex-angular` → all green (290 existing + new).
149+
- [ ] `nx build convex-angular` → success.
150+
- [ ] `nx lint convex-angular` → clean.
151+
- [ ] README: setup snippet (`provideConvex(url, { ssr: { authToken } })` + `provideClientHydration()`), cookie/`REQUEST`-token example, per-helper SSR behavior table, note paginated queries load after hydration.
152+
- [ ] Commit `docs(ssr): document server-side rendering support`.
153+
154+
### Task 7: Demo smoke (stretch, time-boxed)
155+
156+
- [ ] Only if Tasks 1–6 complete cleanly: attempt `ng add @angular/ssr` on `apps/frontend`; if the env-var esbuild plugin conflicts, revert and leave documented as follow-up (spec lists it out of scope).

0 commit comments

Comments
 (0)