Skip to content

feat: rewrite shinycannon in TypeScript#1

Merged
gadenbuie merged 31 commits intomainfrom
rewrite-in-typescript
Mar 18, 2026
Merged

feat: rewrite shinycannon in TypeScript#1
gadenbuie merged 31 commits intomainfrom
rewrite-in-typescript

Conversation

@gadenbuie
Copy link
Collaborator

Summary

Complete rewrite of shinycannon from Kotlin/JVM to TypeScript/Node.js,
republished as @posit-dev/shinyloadtest. This is a 1:1 behavioral
rewrite — same recording format, same CSV output format (compatible with
the shinyloadtest R package).

CLI change: The primary CLI is now shinyloadtest replay (with a
shinycannon alias for backwards compatibility).

image

Key changes:

  • Runtime: Kotlin/JVM → Node.js 20+ with TypeScript strict mode
  • Distribution: Maven/Docker/fpm → npm install @posit-dev/shinyloadtest / npx @posit-dev/shinyloadtest
  • Dependencies: 3 runtime deps (ws, tough-cookie, commander)
  • Build: tsup (esbuild) producing ESM bundle
  • Tests: 176 tests across 15 test files (vitest) covering unit, integration, and behavioral constraints

Architecture: 13 focused modules (types, recording, tokens, sockjs,
url, http, websocket, detect, auth, logger, output, session, worker, cli,
main, ui) with discriminated unions for event types, per-session cookie
jars, and async queue-based WebSocket message buffering.

Behavioral parity: All 12 behavioral constraints from the original
Kotlin implementation are preserved and tested (recording validation,
status code handling, WS queue bounds, key matching, output format, etc.).

Verification

npm install
npx @posit-dev/shinyloadtest replay --help
npx @posit-dev/shinyloadtest replay recording.log https://your-app.example.com --workers 5 --loaded-duration-minutes 2

All CI checks (lint, typecheck, build, test) pass.


Originally opened as rstudio#76.

Add a beautiful interactive terminal experience using yoctocolors and ora:
- Startup banner showing target URL, workers, duration, and output dir
- Warmup spinner with worker readiness counter
- Live loaded-phase display with progress bar, countdown timer, and
  running/done/failed stats (updates every second)
- Shutdown spinner and final summary
- Colored CLI help text: per-argument colors (green for recording,
  magenta for app-url), cyan option flags, bold section headings,
  yellow environment variable names
- TTY-only: falls back to existing logger-based output when piped
- Fix replaceTokens to only replace known allowed tokens instead of
  scanning for all ${UPPERCASE} patterns, preventing false positives
  from JS template literals in minified widget code (e.g. plotly ESM)
- Recognize Python Shiny's empty message format in canIgnore
  ({"values":{}} with objects, not just {"values":[]} with arrays)
- Add comm_id mapping for Jupyter widget playback: capture recorded
  vs actual comm_id from shinywidgets_comm_open messages during
  WS_RECV, substitute in outgoing WS_SEND messages

Ported from PR rstudio#74 (Kotlin) to the TypeScript rewrite.
Split single sequential CI job into parallel jobs for lint, typecheck,
test (Node 20 + 22), and build. Add concurrency controls, least-privilege
permissions, and a gate job for branch protection.
The rstudio#74 port's isEmpty() helper treated both empty arrays and empty
objects as "empty", which caused R Shiny's legitimate response
{"errors":{},"values":{},"inputMessages":[]} to be filtered out.
This starved WS_RECV events, causing 30-second timeouts.

Reverts to the Kotlin original's behavior: only ignore when all three
fields (errors, values, inputMessages) are empty arrays.
The TypeScript rewrite has passed the Kotlin parity audit and all
171 tests. The original Kotlin source is preserved in git history
on the main branch.
Restructure the CLI to use Commander.js subcommands, moving all
existing arguments and options under `shinycannon loadtest`. This
makes room for future subcommands like `record` and `report`.

- `shinycannon` with no args shows root help listing subcommands
- `shinycannon loadtest` with no args shows loadtest-specific help
- `shinycannon loadtest <recording> [app-url] [options]` runs the test
Thread AbortSignal from worker orchestration through session playback
to enable near-instant shutdown. Cancellable sleep, fetch, and
WebSocket receive operations replace the previous design where workers
could only observe shutdown between complete session replays.
Use response.url (the final URL after redirects) when persisting
Set-Cookie headers to the cookie jar, so cookies are associated with
the correct domain when redirects cross origins.
- Rename package from `shinycannon` to `@posit-dev/shinyloadtest` (v2.0.0-alpha.1)
- Rename `loadtest` subcommand to `replay`
- Rename env vars to `SHINYLOADTEST_*` with `SHINYCANNON_*` fallback
- Add `shinycannon` as legacy bin alias in main package
- Add `packages/shinycannon/` stub package for `npx shinycannon` support
- Rename output version file to `shinyloadtest-version.txt`
- Update all tests, docs, and branding references
Show total events completed and events/second rate during the loaded
phase. When running with multiple workers, also display per-worker
averages on a separate line.
- Wrap canIgnore() in try/catch in the WebSocket message handler so
  malformed SockJS frames route through triggerFailure() instead of
  crashing the process with an uncaught exception
- Add HTTP 401 to isProtected() check alongside 403/404; note that
  302 redirects are auto-followed by fetch so they never reach this check
- Add regression tests for both paths
@gadenbuie gadenbuie force-pushed the rewrite-in-typescript branch from ab7a2c4 to b9f2c32 Compare March 17, 2026 22:03
@gadenbuie gadenbuie merged commit 447057e into main Mar 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant