Skip to content

fix: coded errors survive the shard boundary, plus two codegen losses - #332

Merged
prisis merged 3 commits into
alphafrom
fix/shard-error-identity
Aug 2, 2026
Merged

fix: coded errors survive the shard boundary, plus two codegen losses#332
prisis merged 3 commits into
alphafrom
fix/shard-error-identity

Conversation

@prisis

@prisis prisis commented Aug 2, 2026

Copy link
Copy Markdown
Member

Three defects, all found by running apps against the framework rather than by reading it. Each is silent: the code compiles, the command exits 0, and the damage shows up at runtime or not at all.

1. A thrown LunoraError never reached the client (@lunora/platform-cloudflare)

Every coded error a mutation handler threw arrived as a generic 500 INTERNAL / "Internal error". NOT_FOUND, CONFLICT on a unique index, an UNAUTHENTICATED guard — all identical on the wire, all logged as internal faults, none branchable by the client. Queries were unaffected (they never enter a transaction), which is what made it look arbitrary.

Two boundaries in the mutation path lose it:

  • blockConcurrencyWhile — the single-writer gate. workerd treats a rejecting closure as unrecoverable and aborts the Durable Object, discarding its in-memory state and every hibernating WebSocket subscription attached to it. So one user's "not your turn" error tore the shard down for everyone else connected to it. That default is right for the initialization work the API was designed for; it is wrong for a gate wrapping every mutation, where the transaction inside has already rolled the writes back and the object's state is well defined.
  • storage.transaction — rolls back correctly, but propagates a flattened copy: a plain Error carrying name: message and none of the original's own properties. isLunoraError is structural (type/code/status), so the copy fails the check and gets redacted.

Both now settle the closure's outcome inside the boundary and re-raise the original instance outside it. The gate is still held for the whole closure; the transaction closure still throws into the platform, so rollback is unchanged. A failure raised by the platform itself — with no handler error to restore — is surfaced untouched.

Before / after, same handler, same request:

games:start    {"error":{"code":"INTERNAL","message":"Internal error"}}   [500]
games:start    {"error":{"code":"NOT_FOUND","message":"lobby not found"}} [404]

2. Shorthand table columns were dropped (@lunora/codegen)

defineTable({ status }) lost the column entirely — parseObjectShape skipped anything that was not a PropertyAssignment, and a shorthand property is its own initializer.

It fails quietly and late. With an index over the column you get an index_references_unknown_field advisory naming a column that is plainly there in the schema; without one you get no diagnostic at all, just a runtime insert writing a field the generated types deny. object-shorthand autofixes status: status into this form, so a lint run can introduce it into a schema that was correct yesterday. Table shapes, .input() args, http routes and mutators all read their shape through this parser.

3. Exported handler return types emitted unresolvable names (@lunora/codegen)

A handler annotated with a type exported from its own module emitted that bare name into _generated/api.ts — which never imports the handler's module. The generated file failed to compile (TS2304) while lunora codegen exited 0.

symbolDeclaredUnreachable assumed an exported name is importable. Nothing emits that import: the emitter only rewrites qualifiers the checker itself rendered as import("…"), and the checker prints an exported local type by bare name precisely because it is nameable from the handler. Non-exported declarations were already expanded structurally; exported ones now take the same path.

The existing test asserted the old behaviour on the stated grounds that "the name is valid" — it encoded the defect, so it is rewritten.

Verification

  • New cloudflare-host.transaction.test.ts (7 cases) covers error identity, the abort, rollback ordering, platform-raised failures, non-Error throws, and the composed gate+transaction path. Confirmed failing without the fix.
  • Suites green: platform-cloudflare 31, do 519, shard-engine 967, runtime 833, codegen 1045.
  • api:check green (44 snapshots) — no public surface change.
  • End to end against a real app: two authenticated sessions playing a full game, with every illegal move now rejected as BAD_REQUEST/CONFLICT and carrying its HTTP status.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CYvgKochPxnrCCtKB1tXGf

Summary by CodeRabbit

  • Bug Fixes
    • Improved generated API types by correctly expanding locally declared interfaces and type aliases.
    • Fixed validation for shorthand object properties so these fields are included correctly.
    • Preserved application errors during concurrency-controlled operations and transactions while allowing platform-managed recovery to proceed safely.

prisis and others added 3 commits August 2, 2026 18:55
`defineTable({ status })` — a shorthand property whose value is a validator
held in a const — was dropped from the table shape entirely. `parseObjectShape`
skipped anything that was not a `PropertyAssignment`, and a shorthand property
is its own initializer, so the column vanished from `Doc_*` with no error
anywhere.

The failure is silent and arrives late: an index over the missing column
surfaces only as a confusing `index_references_unknown_field` advisory pointing
at a column the author can plainly see in the schema, and a column with no index
produces no diagnostic at all — just a runtime insert that writes a field the
generated types say does not exist. `object-shorthand` autofixes
`status: status` into this form, so the loss can arrive from a lint run on a
schema that was previously correct.

Every caller of the parser was affected: table shapes, `.input()` args, http
routes and mutators all read their shape through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYvgKochPxnrCCtKB1tXGf
A handler annotated with a type it exports from its own module emitted that
bare name into `_generated/api.ts`, which never imports the handler's module —
so the generated file did not compile (TS2304) while `lunora codegen` exited 0.

`symbolDeclaredUnreachable` treated an exported declaration as reachable, on the
assumption that an exported name can be imported. Nothing emits that import: the
emitter only rewrites qualifiers the type checker itself rendered as
`import("…")`, and the checker prints an exported local type by bare name
precisely because it is nameable from the handler.

A non-exported declaration was already expanded structurally. Exported ones now
take the same path, so the emitted type is identical in shape and resolves from
anywhere. The existing test asserted the old behaviour on the stated grounds
that "the name is valid"; it encoded the defect, and is rewritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYvgKochPxnrCCtKB1tXGf
Every `LunoraError` a mutation handler threw reached the client as a generic
500 `INTERNAL` / "Internal error". A `NOT_FOUND`, a `CONFLICT` on a unique
index, an `UNAUTHENTICATED` guard — all indistinguishable, all logged as
internal faults, and all unbranchable by the client. Queries were unaffected
because they never enter a transaction, which made the behaviour look arbitrary.

Two boundaries in the mutation path lose the error:

1. `blockConcurrencyWhile` (the single-writer gate) treats a rejecting closure
   as unrecoverable and ABORTS the Durable Object. That is the right default for
   the initialization work the API was designed for, and the wrong one for a
   gate wrapping every mutation: an ordinary application error tore down the
   shard, discarding its in-memory state and every hibernating WebSocket
   subscription on it, for every other client connected to that shard.
2. `storage.transaction` rolls back correctly but propagates a flattened copy —
   a plain `Error` carrying `name: message` and none of the original's own
   properties. `isLunoraError` is structural (`type`/`code`/`status`), so the
   copy fails that check and is redacted.

Both now settle the closure's outcome inside the boundary and re-raise the
original instance outside it. Semantics are otherwise unchanged: the gate is
still held for the whole closure, and the transaction closure still throws into
the platform so the rollback happens exactly as before. A failure raised by the
platform itself, with no handler error to restore, is surfaced untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYvgKochPxnrCCtKB1tXGf
@netlify

netlify Bot commented Aug 2, 2026

Copy link
Copy Markdown

Deploy Preview for lunorash ready!

Name Link
🔨 Latest commit 4967f16
🔍 Latest deploy log https://app.netlify.com/projects/lunorash/deploys/6a6f76ba1bed740008dd2bf5
😎 Deploy Preview https://deploy-preview-332--lunorash.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2dce4b1d-345d-4dc2-b963-b7975a59a75b

📥 Commits

Reviewing files that changed from the base of the PR and between a008b25 and 4967f16.

⛔ Files ignored due to path filters (3)
  • packages/codegen/__tests__/discover-functions.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/codegen/__tests__/discover-schema.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
  • packages/platform-cloudflare/__tests__/cloudflare-host.transaction.test.ts is excluded by !**/__tests__/**, !**/*.test.ts and included by packages/**
📒 Files selected for processing (3)
  • packages/codegen/src/discover-functions.ts
  • packages/codegen/src/parse-validator.ts
  • packages/platform-cloudflare/src/cloudflare-host.ts

Walkthrough

The PR updates codegen reachability and shorthand validator parsing. It also changes Cloudflare concurrency and transaction handling so application errors are preserved across platform boundaries.

Changes

Generated API type handling

Layer / File(s) Summary
Generated type and validator parsing
packages/codegen/src/discover-functions.ts, packages/codegen/src/parse-validator.ts
Local interfaces and type aliases are expanded during API type generation. Shorthand object properties now provide their names as validator expressions. Regular assignments still use their initializers.

Cloudflare application error propagation

Layer / File(s) Summary
Concurrency and transaction error handling
packages/platform-cloudflare/src/cloudflare-host.ts
runSerialized captures closure errors inside blockConcurrencyWhile and rethrows them afterward. transaction captures closure errors, allows rollback, and restores the original error. Platform errors remain unchanged.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant runSerialized
  participant blockConcurrencyWhile
  participant transaction
  Application->>runSerialized: Invoke application closure
  runSerialized->>blockConcurrencyWhile: Execute boxed closure
  blockConcurrencyWhile-->>runSerialized: Return boxed result or error
  runSerialized-->>Application: Return result or rethrow original error
  Application->>transaction: Invoke transactional closure
  transaction-->>Application: Rethrow closure error after rollback
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the preserved coded errors and the two codegen fixes.
Description check ✅ Passed The description clearly explains all changes and includes detailed verification results, but it omits several template sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/shard-error-identity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Thank you for following the naming conventions! 🙏

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Thank you for confirming the Contributor License Agreement! 🙏

@codspeed-hq

codspeed-hq Bot commented Aug 2, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 23.05%

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 143 untouched benchmarks
⏩ 117 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
broadcastWhisper to 128 members 254 µs 588.3 µs -56.83%
1 shard × 1000 rows (single round-trip) 2.7 ms 3.1 ms -11.69%
in-batch: single IN(...) query + id->doc re-projection 949.3 µs 794.2 µs +19.53%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/shard-error-identity (4967f16) with alpha (a008b25)

Open in CodSpeed

Footnotes

  1. 117 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@prisis
prisis merged commit a4503b5 into alpha Aug 2, 2026
44 of 45 checks passed
@prisis
prisis deleted the fix/shard-error-identity branch August 2, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant