Files
clockwork-labs-bot 84cfe5a920 Fix TypeScript table handle casing (#5286)
## Compatibility note

This PR changes the generated TypeScript table/view handles for
snake_case module accessors to the target-language TypeScript accessor
casing, but keeps the old snake_case handles as deprecated aliases.

Generated TS clients now expose the intended TypeScript accessor casing:

- `ctx.db.databaseTree`
- `tables.loggedOutPlayer`
- `tables.myProgress`

The old snake_case handles continue to work and are marked deprecated in
the generated type surface:

- `ctx.db.database_tree` still works as an alias for
`ctx.db.databaseTree`
- `tables.logged_out_player` still works as an alias for
`tables.loggedOutPlayer`
- `tables.my_progress` still works as an alias for `tables.myProgress`

The database canonical names stay unchanged. For example, the generated
TypeScript handle is camelCase, but the table metadata still has `name:
'logged_out_player'`.

## Remaining API breakage risk

This should be non-breaking for normal generated TypeScript client
usage, including:

- direct table access through `conn.db.snake_case`
- callback contexts like `ctx.db.snake_case`
- exported query builders like `tables.snake_case`
- type inference for deprecated aliases

There are still a few edge cases where users may notice an API shape
change:

- Code that enumerates generated table handles with `Object.keys`,
`Object.entries`, `for...in`, or similar will now see both the
target-language handle and the deprecated snake_case alias.
- Code with pathological table/view accessor collisions may not get
every possible alias. The normal case is `logged_out_player` ->
generated handle `loggedOutPlayer` plus deprecated alias
`logged_out_player`. Pathological cases are shapes like:
- `foo_bar` and `fooBar`: both want the generated TypeScript handle
`fooBar`, so generated clients cannot provide two distinct
`tables.fooBar` entries.
- `foo_bar` and some other table/view whose target-language handle is
already `foo_bar`: the deprecated `foo_bar` alias for `foo_bar` ->
`fooBar` would shadow the other generated handle, so the generator skips
that alias.
- Code that compares the exact generated `index.ts` text, emitted
declaration text, or public type names will see new helper/types such as
`DbView`, `Tables`, and the alias metadata.

TypeScript modules themselves are not expected to break from this unless
they consume generated TypeScript client bindings or depend on exact
generated client object keys.

## Terminology

The casing proposal uses **canonical name** for the database/internal
name. That is language-independent. It uses **accessor name** for the
source/module/client-facing identifier that codegen derives
language-specific handles from.

This PR keeps database canonical names unchanged. It changes the
generated TypeScript accessor handles to match TypeScript casing while
retaining the old generated handles as deprecated aliases.

## Why

The TypeScript code generator was using `table.accessor_name` and
`view.accessor_name` directly as object keys in `tablesSchema`. That
preserved the raw module accessor spelling instead of applying the
target-language `Case::Camel` conversion for TypeScript handles. Per the
casing policy, client codegen should use the server accessor name as its
source and apply the target-language conversion.

## What changed

- Convert generated TypeScript table and view handle keys with
`Case::Camel`.
- Generate deprecated snake_case aliases for table/view handles when the
old generated handle differs from the target-language TypeScript handle.
- Keep runtime table metadata and database canonical names unchanged.
- Avoid duplicate runtime table definitions.
- Add a type-only aliased schema so callback contexts infer deprecated
aliases too.
- Add regression coverage for TypeScript-cased handles and deprecated
aliases.
- Update checked-in generated TS bindings and references that change
under this fix.

Generated code in this repo that changes as a result:

- `crates/bindings-typescript/test-app/src/module_bindings/index.ts`
-
`crates/bindings-typescript/test-react-router-app/src/module_bindings/index.ts`
-
`crates/bindings-typescript/test-solid-router/src/module_bindings/index.ts`
-
`crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts`
- `demo/Blackholio/client-ts/src/module_bindings/index.ts`
- `templates/hangman-react-ts/src/module_bindings/index.ts`
- `templates/money-exchange-react-ts/src/module_bindings/index.ts`
- `crates/codegen/tests/snapshots/codegen__codegen_typescript.snap`

I also updated the corresponding client/test references to the generated
TypeScript-cased handles.

## Verification

- `cargo fmt --all --check`
- `cargo test -p spacetimedb-codegen typescript`
- `pnpm --dir crates/bindings-typescript test --
tests/client_query.test.ts tests/db_connection.test.ts`
- `pnpm --dir crates/bindings-typescript exec vitest run
--typecheck.enabled tests/client_query.test.ts
tests/db_connection.test.ts`
- `git diff --check`

The explicit Vitest typecheck command still reports existing global
typecheck errors from unrelated files loaded by the test project,
including missing `spacetimesys@2.x` ambient modules and existing test
type issues, but the touched alias tests report `Type Errors no errors`
and the earlier generated-code `declare override` issue is gone.

---------

Signed-off-by: Ryan <r.ekhoff@clockworklabs.io>
Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
Co-authored-by: Ryan <r.ekhoff@clockworklabs.io>
2026-06-26 13:01:35 +00:00
..
2026-06-02 01:16:24 +00:00
2026-06-02 01:16:24 +00:00
2026-06-02 01:16:24 +00:00

Get a competitive SpacetimeDB Hangman game running with React and TypeScript.

Prerequisites

Install the SpacetimeDB CLI before continuing.


Create your project

Run the spacetime dev command to create a new project with a TypeScript SpacetimeDB module and React client.

This will start the local SpacetimeDB server, publish your module, generate TypeScript bindings, and start the React development server.

spacetime dev --template hangman-react-ts

Open your app

Navigate to http://localhost:5173 to play Hangman.

Every player guesses the same hidden word on a private board. A round lasts 60 seconds, followed by 10 seconds of revealed results and rankings.

Explore the project structure

Your project contains both server and client code.

Edit spacetimedb/src/index.ts to change game rules or the word list. Edit src/App.tsx to change the game interface.

my-spacetime-app/
├── spacetimedb/          # Your SpacetimeDB module
│   └── src/
│       └── index.ts      # Game tables, reducers, and round transitions
├── src/                  # React frontend
│   ├── App.tsx
│   └── module_bindings/  # Auto-generated types
└── package.json

Understand tables and reducers

Open spacetimedb/src/index.ts to see the module code. The template includes public round and result tables, private player progress, and scheduled round transitions. The set_name reducer registers a nickname and guess_letter submits one letter for the current round.

Tables store game state. Reducers are functions that modify data - they are the only way to write to the database.

export const set_name = spacetimedb.reducer(
  { name: t.string() },
  (ctx, { name }) => {
    const trimmedName = name.trim();
    if (trimmedName.length === 0 || trimmedName.length > 20) {
      throw new SenderError('Names must be between 1 and 20 characters');
    }

    const existing = ctx.db.player.identity.find(ctx.sender);
    if (existing) {
      ctx.db.player.identity.update({ ...existing, name: trimmedName });
    } else {
      ctx.db.player.insert({ identity: ctx.sender, name: trimmedName });
    }
  }
);

export const guess_letter = spacetimedb.reducer(
  { letter: t.string() },
  (ctx, { letter }) => {
    const guess = letter.trim().toUpperCase();
    if (!/^[A-Z]$/.test(guess)) {
      throw new SenderError('Guess one letter from A to Z');
    }

    // Update this player's private progress for the active round.
  }
);

Test with the CLI

Open a new terminal and navigate to your project directory. Then use the SpacetimeDB CLI to join a round, guess letters, and inspect your state.

cd my-spacetime-app

# Pick a nickname before guessing
spacetime call set_name '"Ada"'

# Guess one letter
spacetime call guess_letter '"A"'

# Inspect the active public round and your private board view
spacetime sql "SELECT * FROM current_round"
spacetime sql "SELECT * FROM my_progress"

Understand round state and privacy

The module runs one shared competitive round at a time:

  • current_round publishes the timer, difficulty, and word length. It reveals the answer only during results.
  • my_progress exposes each player only to their own masked word and guesses during an active round.
  • round_result publishes the completed round standings once the timer ends.
  • transition_timer schedules the results phase and the start of the next round.

Customize the game

The built-in word list and round durations are at the top of spacetimedb/src/index.ts. Add words, adjust difficulty labels, or change the active and results durations there.

The React UI in src/App.tsx includes the gallows drawing, masked-word board, keyboard, timer, nickname form, and standings panel. Edit those components and src/App.css to change how the game looks and plays.

Next steps