Files
zeekay 6112841b1e feat(sdk/client): native user-facing RPC surface, zero upstream deps
Implements pkg/sdk/client — the Temporal-replacement surface base,
commerce, and ta call when migrating off go.temporal.io/sdk/client.
Transport is luxfi/zap; zero go.temporal.io/* and zero
google.golang.org/grpc imports.

Opcode allocation (append-only, never reuse):
  0x0050-0x005F pkg/tasks legacy (submit / schedule), reserved
  0x0060-0x006F workflow lifecycle (start, signal, cancel,
                terminate, describe, list)
  0x0070-0x007F schedule ops (create, list, delete, pause)
  0x0080-0x008F namespace ops (register, describe, list)
  0x0090-0x009F health + meta
  0x00A0-0x00AF worker poll / respond (moved up from 0x0070
                to avoid collision with schedule ops)

Wire (v1): request and response bodies are a JSON document wrapped
in a ZAP envelope with three fields:
  field 0  (Bytes)  — body
  field 8  (Uint32) — status (0/200 = ok)
  field 12 (Bytes)  — error detail on non-zero status
Opcode is stamped on the ZAP frame flags (high byte). Native ZAP
serde replaces JSON in a follow-up without changing opcodes.

Files:
- client.go      — Client interface (Close/ExecuteWorkflow/SignalWorkflow/
                   CancelWorkflow/Terminate/Describe/List + schedule +
                   namespace + health), Dial, Transport abstraction,
                   luxfi/zap default transport, envelope codec.
- options.go     — Options, StartWorkflowOptions (RetryPolicy via
                   pkg/sdk/temporal, no upstream import).
- workflow.go    — ExecuteWorkflow, Signal/Cancel/Terminate/Describe/List,
                   WorkflowRun with reflect-based type-name resolution.
- namespace.go   — Register/Describe/List + wire duration-ms codec.
- schedule.go    — Create/List/Delete/Pause + wire duration-ms codec.
- transport.go   — WorkerTransport + PollWorkflowTaskRequest etc. +
                   the worker-opcode constants (now 0x00A0+).
- client_test.go — Dial/Close test, ExecuteWorkflow round-trip with a
                   fake Transport capturing the request and replaying
                   a canned response (same pattern as pkg/tasks).

Worker bridge helpers (worker_bridge.go, worker_transport.go) were
added concurrently on feat/zap-internal; they import and wrap the
Transport this commit defines.

WorkflowRun.Get gap: the v1 schema does not yet declare a
GetWorkflowHistory / GetWorkflowResult RPC, so Get polls
DescribeWorkflow and returns ErrNotImplementedLocally when the
caller asks for a result from a completed workflow. This is marked
in the doc comment and will be filled in when pkg/sdk/worker lands
the history-fetch opcode.

Verification:
  go vet ./pkg/sdk/client/...    — clean
  go test ./pkg/sdk/client/...   — 7/7 pass
  go list -deps ./pkg/sdk/client/...  — no go.temporal.io/*, no
                                        google.golang.org/grpc
2026-04-23 10:38:02 -07:00

133 lines
4.1 KiB
Go

package client
import (
"context"
"errors"
"time"
)
// Namespace mirrors schema/tasks.zap Namespace for the v1 JSON wire.
type Namespace struct {
Info NamespaceInfo `json:"info"`
Config NamespaceConfig `json:"config"`
}
// NamespaceInfo mirrors schema/tasks.zap NamespaceInfo.
type NamespaceInfo struct {
Name string `json:"name"`
State NamespaceState `json:"state"`
Description string `json:"description,omitempty"`
OwnerEmail string `json:"owner_email,omitempty"`
ID string `json:"id,omitempty"`
}
// NamespaceConfig mirrors schema/tasks.zap NamespaceConfig. Retention is
// exposed as a Go Duration on the Go surface and marshalled to/from
// milliseconds on the wire (see retentionJSON below).
type NamespaceConfig struct {
Retention time.Duration `json:"-"`
}
// retentionJSON captures the ms-granular wire shape without leaking
// temporal.io types.
type namespaceConfigWire struct {
RetentionMs int64 `json:"retention_ms,omitempty"`
}
// MarshalJSON implements custom encoding for the wire shape.
func (n NamespaceConfig) MarshalJSON() ([]byte, error) {
return jsonMarshal(namespaceConfigWire{RetentionMs: n.Retention.Milliseconds()})
}
// UnmarshalJSON implements custom decoding for the wire shape.
func (n *NamespaceConfig) UnmarshalJSON(b []byte) error {
var w namespaceConfigWire
if err := jsonUnmarshal(b, &w); err != nil {
return err
}
n.Retention = time.Duration(w.RetentionMs) * time.Millisecond
return nil
}
// NamespaceState mirrors the Int8 state enum.
type NamespaceState int8
const (
NamespaceStateUnspecified NamespaceState = 0
NamespaceStateRegistered NamespaceState = 1
NamespaceStateDeprecated NamespaceState = 2
NamespaceStateDeleted NamespaceState = 3
)
// RegisterNamespaceRequest is the argument to RegisterNamespace. Mirrors
// schema/tasks.zap: server receives the full Namespace.
type RegisterNamespaceRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
OwnerEmail string `json:"owner_email,omitempty"`
Retention time.Duration `json:"-"`
}
type registerNamespaceWire struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
OwnerEmail string `json:"owner_email,omitempty"`
RetentionMs int64 `json:"retention_ms,omitempty"`
}
type describeNamespaceRequest struct {
Name string `json:"name"`
}
type describeNamespaceResponse struct {
Namespace Namespace `json:"namespace"`
}
type listNamespacesRequest struct {
PageSize int32 `json:"page_size,omitempty"`
NextPageToken []byte `json:"next_page_token,omitempty"`
}
// ListNamespacesResponse is returned by ListNamespaces.
type ListNamespacesResponse struct {
Namespaces []Namespace `json:"namespaces"`
NextPageToken []byte `json:"next_page_token,omitempty"`
}
// RegisterNamespace implements Client.
func (c *clientImpl) RegisterNamespace(ctx context.Context, req *RegisterNamespaceRequest) error {
if req == nil || req.Name == "" {
return errors.New("hanzo/tasks/client: namespace name is required")
}
wire := registerNamespaceWire{
Name: req.Name,
Description: req.Description,
OwnerEmail: req.OwnerEmail,
RetentionMs: req.Retention.Milliseconds(),
}
return c.roundTrip(ctx, opRegisterNamespace, wire, nil)
}
// DescribeNamespace implements Client.
func (c *clientImpl) DescribeNamespace(ctx context.Context, name string) (*Namespace, error) {
if name == "" {
return nil, errors.New("hanzo/tasks/client: namespace name is required")
}
var resp describeNamespaceResponse
if err := c.roundTrip(ctx, opDescribeNamespace, describeNamespaceRequest{Name: name}, &resp); err != nil {
return nil, err
}
out := resp.Namespace
return &out, nil
}
// ListNamespaces implements Client.
func (c *clientImpl) ListNamespaces(ctx context.Context, pageSize int32, nextPageToken []byte) (*ListNamespacesResponse, error) {
var resp ListNamespacesResponse
req := listNamespacesRequest{PageSize: pageSize, NextPageToken: nextPageToken}
if err := c.roundTrip(ctx, opListNamespaces, req, &resp); err != nil {
return nil, err
}
return &resp, nil
}