7.7 KiB
typy
One word at a time. Type it, find the next one, settle into a rhythm.
Live: https://typy.yolopreneur.lol/
Brief: time#254
Play
- 30 seconds, 60 seconds, 50 words, endless, and tempo.
- The first typed character starts the run. Backspace fixes mistakes. Space advances; settings can advance on the last correct letter instead. Automatic advancement tolerates one habitual Space immediately afterward without adding an error or extra output.
- Word changes are immediate and stationary. Letter feedback reuses the rendered glyphs; word fitting runs only when the word or viewport changes. Idle stages do no repeated DOM work, and the optional next-word preview sits close to the current target.
- Tempo waits for each word's beat, accelerates by 3.5% on success, and ends after three missed beats. Its starting pace comes from the device's last measured average.
- Common words are weighted by frequency. The words menu also accepts a topic or a pasted passage, capped at 300 words.
- Topic paths such as /react-hooks generate a short Venice passage and cache it for 30 days.
- /daily uses one deterministic passage per UTC day and locks the run to 60 seconds. Sharing a completed daily run adds an optional name to the daily board.
- Finished runs show WPM, accuracy, clean-word streaks, slower words, and a speed sparkline. Replay follows original key timing and supports scrubbing.
- Choose Share to publish a replay at /r/:id. A result includes a real PNG social preview. /race/:id uses the original passage, mode, advancement setting, and timing as a ghost.
Development
Node 22 or newer. The app and Worker have no runtime package dependencies.
cd typy
npm start
The local server runs at http://127.0.0.1:8787 with ephemeral memory storage and the real quota class. Restarting it clears local results. Set PORT to choose another port. Set VENICE_API_KEY to exercise real topic generation locally; otherwise that route returns a clear unavailable response. Common words, pasted text, the daily, sharing, replay, and score images still work locally.
npm test
The test files can also be run directly for individual test output:
node test/game.test.js
node test/backend.test.js
Optional browser acceptance uses Playwright and a Chromium executable:
CHROME=/path/to/chrome node test/browser-smoke.mjs
CHROME=/path/to/chrome node test/integration.mjs
CHROME=/path/to/chrome node test/responsiveness.mjs
The integration script uses the actual local backend, creates a test result, reloads its public link, checks the preview PNG, replays it, and opens the ghost race. Set TYPY_BASE_URL to test a deployment; it creates one clearly named test replay and does not submit a synthetic daily-board score.
The responsiveness script keeps normal motion enabled and checks immediate readable word transitions, idle rendering, fast spaced input in both advancement modes, and phone long-word layout. Engine tests also verify exact 100/160 WPM input streams in 30/60-second runs without changing the scoring formula.
The build bundles the HTML and static source into Worker ES modules. Commit both the authored files and generated src/page.js and src/assets.js. Deployment refuses mismatched generated files.
Deployment
The source lives under typy/ on the time repository's main branch, following the existing quc Worker pattern. This app needs its own backend, so it is served directly by the Worker rather than the static venture folder.
Deploy with the existing operator credentials: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, and VENICE_API_KEY. Their approved locations are documented in the repository's .github/time/SECRETS.md. No credential belongs in this directory or in frontend code.
# From the repository root:
npm run build --prefix typy
npm test --prefix typy
# Commit and merge the tested source, then deploy from that clean checkout:
python3 typy/deploy.py
python3 typy/deploy.py --health
The deploy script reuses .github/time/cloudflare.py, uploads the committed ES modules, reuses a KV namespace named time-typy, and attaches typy.yolopreneur.lol. Its first upload creates the SQLite Durable Object class QuotaGate with migration typy-v1. Later uploads retain the same store and quota namespace. The operator machine needs Python and git; committed generated assets make Node unnecessary there.
The Worker returns its code revision through /api/health. The deployment receipt also records the exact source commit. Verify the custom domain and asset responses separately from the upload.
Storage, privacy, and limits
- Unshared runs and pasted passages remain in the browser. Best scores, settings, and the optional display name use local storage.
- A share contains the chosen passage, relative key timing and outcome metadata, computed score, mode, and optional name. Actual mistyped characters and input buffers are never serialized or accepted into stored results.
- Scores are recomputed on the server from replay outcomes; daily submissions must match the current canonical passage and full 60-second run. This is an informal game leaderboard, not a proof of human typing.
- KV stores one immutable result per ID, one topic per canonical slug, and separate daily-board entries. Concurrent scores do not overwrite a shared leaderboard object. A new link or board entry can take a short time to replicate between locations.
- The quota object reserves new-generation and save limits atomically. Daily defaults are eight uncached topics per visitor, 80 total, 30 saves per visitor, and 2,000 total. It also spaces new topic requests by eight seconds and saves by one second.
- Raw IP addresses are used only inside the quota object's request to derive an HMAC scoped to the UTC day. They are never persisted. Old daily hashes and counters are purged. No analytics, account, or persistent visitor cookie is added.
- Topic input and model output are screened for a general audience, normalized to plain text, and bounded. The provider uses mistral-small-3-2-24b-instruct, configurable through TOPIC_MODEL.
- Endless play can continue indefinitely. Replay memory is bounded to 30,000 events and 6,000 word timings. If a run exceeds that history, the local full score remains available and the incomplete replay cannot be published. Uploads are also capped at 1.5 MB.
Scoring
WPM is net correct output characters divided by five and by elapsed minutes. Completed words include one normalized separator in either advancement mode. Deleting and retyping characters cannot inflate net output. Accuracy is correct character attempts divided by all character attempts, including mistakes and excluding navigation or correction keys.
Timed runs stop at exactly their deadline, including when a background tab resumes late. Tempo misses are measured against the original beat deadlines. The speed chart omits a zero-duration first word because no measurable typing interval exists for it.
API
| Route | Purpose |
|---|---|
| GET /api/health | Runtime bindings and revision |
| GET /api/passage?topic=react-hooks | Cached or generated topic |
| GET /api/daily | Shared UTC-day passage |
| GET /api/board?day=YYYY-MM-DD | Daily top 50 |
| POST /api/results | Validate and publish a completed replay |
| GET /api/results/:id | Read a saved replay |
| GET /api/og/:id.png | 1200 × 630 PNG score preview |
Acceptance evidence
evidence/review.md records the independent desktop/phone review and the corrected keyboard and long-word regressions. The integration receipts cover actual API persistence, public replay, ghost routing, shared daily text, and PNG rendering. Tests and review receipts distinguish mocked frontend cases from real backend checks.