feat(web): initialize Next.js project with Tailwind CSS and TypeScript setup

This commit is contained in:
Shreyas Kapale
2026-05-13 22:15:08 +05:30
committed by patel-lyzr
commit 2e94e6bdf6
84 changed files with 11063 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
// Package durability defines the journal abstraction the engine uses to
// turn each meaningful step (node execution, sub-workflow call, approval wait)
// into a replayable journal entry.
//
// Two implementations ship: RestateDurableCtx for production durability and
// DirectCtx for tests / library-mode embedding without a Restate process.
package durability
import (
"context"
"encoding/json"
"fmt"
"time"
)
// RetryPolicy configures per-step retry behavior. In Restate this maps to
// native per-Run retry options. In DirectCtx (tests) it retries in-process.
type RetryPolicy struct {
MaxAttempts int
InitialInterval time.Duration
}
// DurableCtx abstracts over journaled execution.
type DurableCtx interface {
// Run executes fn as a named journaled step. On replay (after crash/restart),
// completed steps return cached results without re-execution.
// The returned value must be JSON-serializable.
Run(name string, fn func(ctx context.Context) (any, error)) (any, error)
// RunWithRetry is like Run but applies a per-step retry policy.
RunWithRetry(name string, policy RetryPolicy, fn func(ctx context.Context) (any, error)) (any, error)
}
type durableCtxKey struct{}
// WithDurableCtx attaches a DurableCtx to a context.Context.
func WithDurableCtx(ctx context.Context, dctx DurableCtx) context.Context {
return context.WithValue(ctx, durableCtxKey{}, dctx)
}
// FromContext extracts a DurableCtx from the context, if present.
func FromContext(ctx context.Context) (DurableCtx, bool) {
dctx, ok := ctx.Value(durableCtxKey{}).(DurableCtx)
return dctx, ok && dctx != nil
}
// --- Restate context threading ---
// Separate from DurableCtx because some executors need the raw Restate context
// for features not available through the DurableCtx abstraction (e.g., Awakeables).
type restateCtxKey struct{}
// WithRestateCtx attaches a raw Restate WorkflowContext to a Go context.
// Used by the ApprovalExecutor to create Awakeables.
func WithRestateCtx(ctx context.Context, rctx any) context.Context {
return context.WithValue(ctx, restateCtxKey{}, rctx)
}
// RestateCtxFromContext extracts the raw Restate WorkflowContext.
// Returns nil if not running inside Restate.
func RestateCtxFromContext(ctx context.Context) any {
return ctx.Value(restateCtxKey{})
}
// ScopedDurableCtx wraps a DurableCtx and prefixes all step names.
// Used when a sub-workflow runs inside a parent durable workflow — the prefix
// avoids step name collisions between parent and child.
type ScopedDurableCtx struct {
Inner DurableCtx
Prefix string
}
func (s *ScopedDurableCtx) Run(name string, fn func(ctx context.Context) (any, error)) (any, error) {
return s.Inner.Run(s.Prefix+name, fn)
}
func (s *ScopedDurableCtx) RunWithRetry(name string, policy RetryPolicy, fn func(ctx context.Context) (any, error)) (any, error) {
return s.Inner.RunWithRetry(s.Prefix+name, policy, fn)
}
// RunAs executes a durable step and JSON-decodes the result into a concrete type T.
// Restate's `restate.Run` returns map[string]interface{} on replay instead of the
// original struct type; RunAs handles that round-trip transparently.
func RunAs[T any](dctx DurableCtx, name string, fn func(ctx context.Context) (T, error)) (T, error) {
raw, err := dctx.Run(name, func(ctx context.Context) (any, error) {
return fn(ctx)
})
if err != nil {
var zero T
return zero, err
}
if typed, ok := raw.(T); ok {
return typed, nil
}
jsonBytes, err := json.Marshal(raw)
if err != nil {
var zero T
return zero, fmt.Errorf("durability RunAs %q: marshal: %w", name, err)
}
var result T
if err := json.Unmarshal(jsonBytes, &result); err != nil {
var zero T
return zero, fmt.Errorf("durability RunAs %q: unmarshal into %T: %w", name, result, err)
}
return result, nil
}
// RunAsWithRetry is like RunAs but applies a per-step retry policy.
func RunAsWithRetry[T any](dctx DurableCtx, name string, policy RetryPolicy, fn func(ctx context.Context) (T, error)) (T, error) {
raw, err := dctx.RunWithRetry(name, policy, func(ctx context.Context) (any, error) {
return fn(ctx)
})
if err != nil {
var zero T
return zero, err
}
if typed, ok := raw.(T); ok {
return typed, nil
}
jsonBytes, err := json.Marshal(raw)
if err != nil {
var zero T
return zero, fmt.Errorf("durability RunAsWithRetry %q: marshal: %w", name, err)
}
var result T
if err := json.Unmarshal(jsonBytes, &result); err != nil {
var zero T
return zero, fmt.Errorf("durability RunAsWithRetry %q: unmarshal into %T: %w", name, result, err)
}
return result, nil
}
// --- Approval persistence interface ---
// ApprovalCreator persists a HITL approval row. Implemented by the storage layer.
// Defined here to avoid a cycle from executors → storage.
type ApprovalCreator interface {
CreateFromRecord(ctx context.Context, a *ApprovalRecord) error
}
// ApprovalRecord is the data needed to persist a pending approval.
type ApprovalRecord struct {
ID string
ExecutionID string
NodeName string
AwakeableID string
Status string
InputData map[string]any
APIKey string
}
type approvalCreatorKey struct{}
type executionIDKey struct{}
type apiKeyCtxKey struct{}
// WithApprovalCreator attaches an ApprovalCreator to a context.
func WithApprovalCreator(ctx context.Context, c ApprovalCreator) context.Context {
return context.WithValue(ctx, approvalCreatorKey{}, c)
}
// ApprovalCreatorFromContext extracts the ApprovalCreator, or nil.
func ApprovalCreatorFromContext(ctx context.Context) ApprovalCreator {
c, _ := ctx.Value(approvalCreatorKey{}).(ApprovalCreator)
return c
}
// WithExecutionID attaches the workflow execution ID to a context.
func WithExecutionID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, executionIDKey{}, id)
}
// ExecutionIDFromContext extracts the execution ID, or "".
func ExecutionIDFromContext(ctx context.Context) string {
s, _ := ctx.Value(executionIDKey{}).(string)
return s
}
// WithAPIKey attaches the API key to a context.
func WithAPIKey(ctx context.Context, key string) context.Context {
return context.WithValue(ctx, apiKeyCtxKey{}, key)
}
// APIKeyFromContext extracts the API key, or "".
func APIKeyFromContext(ctx context.Context) string {
s, _ := ctx.Value(apiKeyCtxKey{}).(string)
return s
}
+62
View File
@@ -0,0 +1,62 @@
package durability
import (
"context"
"log/slog"
"time"
)
// DirectCtx executes functions inline without journaling.
// Used in tests and for library-mode embedding (e.g., governor importing flow's
// engine but not running a Restate process).
type DirectCtx struct {
Ctx context.Context
}
func (d *DirectCtx) ctx() context.Context {
if d.Ctx != nil {
return d.Ctx
}
return context.Background()
}
func (d *DirectCtx) Run(_ string, fn func(ctx context.Context) (any, error)) (any, error) {
return fn(d.ctx())
}
func (d *DirectCtx) RunWithRetry(name string, policy RetryPolicy, fn func(ctx context.Context) (any, error)) (any, error) {
maxAttempts := policy.MaxAttempts
if maxAttempts <= 0 {
maxAttempts = 1
}
wait := policy.InitialInterval
if wait <= 0 {
wait = time.Second
}
ctx := d.ctx()
var lastErr error
for attempt := 0; attempt < maxAttempts; attempt++ {
result, err := fn(ctx)
if err == nil {
if attempt > 0 {
slog.InfoContext(ctx, "retry_succeeded",
slog.String("step", name),
slog.Int("attempt", attempt+1),
)
}
return result, nil
}
lastErr = err
if attempt < maxAttempts-1 {
slog.WarnContext(ctx, "retry",
slog.String("step", name),
slog.Int("attempt", attempt+1),
slog.Int("max", maxAttempts),
slog.Any("error", err),
)
time.Sleep(wait)
}
}
return nil, lastErr
}
+87
View File
@@ -0,0 +1,87 @@
package durability
import (
"context"
"errors"
"testing"
"time"
)
func TestDirectCtx_Run_passesThrough(t *testing.T) {
d := &DirectCtx{}
out, err := d.Run("step", func(_ context.Context) (any, error) {
return 42, nil
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != 42 {
t.Fatalf("expected 42, got %v", out)
}
}
func TestDirectCtx_RunWithRetry_succeedsOnSecondAttempt(t *testing.T) {
d := &DirectCtx{}
calls := 0
out, err := d.RunWithRetry("step", RetryPolicy{MaxAttempts: 3, InitialInterval: time.Millisecond}, func(_ context.Context) (any, error) {
calls++
if calls < 2 {
return nil, errors.New("flaky")
}
return "ok", nil
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if calls != 2 {
t.Fatalf("expected 2 calls, got %d", calls)
}
if out != "ok" {
t.Fatalf("expected ok, got %v", out)
}
}
func TestDirectCtx_RunWithRetry_givesUpAfterMaxAttempts(t *testing.T) {
d := &DirectCtx{}
calls := 0
_, err := d.RunWithRetry("step", RetryPolicy{MaxAttempts: 3, InitialInterval: time.Millisecond}, func(_ context.Context) (any, error) {
calls++
return nil, errors.New("permanent")
})
if err == nil {
t.Fatal("expected error after exhausting retries")
}
if calls != 3 {
t.Fatalf("expected 3 calls, got %d", calls)
}
}
func TestDirectCtx_RunWithRetry_zeroAttemptsDefaultsToOne(t *testing.T) {
d := &DirectCtx{}
calls := 0
_, err := d.RunWithRetry("step", RetryPolicy{}, func(_ context.Context) (any, error) {
calls++
return nil, errors.New("nope")
})
if err == nil {
t.Fatal("expected error")
}
if calls != 1 {
t.Fatalf("expected single attempt, got %d", calls)
}
}
func TestWithDurableCtx_roundTrip(t *testing.T) {
d := &DirectCtx{}
ctx := WithDurableCtx(context.Background(), d)
got, ok := FromContext(ctx)
if !ok || got != d {
t.Fatal("DurableCtx round-trip failed")
}
}
func TestFromContext_nilCtx(t *testing.T) {
if _, ok := FromContext(context.Background()); ok {
t.Fatal("FromContext should be false on a bare context")
}
}
+45
View File
@@ -0,0 +1,45 @@
package durability
import "errors"
// TerminalClassifier is implemented by errors that know whether they are permanent.
// RestateDurableCtx checks this interface: errors marked terminal are wrapped with
// restate.TerminalError so Restate stops retrying immediately.
type TerminalClassifier interface {
IsTerminal() bool
}
// PermanentError marks an error as terminal for durable execution.
// Use this to wrap errors that should never be retried (e.g., resource not found,
// invalid configuration) without importing Restate.
type PermanentError struct{ Err error }
func (e *PermanentError) Error() string { return e.Err.Error() }
func (e *PermanentError) Unwrap() error { return e.Err }
func (e *PermanentError) IsTerminal() bool { return true }
// RetryableError marks an error as retryable for durable execution.
// By default all errors are terminal (no retry). Wrap with this to allow
// Restate to retry (e.g., transient network errors, rate limits).
type RetryableError struct{ Err error }
func (e *RetryableError) Error() string { return e.Err.Error() }
func (e *RetryableError) Unwrap() error { return e.Err }
func (e *RetryableError) IsTerminal() bool { return false }
// IsTerminalError checks whether err should be treated as terminal.
// Returns true unless the error (or any in its chain) is explicitly marked
// retryable via RetryableError.
func IsTerminalError(err error) bool {
var tc TerminalClassifier
if errors.As(err, &tc) {
return tc.IsTerminal()
}
return true
}
// IsRetryableError checks whether err is explicitly marked as retryable.
func IsRetryableError(err error) bool {
var tc TerminalClassifier
return errors.As(err, &tc) && !tc.IsTerminal()
}
+55
View File
@@ -0,0 +1,55 @@
package durability
import (
"errors"
"fmt"
"testing"
)
func TestIsTerminalError_default(t *testing.T) {
if !IsTerminalError(errors.New("plain")) {
t.Fatal("plain errors should default to terminal (no retry)")
}
}
func TestPermanentError_isTerminal(t *testing.T) {
err := &PermanentError{Err: errors.New("nope")}
if !IsTerminalError(err) {
t.Fatal("PermanentError must be terminal")
}
if IsRetryableError(err) {
t.Fatal("PermanentError must not be retryable")
}
}
func TestRetryableError_isNotTerminal(t *testing.T) {
err := &RetryableError{Err: errors.New("transient")}
if IsTerminalError(err) {
t.Fatal("RetryableError must not be terminal")
}
if !IsRetryableError(err) {
t.Fatal("RetryableError must be retryable")
}
}
func TestRetryableError_unwrappedThroughFmt(t *testing.T) {
inner := errors.New("network down")
wrapped := fmt.Errorf("step bar: %w", &RetryableError{Err: inner})
if IsTerminalError(wrapped) {
t.Fatal("retryable classification must survive fmt.Errorf wrapping")
}
if !IsRetryableError(wrapped) {
t.Fatal("retryable classification must survive fmt.Errorf wrapping")
}
}
func TestPermanentError_unwrap(t *testing.T) {
inner := errors.New("missing")
err := &PermanentError{Err: inner}
if !errors.Is(err, inner) {
t.Fatal("PermanentError must unwrap to inner")
}
if err.Error() != "missing" {
t.Fatalf("unexpected message: %q", err.Error())
}
}
+50
View File
@@ -0,0 +1,50 @@
package durability
import (
"context"
restate "github.com/restatedev/sdk-go"
)
// RestateDurableCtx wraps a Restate Context to provide durable step execution.
// Each Run() call is journaled by Restate — on crash/restart, completed steps
// return cached results without re-execution.
// Works with any Restate context type (Context, ObjectContext, WorkflowContext).
type RestateDurableCtx struct {
Rctx restate.Context
}
func (d *RestateDurableCtx) run(name string, fn func(ctx context.Context) (any, error), opts ...restate.RunOption) (any, error) {
allOpts := append([]restate.RunOption{restate.WithName(name)}, opts...)
result, err := restate.Run(d.Rctx, func(rc restate.RunContext) (any, error) {
result, err := fn(rc)
if err != nil && !IsRetryableError(err) {
// Default: all errors are terminal unless explicitly wrapped
// in RetryableError by the caller.
return result, restate.TerminalError(err)
}
return result, err
}, allOpts...)
// On replay, Restate preserves terminal-ness (restate.IsTerminalError) but
// strips Go error types. Re-wrap as PermanentError so callers can use
// durability.IsTerminalError without parsing error strings.
if err != nil && restate.IsTerminalError(err) && !IsTerminalError(err) {
return result, &PermanentError{Err: err}
}
return result, err
}
func (d *RestateDurableCtx) Run(name string, fn func(ctx context.Context) (any, error)) (any, error) {
return d.run(name, fn)
}
func (d *RestateDurableCtx) RunWithRetry(name string, policy RetryPolicy, fn func(ctx context.Context) (any, error)) (any, error) {
var opts []restate.RunOption
if policy.MaxAttempts > 0 {
opts = append(opts, restate.WithMaxRetryAttempts(uint(policy.MaxAttempts)))
}
if policy.InitialInterval > 0 {
opts = append(opts, restate.WithInitialRetryInterval(policy.InitialInterval))
}
return d.run(name, fn, opts...)
}
+92
View File
@@ -0,0 +1,92 @@
package durability
import (
"context"
"errors"
"testing"
)
type sample struct {
Name string `json:"name"`
Count int `json:"count"`
}
// fakeReplayCtx returns map[string]any from Run instead of the original type,
// the way Restate behaves on journal replay. Verifies RunAs handles the
// JSON round-trip transparently.
type fakeReplayCtx struct{}
func (fakeReplayCtx) Run(_ string, fn func(ctx context.Context) (any, error)) (any, error) {
v, err := fn(context.Background())
if err != nil {
return nil, err
}
// Simulate JSON round-trip ala Restate replay: encode then decode into map.
// Real Restate does this via its journal serialization.
if s, ok := v.(sample); ok {
return map[string]any{"name": s.Name, "count": float64(s.Count)}, nil
}
return v, nil
}
func (fakeReplayCtx) RunWithRetry(name string, _ RetryPolicy, fn func(ctx context.Context) (any, error)) (any, error) {
return fakeReplayCtx{}.Run(name, fn)
}
func TestRunAs_directPath(t *testing.T) {
d := &DirectCtx{}
got, err := RunAs[sample](d, "step", func(_ context.Context) (sample, error) {
return sample{Name: "alice", Count: 7}, nil
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Name != "alice" || got.Count != 7 {
t.Fatalf("unexpected: %+v", got)
}
}
func TestRunAs_replayPath(t *testing.T) {
got, err := RunAs[sample](fakeReplayCtx{}, "step", func(_ context.Context) (sample, error) {
return sample{Name: "bob", Count: 9}, nil
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Name != "bob" || got.Count != 9 {
t.Fatalf("RunAs failed to round-trip via JSON: %+v", got)
}
}
func TestRunAs_propagatesErrors(t *testing.T) {
d := &DirectCtx{}
_, err := RunAs[sample](d, "step", func(_ context.Context) (sample, error) {
return sample{}, errors.New("boom")
})
if err == nil || err.Error() != "boom" {
t.Fatalf("expected boom, got %v", err)
}
}
func TestScopedDurableCtx_prefixesStepName(t *testing.T) {
captured := ""
rec := &recordingCtx{onRun: func(name string) { captured = name }}
scoped := &ScopedDurableCtx{Inner: rec, Prefix: "iter:0/"}
_, _ = scoped.Run("foo", func(_ context.Context) (any, error) { return nil, nil })
if captured != "iter:0/foo" {
t.Fatalf("expected prefixed step name, got %q", captured)
}
}
type recordingCtx struct{ onRun func(string) }
func (r *recordingCtx) Run(name string, fn func(ctx context.Context) (any, error)) (any, error) {
if r.onRun != nil {
r.onRun(name)
}
return fn(context.Background())
}
func (r *recordingCtx) RunWithRetry(name string, _ RetryPolicy, fn func(ctx context.Context) (any, error)) (any, error) {
return r.Run(name, fn)
}