@volter/twin-temporal
v0.1.37
Published
Local Temporal frontend twin: WorkflowService over gRPC (HTTP/2) on loopback — task queues, workflow and activity tasks, timers on the World clock, signals, queries, children, continue-as-new, visibility; unserved RPCs answer UNIMPLEMENTED. Built on @volt
Readme
@volter/twin-temporal
Temporal — the workflow engine — served from a World with nothing installed: the frontend's gRPC API
(temporal.api.workflowservice.v1.WorkflowService, temporal.api.operatorservice.v1.OperatorService,
grpc.health.v1.Health) on one loopback port. An unmodified app on the Temporal SDKs
(@temporalio/client, @temporalio/worker) starts, runs, signals, queries and reads workflows against
it; the worker runs the app's own workflow and activity code exactly as it would against a cluster.
It stands where Temporal's local setups stand: temporal server start-dev (the Temporal CLI's
single-binary dev server with SQLite) or a docker-compose stack (temporalio/auto-setup with
PostgreSQL and Elasticsearch — Postiz's own docker-compose.dev.yaml runs four containers for it).
Neither is installed or run; this twin is that frontend, containerless, with its state in the World.
world-temporal serve --port 7233 --root .volter/world
# app: TEMPORAL_ADDRESS=127.0.0.1:7233 (namespace `default` exists)| command | what it does |
|---|---|
| world-temporal serve [--port N] [--root DIR] [--read-only] | the frontend on loopback: gRPC (HTTP/2, h2c) for the SDKs; HTTP/1 on the same port for the World's doors and the HTTP API |
| world-temporal conformance [--root DIR] | probe the wire over a real socket; non-zero on any failure |
Wiring
The SDKs dial the address they are configured with (Connection.connect({ address }),
NativeConnection.connect({ address })), conventionally TEMPORAL_ADDRESS. A World injects
TEMPORAL_ADDRESS=<host>:<port> at this twin (APP_READ_ENDPOINT_ENV in
packages/world-runtime/src/init.ts); there is no injector host, because gRPC never passes the
http/fetch injector. TEMPORAL_API_KEY must stay unset: the SDKs turn TLS on when a key is given, and
this loopback frontend speaks h2c only.
How it is built
- The wire is this pack's own HTTP/2 (
temporal-h2.ts: framing, both directions of flow control, HPACK with the tables derived from the vendored RFC 7541) under gRPC framing (temporal-grpc.ts). Bun 1.3'snode:http2server fails nghttp2 clients at the connection preface (a node client getsERR_HTTP2_ERRORbefore the first SETTINGS; Bun 1.3.11, Node 22.23.2, 2026-09-27), and the repository runs under Bun; a socket works under both runtimes. The first bytes of a connection decide HTTP/2 or HTTP/1. - The RPC table (
temporal-twin.tsSERVED) routes each served method to the engine; every other method the vendored services declare answers gRPCUNIMPLEMENTEDnaming it. - The engine (
temporal-engine.ts) is the history service: task queues with long polls, workflow tasks and the commands they return, activities with retry policies, heartbeats and timeouts, timers, signals, queries, children with parent-close policy, continue-as-new, workflow retry, markers, search attributes and memo, cancellation and termination, buffered events (a completion that would close over events that arrived during its task failsUNHANDLED_COMMAND, as the server does). Time is the World clock (worldNow()): a pinned clock holds every timer, andvolter-world clock advancefires what falls due. - State is the World's. A run is a
workflow-executionsubject in its WorkflowExecutionInfo shape (plus the server's_state); a namespace is anamespacesubject with its custom search attributes. A run's history is the engine slot (temporal-history.ts): content-addressed event chunks in the World's blob store, named in order by the run's subject, so a branch replays exactly the history its tree names. - Visibility reads Temporal's list filter language (
temporal-visibility.ts) over the runs' system and custom search attributes. - The HTTP API (
temporal-http.ts) routes the protos'google.api.httpannotations to the same table with proto3 JSON; it is also the pack's fetch (createTemporalTwinFetch) for the World's doors (GET /twin,GET /twin/store/workflows).
The spec is the vendored spec/proto (temporalio/api as @temporalio/[email protected] ships it);
derive.ts compiles it to src/generated/temporal-api.json and derive.ts --check re-derives it.
Coverage
The denominator is every method of the three services and every CommandType
(src/temporal-capabilities.ts); a served method is done with a verify, every other is todo.
Served: GetSystemInfo, DescribeNamespace, ListNamespaces, RegisterNamespace, StartWorkflowExecution,
SignalWithStartWorkflowExecution, SignalWorkflowExecution, RequestCancelWorkflowExecution,
TerminateWorkflowExecution, GetWorkflowExecutionHistory, DescribeWorkflowExecution,
ListWorkflowExecutions, CountWorkflowExecutions, PollWorkflowTaskQueue, RespondWorkflowTaskCompleted,
RespondWorkflowTaskFailed, PollActivityTaskQueue, RespondActivityTask{Completed,Failed,Canceled}[ById],
RecordActivityTaskHeartbeat[ById], QueryWorkflow, RespondQueryTaskCompleted, ResetStickyTaskQueue,
ShutdownWorker; OperatorService List/Add/RemoveSearchAttributes; Health Check.
Not served (UNIMPLEMENTED, filed todo): schedules, workflow updates, batch operations, reset,
worker deployments and versioning, Nexus, standalone activities, task-queue description and
reachability, archival, the rest of OperatorService. Narrower than the server, and filed:
sticky queues (every task goes to the normal queue with its full history, which the SDKs accept),
cron schedules (refused by name), eager start, completion callbacks, Text attributes matched by
equality rather than tokenized search, TLS, message compression, grpc-status-details-bin, and the
HTTP API's payload shorthand, transient workflow-task attempts (every failed attempt is written),
namespace retention (closed runs are kept), and search-attribute value types (only names are checked).
No mirror: someone doing Temporal's core job writes workflow code and runs workers; Temporal's Web UI is an observer over the frontend API. Several of the reads it makes (reverse history, task-queue description, schedules, counts grouped by status) are among the unserved RPCs above.
Proofs
src/temporal-sdk.integration.test.tsruns the real@temporalio/clientand@temporalio/worker1.15.0 under Node against the twin: an activity retried to success, a timer, a query, a signal, exhausted retries, a failing workflow, children (awaited and abandoned), continue-as-new,patched(), a search-attribute upsert and a list query, a conflicting start, terminate, and an UNIMPLEMENTED method (test-fixtures/sdk/); then Postiz's own client calls and workflow shapes on a World clock the test advances (test-fixtures/postiz/).src/temporal-h2.test.tsdecodes every RFC 7541 Appendix C example.
Connector
syncTemporalFromReal pulls a real frontend's namespaces and their custom search attributes over an
injected unary call (liveTemporalExecute: gRPC over node:http2, behind the rate budget in
temporal-budget.ts). Runs are not pulled — a run is its history, advanced only by the workers that
execute it — and a real head refuses every entry by name (performTemporalAction).
