Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/specs/solid-monkeytype/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ Serial; each step needs the previous one. Verify each in the browser (`__mt.chec

| Done | ID | Size | Step | Blocked by | Notes |
|---|---|---|---|---|---|
| [ ] | U1 | L · PR | Single-row user collection with `getUser()` and named writes (direct writes for server-side effects, optimistic actions for user changes, incl. quote ratings); convert **all writers** (result save, xp, badges, inbox count, lb memory, streak offset, quote rate, favourite quotes, edit profile, update name, unlink Discord, Discord OAuth, auth); delete the reactive mirror; `DB.getSnapshot()` becomes a read-only shim over the row | none | Writers must move in the same PR: there's no mutable object left |
| [ ] | U2 | M · PR | Activity calendar: plain activity data in the user row; the calendar becomes a Solid component; delete the imperative test-activity modules | U1 (data shape) | Can share a PR with U1 |
| [ ] | U3 | M · PR | Remove the snapshot-updated auth event (PSA, last-signed-out-result modal read `getUser()`); auth becomes a signal the router reads, with the user-data load as state (loading/loaded/failed) plus `reloadUserData()` replacing the event's promise; delete the auth bus | U1 | |
| [x] | U1 | L · PR | Single-row user collection with `getUser()` and named writes (direct writes for server-side effects, optimistic actions for user changes, incl. quote ratings); convert **all writers** (result save, xp, badges, inbox count, lb memory, streak offset, quote rate, favourite quotes, edit profile, update name, unlink Discord, Discord OAuth, auth); delete the reactive mirror; `DB.getSnapshot()` becomes a read-only shim over the row | none | Done: `collections/user.ts` (local-only collection; `loadUserData`/`clearUser` for auth); `DB.getSnapshot = getUser`; last-result signal in `states/last-result.ts` |
| [ ] | U2 | M · PR | Activity calendar (row already holds plain activity data; the per-year cache sits in `db.ts`): the calendar becomes a Solid component; delete the imperative test-activity modules | U1 (data shape) | Can share a PR with U1 |
| [ ] | U3 | M · PR | Remove the snapshot-updated auth event (PSA, last-signed-out-result modal read `getUser()`); auth becomes a signal the router reads, with the user-data load as state (loading/loaded/failed) plus `reloadUserData()` (reruns `loadUserData()`) replacing the event's promise; delete the auth bus | U1 | |
| [ ] | U4 | S · commit per area | Readers move from the `DB.getSnapshot()` shim to `getUser()`, by area; the last PR deletes the shim and the snapshot code in `db.ts` | U1 | Parallel by area |
| [ ] | U5 | M · PR | Tags, presets and custom themes get the `getUser()` pattern: plain getters backed by a module-level reactive mirror, replacing their live-query hooks (keep `gcTime: Infinity`). Rewrite the non-test `__nonReactive` callers as Solid code reading the getters: commandline tag/preset lists, the custom-themes check in config metadata, the tag PB helpers in `db.ts` (or they go with U4) | none | Test-page callers go with T6a/T6c |
| [ ] | U6 | S · commit | Delete `__nonReactive` from every module once no caller is left | U5, T6a, T6c | Pure deletion: a caller still left means a step missed it |
Expand Down
10 changes: 8 additions & 2 deletions docs/specs/solid-monkeytype/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ Users should see no behaviour change, except where a bug at today's seams disapp
- Interim rule until the event bus is gone: the store must be updated *before* the event fires, so listeners never see stale values.
- **Config metadata** is the single source for settings UI, commandline generation, validation and restart behaviour (`changeRequiresRestart`). The separate commandline-metadata module is merged into it. Commandline icons use the icon component's types, not strings.
- **User**: a single-row TanStack DB collection keyed by uid replaces both the mutable user snapshot and its reactive mirror (and the planned snapshot store).
- It's a query collection on the existing cached user response (as tags already are); the mapping from the API shape lives in its `queryFn`. `enabled` while signed in, `staleTime`/`gcTime: Infinity`.
- It's a local-only collection: the auth flow loads the row (the mapping from the API shape lives in the user module) and sign-out deletes it. Not a query collection: a refetch resolves before the row lands in the collection, and a disabled query doesn't clear the row on sign-out.
- The row holds what the snapshot holds today. Test activity is plain data (days and last day); the calendar class and per-year cache live in the calendar component. Tags, presets etc. stay in their own collections; the last-result signal stays a signal.
- Signed out means no row; consumers handle "no user". No default user stands in. A failed user fetch shows the error and leaves no row (today it silently falls back to defaults). Sign-in waits for the row inside the blocking user-data step, as it does for tags and presets.
- **Reads**: one `getUser()`, backed by a module-level reactive mirror of the row: reactive inside components and effects, a plain synchronous read elsewhere (engine at finish, plain modules). No separate hook, no `__nonReactive`, no direct collection access outside the module.
- **Writes**: named functions only; the raw update stays private. Mirrors of what the server already did (xp, streak, typing stats, PBs, leaderboard memory, badges, inbox count) are direct writes with no API call or refetch. User-initiated changes (name, Discord unlink, streak offset, quote ratings, favourite quotes) are optimistic actions that call the API and roll back on failure, as custom themes and connections do. Server-owned flags (banned, verified, leaderboard opt-out) have no setters.
- **Writes**: named functions only; the raw update stays private. Mirrors of what the server already did (xp, streak, typing stats, PBs and PB reset, leaderboard memory, badges, inbox count, Discord link) are direct writes with no API call or refetch. User-initiated changes (name, Discord unlink, streak offset, quote ratings, favourite quotes, profile) are optimistic actions that call the API and roll back on failure, as custom themes and connections do. On success an action keeps the current row with its change applied, not the row as it was when the action started, so direct writes made meanwhile survive. Server-owned flags (banned, verified, leaderboard opt-out) have no setters.
- The auth bus's snapshot-updated event goes away; consumers read `getUser()`.
- **Auth**: auth state is a signal (unknown, signed in, signed out), which the router's auth context and UI read. The user-data load sits next to it as state (loading, loaded, failed with an error) plus a `reloadUserData()` command; the router's auth context reads it for the blocking user-data step (user row, tags, presets) and its retry, instead of receiving a promise through an event. The auth bus (both its auth-state-changed and snapshot-updated events) is deleted.
- **Server data**: Query for read-only server data, DB collections for user-owned lists (results, tags, presets, connections, ape keys, inbox, custom themes, filter presets) and the single-row user collection above.
Expand Down Expand Up @@ -266,6 +266,12 @@ From config (C1):
- A key that fails to apply during a whole-config apply holds its default in the store (it used to hold the rejected value).
- A single setting change runs store effects before config event listeners. A whole-config apply runs them once, before failed keys are re-saved and before full-config-change finished (they used to run after it).

From user data (U1):

- Resetting PBs updates the PBs shown straight away (the reactive copy used to keep the old ones).
- User changes (name, Discord unlink, streak offset, quote rating, favourite quote, profile) show at once and roll back if the request fails; they used to show only after the request.
- The account page's activity calendar updates after a test without a reload.

## Settled decisions (grilling, 2026-10-24)

| # | Decision |
Expand Down
14 changes: 6 additions & 8 deletions frontend/__tests__/__harness__/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ import {
import { createSignal, For, JSXElement } from "solid-js";
import { vi } from "vitest";

import { __testing as userTesting } from "../../src/ts/collections/user";
import { blockingDownloadOptions } from "../../src/ts/collections/utils/blocking-download";
import { __testing as configTesting } from "../../src/ts/config/testing";
import { getDefaultSnapshot } from "../../src/ts/constants/default-snapshot";
import { setSnapshot } from "../../src/ts/db";
import {
AppRouter,
createAppRouter,
Expand Down Expand Up @@ -342,9 +342,7 @@ export function createFakeAuth(
function setSignedIn(signedIn: boolean, userData?: UserDataDownload): void {
isSignedIn = signedIn;
// the account route checks the user data downloaded
setSnapshot(signedIn ? getDefaultSnapshot() : undefined, {
dispatchEvent: false,
});
userTesting.setUser(signedIn ? getDefaultSnapshot() : undefined);
auth.stateChanged(userData);
}

Expand Down Expand Up @@ -568,9 +566,9 @@ export function renderApp(options: RenderAppOptions = {}): App {
blockingData.friends.collection = createDownloadCollection("friends");
blockingData.friendsRefresh.set(options.friendsRefresh !== "pending");
// the account route checks the user data downloaded
setSnapshot(options.auth === "signedIn" ? getDefaultSnapshot() : undefined, {
dispatchEvent: false,
});
userTesting.setUser(
options.auth === "signedIn" ? getDefaultSnapshot() : undefined,
);
for (const [name, content] of Object.entries(options.pageContent ?? {})) {
pageContent.set(name, content);
}
Expand Down Expand Up @@ -725,7 +723,7 @@ export function cleanupApp(): void {
setIsTestRestarting(false);
setResultCalculating(false);
configTesting.replaceConfig({});
setSnapshot(undefined, { dispatchEvent: false });
userTesting.setUser(undefined);
for (const name of ["results", "friends"] as const) {
void blockingData[name].collection?.cleanup();
blockingData[name].collection = undefined;
Expand Down
Loading
Loading