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.
| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | @ultimat3/realtime has two entries | you import a server name — NATS, pg replication, the sync node, the channel hub |
| 2 | IdempotencyStore.settle / fail take a reservation id | you call either, or implement the interface |
| 3 | pwa.installPrompt, auth.afterSignInPath, ai.modelEnv deleted | your app.config.ts sets one |
| 4 | @ultimat3/manifest drops canonical | you imported it |
| 5 | @ultimat3/render drops matchRoute / RouteMatch | you imported either |
| 6 | SQL_CANCEL projects its columns | you 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.