Upgrade @arki/contracts 0.1.0 to 0.2.0

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

This records the original 0.2.0 additions. Current callers must also follow the typed ID migration and handle returned cursor failures; the older exception-only cursor examples below describe the superseded API.

What changed

  • Adds prefixed ids declared once and keyset pagination schemas (RFC docs/rfcs/2026-10-15-dot-feature-builder.md, phase 1c). Both are browser-safe (zod only).

    • defineId('sty_') returns { prefix, schema, create, is }; IdOf<typeof storyId> is `sty_${string}`. The schema is a zod template literal (it exports a JSON Schema pattern); create() returns prefix + 24 cuid2-shaped characters with Web Crypto or an exact IdGenerationFailure; check isFailure before using the ID. Missing/malformed providers return RandomSourceUnavailable; provider defects return UnexpectedDefect with private causes. ID_GENERATION_FAILURES declares both definitions. Prefixes are kept exactly as given, so stored ids stay valid. assertUniqueIdPrefixes(...ids) rejects a repeated prefix at compile time and runtime, and a prefix that starts another at runtime.
    • keyset({ createdAt: 'desc', id: 'desc' }) declares a list order and its opaque base64url cursor (encode, decode, cursor schema; dates round-trip as dates). pageInput(keyset, { defaultLimit, maxLimit }) and page(itemSchema) are the contract's input and output; @arki/db/pagination builds the query from the same declaration.
  • Coded errors and Hermes support. Static definition misuse raises ContractsError (a TypeError) with a stable ARKI_CONTRACTS_E00n code and a remediation; error messages start with the code instead of [contracts]. Operational ID and cursor failures are returned. Keyset cursors no longer use TextDecoder, which React Native (Hermes) lacks, so non-ASCII cursors decode there. The package uses the shared library lint preset and a cross-runtime smoke test.

  • New @arki/contracts/orpc entry: one typed identity per oRPC contract procedure. operations(contract) returns a frozen tree with an Operation (path, name, meta, phantom input/output/error types) at every leaf; operationAt(contract, path) finds one; OPERATION ('~op'), Identified<Op> and operationOf(carrier) let client utils carry it. Types are inferred from the contract (OperationOf<C>, InputOf, OutputOf, ErrorOf, MetaOf). @orpc/contract is a new optional peer; the main entry still does not import it.

    Fix: pageInput's client input now types the cursor as string (it was unknown), because Keyset.cursor is z.ZodType<string, string>.

    The package now runs test:runtimes (Node, Bun, Deno, browser, Hermes).

Public API changes

Nothing is removed. These are new:

  • defineId, IdOf, IdDeclaration, assertUniqueIdPrefixes: prefixed ids declared once.
  • keyset, pageInput, page, Page, Keyset, CursorValue: keyset pagination.
  • ContractsError, CONTRACTS_ERROR_CODES, ContractsErrorCode: coded errors.
  • @arki/contracts/orpc: operations, operationAt, operationOf, OPERATION, Identified and the Operation types. It needs the optional peer @orpc/contract.

Two things changed in behaviour:

  • Error messages start with the code (ARKI_CONTRACTS_E004: ...) instead of [contracts].
  • pageInput's client input types the cursor as string (it was unknown).

Steps

  1. Update the package.

    bun add @arki/[email protected]
  2. Stop matching error text. Match the code. A ContractsError is still a TypeError, so instanceof TypeError keeps working.

    Before:

    try {
      keyset.decode(input);
    } catch (error) {
      if (error instanceof Error && error.message.startsWith('[contracts]')) return null;
      throw error;
    }

    After:

    import { CONTRACTS_ERROR_CODES, ContractsError } from '@arki/contracts';
     
    try {
      keyset.decode(input);
    } catch (error) {
      if (error instanceof ContractsError && error.code === CONTRACTS_ERROR_CODES.invalidCursor) return null;
      throw error;
    }
  3. If typed client code gave pageInput a cursor of type unknown, pass a string (the cursor the last page returned) or leave it out.

  4. Optional. Use the new tools:

    • Declare ids once with defineId('sty_') instead of writing the prefix in a schema, a type and a generator.
    • Declare list order once with keyset({ createdAt: 'desc', id: 'desc' }). @arki/db/pagination builds the query from it.
    • Give each oRPC procedure an identity with operations(contract) from @arki/contracts/orpc. Install @orpc/contract first.

Verify

bun run typecheck && bun run test

Expected: the type check ends with no errors, and Vitest reports Test Files ... passed with no failed tests.

All docs for @arki/[email protected]