Crux
API Reference@use-crux/core

Runtime Signal adapter surface

Exact @use-crux/core/runtime contracts for durable Signal storage, records, codecs, atomic composites, and conformance.

This page is the complete adapter extension surface shipped for durable Signal-to-Flow delivery. All runtime contracts come from @use-crux/core/runtime; conformance helpers come from @use-crux/core/runtime/testing.

The bundled in-memory Runtime store is process-local. No bundled durable Signal store or provider is certified in this release. A custom adapter must implement the contracts below and pass reactive composite certification before it can honestly declare durable Signal support.

Store capability

RuntimeStoreAdapter.durability reports whether the configured substrate survives process loss:

interface RuntimeStoreAdapter extends RuntimeStoreTransaction {
  readonly id: string;
  readonly durability?: "durable" | "process-local";
  readonly leases: LeasePort;
  readonly results?: RuntimeResultPayloadPort;
  runComposite?<K extends RuntimeCompositeKind>(
    kind: K,
    input: RuntimeCompositeInput[K],
  ): Promise<RuntimeCompositeResult[K]>;
  transact<T>(
    fn: (tx: RuntimeStoreTransaction) => Promise<T>,
  ): Promise<T>;
}

interface RuntimeStoreTransaction {
  // Other required Runtime ports are omitted here.
  readonly signals?: RuntimeSignalStorePort;
}

durability: "durable" is a mechanical claim, not certification. An omitted value is treated as unproven. "process-local" and unproven stores fail static Signal Flow activation before handler or work allocation. A durable store must also expose signals and satisfy the complete Runtime capability profile.

Signal store port

interface RuntimeSignalStorePort {
  getOccurrence(
    namespace: string,
    occurrenceId: string,
  ): Promise<SignalOccurrenceRecord | null>;

  findOccurrenceByIdempotency(
    namespace: string,
    signalId: string,
    idempotencyHash: string,
  ): Promise<SignalOccurrenceRecord | null>;

  putOccurrence(record: SignalOccurrenceRecord): Promise<void>;

  getDelivery(
    namespace: string,
    deliveryId: string,
  ): Promise<SignalDeliveryRecord | null>;

  listDeliveries(
    namespace: string,
    occurrenceId: string,
  ): Promise<readonly SignalDeliveryRecord[]>;

  putDelivery(record: SignalDeliveryRecord): Promise<void>;
}

These methods run inside the enclosing Runtime transaction or native named composite. Child A exposes no provider-envelope field and no Session dynamic subscription record or store method.

Persisted records

type ReactiveConsumerRef = {
  readonly kind: "flow.signal-wait";
  readonly flowId: string;
  readonly waiterId: string;
  readonly workId: string;
};

interface SignalOccurrenceRecord {
  readonly schemaVersion: 1;
  readonly namespace: string;
  readonly occurrenceId: string;
  readonly signalId: string;
  readonly payload: JsonValue;
  readonly payloadCodec?: SignalPayloadCodec;
  readonly acceptedAt: string;
  readonly idempotencyHash?: string;
}

interface SignalDeliveryRecord {
  readonly schemaVersion: 1;
  readonly namespace: string;
  readonly deliveryId: string;
  readonly occurrenceId: string;
  readonly consumer: ReactiveConsumerRef;
  readonly state:
    | "pending"
    | "leased"
    | "delivered"
    | "failed"
    | "dead-letter";
  readonly attempts: number;
  readonly updatedAt: string;
}

Delivery identity is per occurrence and waiter binding. Do not collapse two waiters owned by the same Flow work item into one delivery. Persist ISO strings as strings; public Date objects are materialized as detached values at the SDK boundary.

Payload codec

The following are public exports:

const SIGNAL_PAYLOAD_CODEC = "crux.signal-json.v1" as const;
type SignalPayloadCodec = typeof SIGNAL_PAYLOAD_CODEC;

function encodeSignalPayload(payload: JsonValue): string;
function decodeSignalPayload(
  payload: JsonValue,
  codec?: string,
): JsonValue;

New occurrence records store the encoded string in payload and SIGNAL_PAYLOAD_CODEC in payloadCodec. Adapters must round-trip both values opaquely. This codec preserves accepted finite JavaScript values such as negative zero. An absent codec reads a legacy raw JsonValue; an unknown codec or malformed encoded value fails with PAYLOAD_NOT_JSON.

Atomic named composites

Two named composites are relevant to durable Signal delivery:

KindAtomic responsibility
flow.signal-wait.registerPersist Flow suspension, waiter binding, snapshot, and leased-work transition together.
signal.publishAccept one occurrence plus every required delivery, event/waiter transition, replay snapshot, and wake row.

The generic adapter override is:

runComposite?<K extends RuntimeCompositeKind>(
  kind: K,
  input: RuntimeCompositeInput[K],
): Promise<RuntimeCompositeResult[K]>;

Most adapters should omit it. Core then calls transact() and executes the kernel-owned body through runDefaultRuntimeComposite(). Override runComposite() only when the substrate requires its atomic boundary to run inside the provider. Such an override must preserve the exact typed RuntimeCompositeInput and RuntimeCompositeResult mapping. The public runtimeCompositeBodies registry is available when a host executes the same kernel-owned policy within its native transaction.

Required certification

Run both general store conformance and reactive composite conformance in the adapter's Vitest suite:

import {
  runReactiveCompositeAdapterTests,
  runStoreAdapterTests,
} from "@use-crux/core/runtime/testing";

runStoreAdapterTests({
  name: "acme-runtime",
  createStore,
});

runReactiveCompositeAdapterTests({
  name: "acme-runtime",
  createStore,
  failAfterWrites: (store, writes) => {
    store.testing.failAfter(writes);
  },
});

createStore must return a fresh RuntimeStoreAdapter whose signals port is present. failAfterWrites must deterministically abort the next composite after exactly the requested number of successful mutations and then reset. Claiming that the substrate is atomic does not skip this requirement.

The reactive suite verifies occurrence-plus-delivery atomicity, distinct delivery identities, predicate candidate ordering while work is pending or leased, and rollback at every reactive write boundary. Passing certifies only the supplied adapter harness and configuration; it does not certify a provider family or every deployment topology.

On this page