Skip to content

XMachines 6.0.0: @xmachines/core, and a plain release

XMachines 6.0.0 adds @xmachines/core, the base of every other package. The release protocol moves into it, and it becomes opt-in: a subscription hands back a plain release, and asDisposable makes that release a Disposable. The release also moves the renderers to json-render 0.21.

The new base package: @xmachines/core

@xmachines/core holds code that more than one package uses, and that belongs to no layer of the Play Architecture. It has no root entry. It has three subpaths:

@xmachines/core is a peer of @xmachines/play, play-pattern, play-url, play-router, play-view, play-xstate, play-dom, play-react and play-tanstack-router. Install it with them:

Terminal window
pnpm add @xmachines/core

Caution: install one copy only. err instanceof BaseError answers false for an error of a second copy.

Every typed error extends BaseError

PlayError now extends BaseError, so err instanceof BaseError matches every typed error of the packages. Each error has two stable fields: scope, the class or module that threw it, and code, a machine-readable identifier.

import { BaseError } from "@xmachines/core/errors";
try {
start();
} catch (err) {
if (err instanceof BaseError) {
console.error(`[${err.scope}:${err.code}] ${err.message}`);
} else {
throw err;
}
}

Branch on the class or on code, and never on message.

The release protocol becomes opt-in

From 3.0.0 to 5.0.0, a subscription handed back a Cleanup: a function that was also a Disposable. Each package had to build that value, and each test double had to write the dispose key.

In 6.0.0 these values are plain again. They hand back a release function, or an object with a release method, and no dispose key:

A caller that wants a using scope publishes the release with asDisposable of @xmachines/core/utils. A caller that does not want one pays nothing.

import { asDisposable } from "@xmachines/core/utils";
// A release function: the function is its own release.
{
using stop = asDisposable(watchAtom(count, render));
render(count.get());
}
// An object: name its release.
{
const plain = createBrowserHistory({ window });
using history = asDisposable(plain, () => plain.destroy());
history.push("/first");
} // destroy() runs here
// An actor: publish its stop().
{
const actor = definePlayer({ machine })();
using _stop = asDisposable(() => {
actor.stop();
});
actor.start();
} // stop() runs here, and after an exception too

asDisposable(target, release) throws DisposeConflictError when the target already has a dispose member that is not the release. A second call with the same release changes nothing.

@xmachines/play no longer exports Cleanup, asCleanup, DISPOSE or DisposeKey, and no other package re-exports the protocol. Import asDisposable, DISPOSE and DisposeKey from @xmachines/core/utils. A class that implements the protocol itself writes its release under DISPOSE, and not under a bare Symbol.dispose. A bundler that downlevels using reads the same key pair that DISPOSE names.

The cache moves to @xmachines/core/cache

PatternCache and createPatternCache are removed from @xmachines/play-pattern, @xmachines/play-url and @xmachines/play-router. Use KeyValueCache and createLruCache. The three packages re-export them.

// 5.0
new RouteMap(mappings, { pathCache: createPatternCache(5000) });
// 6.0
new RouteMap(mappings, { pathCache: createLruCache(5000) });

Helpers in @xmachines/core/utils

shallowEqualExcept moves from @xmachines/play to @xmachines/core/utils. The subpath also holds new helpers that the packages share:

  • releaseQuietly(release, onFailure) runs one release, and gives a throw to onFailure. A teardown of several releases then runs each one.
  • rethrowLater(error) throws an error again from a microtask of its own. A host escalates a failure this way from the onError handler of a renderer.
  • isError(value) tells if a value is an Error of any realm, also from an iframe or from node:vm.
  • safeString(value) and describeValue(value) put an unknown value in a message, and neither one throws.
  • isThenable(value), createNullRecord() and createWarnOnce().

json-render 0.21

The renderers and @xmachines/play-view need the @xmachines/json-render-* packages ^0.21.0-xm.1 as peers. Install json-render 0.21 with them.

Upgrade

  1. Install @xmachines/core, one copy.
  2. Replace Cleanup and asCleanup with asDisposable of @xmachines/core/utils. Wrap each value that a using declaration holds.
  3. Import DISPOSE, DisposeKey and shallowEqualExcept from @xmachines/core/utils.
  4. Replace createPatternCache with createLruCache, and PatternCache with KeyValueCache.
  5. Move the @xmachines/json-render-* packages to ^0.21.0-xm.1.

The previous release is 5.0.0. The getting started guide of 6.0 lists the packages to install.

The changelog of 6.0.0 lists every change of the release.