Skip to content

Generate TypeScript error-code declarations from the registry - #357

Open
lmars wants to merge 1 commit into
mainfrom
error-code-typescript-gen
Open

Generate TypeScript error-code declarations from the registry#357
lmars wants to merge 1 commit into
mainfrom
error-code-typescript-gen

Conversation

@lmars

@lmars lmars commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

Adds errors/scripts/generate-ts.js, which turns the registry in errors/codes/ into TypeScript error-code declarations for the JavaScript SDKs. Nothing generated is committed here — this PR adds the generator, its tests, and the generate:errorcodes-ts script.

The JS SDKs currently name error codes themselves, and the names have drifted from the registry: 15 of ably-chat-js's 20 disagree with the code's identifier, and ably-js uses bare numeric literals at ~149 call sites (five of which had code and statusCode transposed). Generating the names makes identifier the single spelling every SDK uses, and makes registering a code here the only step needed before referring to it — which is also why identifier is a frozen contract rather than something to churn.

The two output formats

--format=type emits a bare union of numeric literals:

/** A registered Ably error code. */
export type ErrorCode =
  | 10000
  | 40000
  // ...

This is for ably-js, which gates PRs on bundle size. The type erases at compile time, so it costs zero bytes, and existing numeric literals already satisfy the union — so all ~149 call sites keep compiling untouched while gaining the checking. Because registry codes are 5–6 digits and HTTP statuses are 3, a transposed code/statusCode pair is caught exactly rather than heuristically.

--format=const emits one documented constant per code, then the union:

/**
 * Bad request.
 *
 * The request was rejected because it was invalid and could not be processed.
 * @see https://help.ably.io/error/40000
 */
export const BadRequest = 40000;

This is for the wrapper SDKs, which need the values at runtime. Individual consts rather than an aggregate object or a TS enum, because object properties and enum members don't tree-shake — a 260-member enum would land whole in the bundles those SDKs ship to browsers. The JSDoc carries the registry title, summary, and help link, so hovering a constant in an editor shows what the code means.

How consumers use it

Each SDK runs the generator against the ably-common submodule it already vendors, commits the output into its own src/, and adds a CI step that regenerates at the pinned commit and diffs — the same arrangement as ably/docs and its error pages. Two properties that buys: a developer can register a code and use it without publishing anything, and because CI regenerates at the pinned commit, an SDK can't merge a reference to a code that isn't merged here first.

Output is sorted by numeric code and byte-identical for a given registry state, as that drift check requires, and depends only on Node's standard library plus the local frontmatter.js — so it runs from a superproject with no npm install inside the submodule.

Failure behaviour

The generator refuses to emit rather than produce an unusable file, on a duplicate identifier, two identifiers colliding on one PascalCase name, or a name that isn't a valid JavaScript identifier. None of the three occurs in the registry today; the assertions are to keep it that way.

Those failures, a bad argument, an absent errors/codes/, and an unwritable --out all print as a plain message and exit 1. A stack trace there would only be noise, so one now indicates a bug in the generator rather than a problem with the input. The absent-registry message calls out an uninitialised or stale submodule specifically, since ably-js's pin predates errors/codes/ existing and that's the first thing anyone adopting this there will hit.

Verification

  • 25 jest tests: the naming transform, numeric-vs-lexical ordering, byte-identical repeat runs, each of the three assertions, both rendered formats verbatim, one declaration and one union member per registry file, and four CLI-level tests that spawn the script and assert a clean message with no stack frames.
  • Both formats compile under tsc --strict. const d: ErrorCode = 400 (a transposed HTTP status) and an unregistered 12345 are both rejected, which is the property ably-js wants. Not wired into the test suite, since TypeScript isn't a dependency here.
  • All 17 names in the planned rename tables for ably-chat-js and ably-ai-transport-js are produced exactly as tabulated.

The .eslintrc.js change belongs to the tests: they assert through an expectCleanFailure helper, which jest/expect-expect reads as a test with no assertions, so assertFunctionNames is configured for test/** rather than disabling the rule.

Follow-ups, not in this PR

  • Adoption in each SDK: bump the submodule pin, add the npm script, commit the generated file, add the drift check. ably-js needs its pin moved off 496da5e, which predates errors/codes/.
  • The renames in ably-chat-js and ably-ai-transport-js are breaking, since both export ErrorCode as a TS enum from their public API — one major, or @deprecated aliases for a release.
  • Whether to emit an opt-in isErrorCode(n): n is ErrorCode guard for narrowing a relayed wire code. It needs the full set as runtime data, so it can't be part of the type output.
  • errors/scripts/generate-errors-json.js has the stack-trace-on-registry-fault behaviour that this generator now avoids; worth aligning separately.

🤖 Generated with Claude Code

The JS SDKs each name error codes themselves, and the names have drifted
from this registry: 15 of ably-chat-js's 20 disagree with the code's
`identifier`, and ably-js uses bare numeric literals at ~149 call sites.
Generating the names instead makes `identifier` the single spelling every
SDK uses, and makes registering a code here the only step needed before
referring to it.

errors/scripts/generate-ts.js emits one of two shapes. `--format=type`
gives a bare `ErrorCode` union of numeric literals, for a consumer that
wants compile-time checking at no bundle-size cost — the type erases, and
because registry codes are 5-6 digits while HTTP statuses are 3, a `code`
and `statusCode` transposed at a call site fails to compile.

`--format=const` gives one documented `export const` per code plus the
union, for consumers that need the values at runtime; individual consts
rather than an aggregate object or a TS `enum` so that unused codes
tree-shake out of the browser bundles those SDKs ship.

Nothing generated is committed here. Each consuming repository generates
from the submodule it already vendors, commits the output into its own
src/, and regenerates in CI at the pinned commit to fail on a diff. That
mirrors how ably/docs publishes the error pages, and means an SDK cannot
merge a reference to a code that is not merged here first.

The generator refuses to emit rather than produce an unusable file, on a
duplicate `identifier`, two identifiers colliding on one PascalCase name,
or a name that is not a valid JavaScript identifier. None of the three
occurs in the registry today. Those failures, a bad argument, and an
unwritable `--out` all print as a plain message and exit 1; a stack trace
there would only be noise, so one now indicates a bug in the generator
rather than a problem with the input. Output is sorted by numeric code and
byte-identical for a given registry state, as the drift check requires,
and depends only on Node's standard library and the local frontmatter
parser, so it runs from a superproject with no `npm install` inside the
submodule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lmars
lmars force-pushed the error-code-typescript-gen branch from ee284fd to b0ae20c Compare July 31, 2026 15:11
lmars added a commit to ably/ably-ai-transport-js that referenced this pull request Jul 31, 2026
The AIT error codes are now registered, so pin ably-common at the merge of
ably/ably-common#353. That brings in 104009, 104012 and 104013, which the
previous commit's enum referred to before they existed, and renames
104003's identifier to run_lifecycle_event_publish_failed.

protocol/errors.json no longer maps a code straight to a description
string. Entries now sit under a "codes" envelope, each an object with an
identifier, title and summary. validate-error-codes.ts indexed the top
level, so against the bumped pin it would have found nothing and reported
all 19 codes as missing - a misleading failure rather than a clean pass.
It now reads codes and prints each entry's identifier, and reports an
absent envelope as a stale or uninitialised submodule instead of letting
it read as 19 unregistered codes.

The check compares codes, not names. Generating the constants from the
registry (ably/ably-common#357) is what makes the registry identifier the
one spelling every SDK uses, and replaces this script.

[AIT-1259]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant