Ultimate

4.1.0 → 5.0.0, entry by entry

Two breaking entries over six surfaces, one of which needs an edit. There is no codemod, and there does not need to be: the whole migration is deleting one line, and only if you wrote it.

Start here — the one edit

  jobs: {
-   driver: 'postgres',
    queues: ['app-default'],
    concurrency: 8,
  },

jobs.driver accepted 'postgres' | 'redis' | 'nats' and had no reader anywhere. Boot always built createPgDriver, so setting it to redis did not throw, did not warn and did not boot Redis — it changed nothing and you silently got Postgres. If you were relying on it doing something, it was not: you were on Postgres the whole time.

Which driver runs is setJobDriver(driver), and only that:

setJobDriver(createPgDriver({ executor }))   // production
setJobDriver(createMemoryDriver())           // a test

JobsDriver (the type) goes with it. JobsConfig.driver was its only use.

Leaving the line in also works. A spread carries a key no type names, so a stale app.config.ts still boots and the field still does nothing — packages/core/src/config.test.ts pins exactly that. TypeScript will flag it; the runtime will not.

The other four need no edit unless you wrote a test driver

They are @ultimat3/testing's subscribe fixture, which was declared and had no driver — so nothing could have been implementing these types. They changed because they described an API that could not work: LiveTarget was { name, queryHash }, and a node keys a subscription by (name, input); a hash is the input already thrown away.

WasIs
Subscribe = (target) => Promise<LiveFeed>(target, input, actor?) => Promise<LiveFeed>
LiveTarget = { name, queryHash }{ name } — the query itself
LiveFeed had no reconnect()it has one
DRIVER_FIXTURE_NAMES held subscribeFRAMEWORK_FIXTURE_NAMES does; the framework builds it

A test that destructured subscribe and called it now reads:

-const feed = await subscribe(liveFeed.as(actorFor(ada), { orgId: acme.id }));
+const feed = await subscribe(liveFeed, { orgId: acme.id }, actorFor(ada));

The actor is the third argument rather than baked into the target because that is where the framework puts it: the shared window is built with no subject, and every decision about an actor is per subscriber.

Behaviour that changed without breaking a type

Error fields are escaped where they are built. UltimateError and SchemaError run singleLine over code, title, cause, fix and docs in their constructors, so .message, .cause, format(), toJSON() and any renderer you write are one line by construction. Measured over every shipped cause:/fix: literal: none contains a newline, so no framework message changed. If you build error text from a value a CALLER controls, you no longer have to remember — and if you were already escaping, singleLine is idempotent, so nothing doubles.

One fix: line changed text. X_REPLICATION_FAILED on SQLSTATE 42704 said x db replication init, which is not a command — x db takes gen, migrate, reset, seed, studio, branch, backfill. It now names the CREATE PUBLICATION an operator can paste.

One thing to know before you subscribe to a projected live query

Not a change in this release — a defect it made visible. If a query({ live: true }) declares an orderBy on a column its rows do not carry (a projection that omits it), every change to a row reads as a move, and the re-delivered row is the raw entity row rather than the projection. Columns you left out of the projection reach the subscriber. #230, and Known gaps carries it. Until it is fixed, order a live query by a column its rows carry.

Edit this page on GitHubThis page as markdown