Ultimate

7.x → 8.0.0, entry by entry

Six breaking entries, from one whole-repo bug sweep. Five are compile errors the moment you upgrade. The sixth is a silent behaviour change, and it is the one to read even if nothing else here applies to you. No codemod.

#SurfaceCosts you an edit if
1@ultimat3/realtime has two entriesyou import a server name — NATS, pg replication, the sync node, the channel hub
2IdempotencyStore.settle / fail take a reservation idyou call either, or implement the interface
3pwa.installPrompt, auth.afterSignInPath, ai.modelEnv deletedyour app.config.ts sets one
4@ultimat3/manifest drops canonicalyou imported it
5@ultimat3/render drops matchRoute / RouteMatchyou imported either
6SQL_CANCEL projects its columnsyou asserted on that constant's text

1. @ultimat3/realtime splits into . and ./server

- import { ChannelHub, createSyncNode, LiveQueryRegistry } from '@ultimat3/realtime';
+ import { ChannelHub, createSyncNode, LiveQueryRegistry } from '@ultimat3/realtime/server';

Client names — useLive, liveHookFor, LiveClient, the offline queue, rebase, the wire protocol, cursors — are unchanged on .. A file importing both halves now writes both imports.

Why: the single barrel carried useLive beside openNatsClient, so bun build --target=browser on an entry importing only the hook failed with "Browser build cannot require() Node.js builtin: stream/web", out of nats. The island this framework tells you to write could not be bundled.

The two barrels are disjoint — ./server re-exports no client name — so which half a symbol lives in is checkable rather than conventional. If an import stops resolving, the name moved to ./server; nothing was deleted.

2. IdempotencyStore.settle and fail take the reservation id

- await store.settle(key, value);
+ await store.settle(key, value, reservation.record.id);

reservation is what store.reserve(key, hash) answered. Same shape for fail.

Read this if you implement the interface — it is the one silent entry in this major. A store with the old two-parameter method still compiles, because a shorter function is assignable to a longer signature, and it silently loses the fence. Both statements now match on id and state, so a straggler from a slow first attempt can no longer overwrite a replacement reservation still in flight. The fail half was the worse one: a straggler's failure marked a live replacement failed, and the replacement's own settle was then fenced out.

3. Three config fields are deleted

- pwa: { enabled: true, offline: 'runtime', installPrompt: true },
+ pwa: { enabled: true, offline: 'runtime' },
- auth: { signInPath: '/signin', afterSignInPath: '/dashboard' },
+ auth: { signInPath: '/signin' },
- ai: { mcp: { expose: true, path: '/mcp' }, modelEnv: 'ANTHROPIC_MODEL' },
+ ai: { mcp: { expose: true, path: '/mcp' } },

There is no replacement key, because there was never a behaviour. Each was declared, defaulted, merged, and read by nothing. Use createInstallController from @ultimat3/pwa, send the visitor from your own sign-in route, and pass model on the llm() request.

ai.modelEnv's own doc comment argued for its deletion: "an intention, not a behaviour… nothing consumes the merged value… So the exact thing this key exists to prevent — a model string baked into the image — is what actually happens."

Same precedent as JobsConfig.driver in 5.0.0 and realtime.heartbeatMs in 4.0.0. All three fail at typecheck only — and an app that builds its config into a variable before passing it loses excess-property checking and sees no error at all. scripts/config-readers.ts now keeps the class out.

4. @ultimat3/manifest no longer exports canonical

Use canonicalJson from @ultimat3/core. It was the third of five copies of one serialiser; manifest's fed buildId and the contract-diff equality, so a -0/NaN/Date fold could make a breaking API change diff as "no change" and ship silently.

5. @ultimat3/render no longer exports matchRoute or RouteMatch

Two exported route matchers existed with different precedence. @ultimat3/http's trie is the live one; render's had zero consumers repo-wide.

6. SQL_CANCEL projects its columns instead of returning *

It fed toJobRecord, which does Number(row.run_at) — so against a text-decoding PgExecutor every timestamp came back NaN. Only an edit if you asserted on the constant's SQL text.

Edit this page on GitHubThis page as markdown