Upgrade @arki/dot 0.6.1 to 0.7.0

For @arki/[email protected] · source docs/upgrades/0.6.1-to-0.7.0.md

Everyone who calls .configure(), .boot(), .start() or .stop(), writes plugin() or feature() units, or reads dot CLI output must act; apps that only call defineApp(...).start() and never touch these can skip the plugin steps but still need the first and last steps.

What changed

This is a breaking minor. @arki/dot is below 1.0.

Added, nothing to do:

  • defineFeature(name) chain builder in @arki/dot/feature, with routerFor, feature.instance(name, rebind?) and nested .features().
  • @arki/dot/orpc: needsMiddleware, rpc, errorCaptureMiddleware.
  • @arki/dot/runtime: createAppRuntime, one started app per server process, safe under Vite hot updates.
  • dot dev command. It checks the database, applies migrations and runs Vite.
  • Every usecase from defineFeature runs in an OpenTelemetry span named feature.<feature>.<usecase>.
  • $signal (an AbortSignal) in every hook and provides factory context. It aborts when the hook passes hookTimeoutMs.
  • $describeAction hook. dot explain prints action summaries and contract meta.
  • Failures at compile time: wiring mistakes now fail on the argument, with a DOT: label.
  • Dot now runs on Node, Bun, Deno, browsers and Hermes.

Breaking, see the steps:

  • configure(), boot(), start(), stop() and dispose() return failures instead of throwing.
  • plugin() and feature() publish through provides factories. boot returns nothing.
  • plugin({ version }) is gone.
  • Kernel error codes are renamed to ARKI_DOT_E001 to ARKI_DOT_E032.
  • defineFeature changes: .needs() takes aliases only, .usecases() takes an object of factories, rpc() takes three arguments.
  • testApp<TServices>() lost its type argument.
  • Adapter authors: driver() and driven() receive MountedFeature[].
  • The dot CLI uses the arki.cli/1 JSON envelope, new exit codes and dot.* failure codes.

Public API changes

Removed:

  • DotCliError, DotCliErrorCode, dotCliDocsUrl, DotCliEnvelope, DotCliEnvelopeStatus, DotNewEnvelope, main, parseArgs from @arki/dot/cli. Use program. runExplain, runDoctor and runNew now return data or a failure and print nothing.
  • InferredProvides.
  • plugin({ version }) and PluginManifest.version.
  • The --openapi flag. Use --as openapi.
  • The single-factory .usecases(create) form.
  • The explicit testApp<TServices>() type argument.
  • Error code strings DOT_LIFECYCLE_E0nn and FEATURE_E0nn.

Added: defineFeature, routerFor, handleOf, AnyHandle, Handle, LazyHandle, Handles, Provided, ProvidesFactories, providesOf, servicesOf, resolveNeeds, entrypointsOf, fake, createAppRuntime, needsMiddleware, errorCaptureMiddleware, rpcFailureDefinitions, runDev and its event and failure types, UsecaseFactories, ProcedureFactories, ListGuard.

Behaviour changes: see the steps below.

Steps

  1. Rebuild @arki/dot and every package that emits its types. Then run your typecheck. The compiler will list most breaks below.

  2. Check failures from the app phases. They no longer throw.

    Before:

    const manifest = defineApp('shop').use(catalog).configure().manifest;
    const app = await defineApp('shop').use(catalog).boot();
    await app.dispose();

    After:

    import { isFailure } from '@arki/failure';
     
    const configured = defineApp('shop').use(catalog).configure();
    if (isFailure(configured)) return configured; // or exit at a process boundary
    const manifest = configured.manifest;
    const app = await configured.boot();
    if (isFailure(app)) return app;
    const disposed = await app.dispose();
    if (isFailure(disposed)) return disposed;

    Do the same for start() and stop(). Hooks return undefined or a typed failure. configure() stays synchronous, so a configure hook must not return a Promise. Details: docs/upgrade-phases.md and docs/upgrade-ownership.md.

  3. Move published services into provides factories. This applies to plugin() and feature().

    Before:

    plugin({
      name: 'database',
      boot: async () => ({ db: await openDb() }),
    });

    After:

    plugin({
      name: 'database',
      provides: { db: () => openDb() },
    });

    boot is for side effects only. A boot that returns a value no longer compiles. Each factory gets the unit's needs and may be async. Consumers now write needs: { db: database.provides.db } and get the type from Provided<typeof database.provides.db>.

  4. Delete version from every plugin() call.

    Before:

    plugin({ name: 'database', version: '1.0.0', provides: { db: () => openDb() } });

    After:

    plugin({ name: 'database', provides: { db: () => openDb() } });

    The app version in defineApp(name, { version }) stays. PluginManifest.version, the dot explain VERSION column and the dot.plugin.version span attribute are gone too.

  5. Rename error codes you match on.

    • DOT_LIFECYCLE_E0nn becomes ARKI_DOT_E0nn (same number).
    • FEATURE_E001 to FEATURE_E010 become ARKI_DOT_E022 to ARKI_DOT_E031.
    • The constants DotLifecycleErrorCode and FEATURE_ERROR_CODES keep their names.

    Before:

    if (error.code === 'DOT_LIFECYCLE_E020') { /* ... */ }

    After:

    if (error.code === 'ARKI_DOT_E020') { /* ... */ }
  6. Fix testApp and bootTestApp calls. Drop the type argument. The services are read from the plugins.

    Before:

    const app = testApp<{ db: Db }>([database]);

    After:

    const app = testApp([database]);
  7. If you use defineFeature, update the chain:

    • .needs() takes a record of aliases only. .needs({ db: database.provides.db }), not a bare handle.
    • .usecases() takes an object of factories, one per usecase.
    • Feature services are published under one key, <name>.services. Code that read per-service keys of a feature from the services bag must read that key. needsMiddleware already does.
    • rpc(contract, router) becomes rpc(contract, implementer, factories).

    Before:

    .usecases(createUsecases)
    .entrypoints(rpc(waitlistRpcContract, procedures(os)))

    After:

    .usecases({ signup, markInvited })
    .entrypoints(rpc(waitlistRpcContract, rpcImplementer, waitlistProcedures))

    Put the factory tree in one entrypoints/procedures.ts, shaped like the contract: export const waitlistProcedures = { signup: (impl: typeof rpcImplementer) => impl.signup.handler(...) }. An extra or missing key does not compile.

  8. Adapter authors only: driver(...) and driven(...) now receive MountedFeature[]. Read declarations with entrypointsOf(f) and a chain feature's units with f.plugins(). HTTP boundaries and auth policies return typed failure values; update code that inspects legacy strings or expects thrown refusals. Schema parsing and Journal, queue and mail calls now return typed failures too; see @arki/failure/docs/upgrade-typed-validation.md.

  9. Scripts and agents that read dot output:

    • Output is the arki.cli/1 envelope { ok, command, data, error, meta }. It is JSON when stdout is piped. Use --json or -o json to force it.
    • Exit codes: 0 ok, 1 bug, 2 usage error, 3 returned failure.
    • DOT_CLI_E001 to E011 are replaced by dot.* codes (dot.app_not_found, dot.doctor_failed, and so on). E004 and E005 are now cli.unknown_command and cli.invalid_input.
    • dot doctor reads .data.status. Error issues return dot.doctor_failed with the report in .error.data.
    • dot new returns .data.app, .data.operations[] and .data.nextSteps.
    • Managed preflight failures return dot.preflight_failed in .error.data.{app,migrations,report,failure}.

    Before:

    dot explain --openapi

    After:

    dot explain --as openapi

Verify

Run this in the consumer app:

bun run typecheck && dot doctor --app <path> --json | jq '.ok, .data.status'

It passes when typecheck exits 0, .ok is true and .data.status is "success" (or "warning" with no error issues).

All docs for @arki/[email protected]