diff --git a/packages/libs/restate-sdk-gen/e2e/state.e2e.test.ts b/packages/libs/restate-sdk-gen/e2e/state.e2e.test.ts index 57a8de61..705e6bfe 100644 --- a/packages/libs/restate-sdk-gen/e2e/state.e2e.test.ts +++ b/packages/libs/restate-sdk-gen/e2e/state.e2e.test.ts @@ -9,19 +9,16 @@ * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE */ -// State e2e: a virtual-object counter exercises state() against a real -// Restate runtime — get/set are journaled, clear and clearAll affect -// persistent VO state, keys() returns the live key set. +// State e2e: a virtual-object counter exercises the new per-key accessor API +// against a real Restate runtime. // -// Two flavors of the API are demonstrated: -// -// - Typed: state() gives keyof-checked names and -// per-key value types. get("count") infers Future; -// state.get("nope") would be a type error. -// - Shared: read-only handlers use sharedState(), -// which drops set/clear/clearAll from the type — calling them is -// a compile error, mirroring what the runtime would do anyway -// (ObjectSharedContext has no write methods). +// Demonstrates: +// - state(config) defined at module level (lazy — resolves ops on method call). +// count has default 0 → get() returns Future (never null). +// secondary uses typed() → get() returns Future. +// - state(config) with factory default (closure) for mutable defaults. +// - getAllStateKeys(): list live keys. +// - clear() on per-key accessors. // // Both runtime modes: default + alwaysReplay. @@ -29,14 +26,21 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest"; import * as restate from "@restatedev/restate-sdk"; import * as clients from "@restatedev/restate-sdk-clients"; import { RestateTestEnvironment } from "@restatedev/restate-sdk-testcontainers"; -import { gen, execute, state, sharedState } from "@restatedev/restate-sdk-gen"; - -// Typed-state shape for the counter object. Keys and value types here -// flow through to state() at every call site below. -type CounterState = { - count: number; - secondary: string; -}; +import { + gen, + execute, + state, + typed, + getAllStateKeys, +} from "@restatedev/restate-sdk-gen"; + +// Defined once at module level — lazy, resolves ops only when methods are called. +// count has a static default 0 (get returns Future). +// secondary uses typed() — typed but no default (get returns Future). +const counterState = state({ + count: { default: 0 }, + secondary: typed(), +}); const counterObj = restate.object({ name: "counter", @@ -45,20 +49,18 @@ const counterObj = restate.object({ execute( ctx, gen(function* () { - const s = state(); - const current = (yield* s.get("count")) ?? 0; + const current = yield* counterState.count.get(); // Future const next = current + n; - s.set("count", next); + counterState.count.set(next); return next; }) ), - // Read-only handler — uses sharedState() so writes wouldn't compile. current: async (ctx: restate.ObjectSharedContext): Promise => execute( ctx, gen(function* () { - return (yield* sharedState().get("count")) ?? 0; + return yield* counterState.count.get(); // still Future — default 0 }) ), @@ -66,7 +68,7 @@ const counterObj = restate.object({ execute( ctx, gen(function* () { - return yield* sharedState().keys(); + return yield* getAllStateKeys(); }) ), @@ -77,9 +79,8 @@ const counterObj = restate.object({ execute( ctx, gen(function* () { - const s = state(); - s.set("secondary", value); - const count = (yield* s.get("count")) ?? 0; + counterState.secondary.set(value); + const count: number = yield* counterState.count.get(); return { count, secondary: value }; }) ), @@ -88,7 +89,7 @@ const counterObj = restate.object({ execute( ctx, gen(function* () { - state().clear("secondary"); + counterState.secondary.clear(); }) ), @@ -96,7 +97,35 @@ const counterObj = restate.object({ execute( ctx, gen(function* () { - state().clearAll(); + counterState.count.clear(); + counterState.secondary.clear(); + }) + ), + }, +}); + +const listState = state({ + items: { default: () => [] as string[] }, +}); + +// Object for testing factory defaults. +const listObj = restate.object({ + name: "list", + handlers: { + get: async (ctx: restate.ObjectSharedContext) => + execute( + ctx, + gen(function* () { + return { items: yield* listState.items.get() }; + }) + ), + + append: async (ctx: restate.ObjectContext, item: string): Promise => + execute( + ctx, + gen(function* () { + const current = yield* listState.items.get(); + listState.items.set([...current, item]); }) ), }, @@ -113,7 +142,7 @@ describe.each(modes)("state — $name mode", ({ alwaysReplay }) => { beforeAll(async () => { env = await RestateTestEnvironment.start({ - services: [counterObj], + services: [counterObj, listObj], alwaysReplay, }); ingress = clients.connect({ url: env.baseUrl() }); @@ -132,15 +161,18 @@ describe.each(modes)("state — $name mode", ({ alwaysReplay }) => { expect(await client.current()).toBe(10); }); + test("static default: count returns 0 for a fresh object", async () => { + const key = `fresh-${alwaysReplay ? "replay" : "default"}`; + expect(await ingress.objectClient(counterObj, key).current()).toBe(0); + }); + test("isolation: each VO key has independent state", async () => { const k1 = `iso1-${alwaysReplay ? "replay" : "default"}`; const k2 = `iso2-${alwaysReplay ? "replay" : "default"}`; - const c1 = ingress.objectClient(counterObj, k1); - const c2 = ingress.objectClient(counterObj, k2); - await c1.add(5); - await c2.add(11); - expect(await c1.current()).toBe(5); - expect(await c2.current()).toBe(11); + await ingress.objectClient(counterObj, k1).add(5); + await ingress.objectClient(counterObj, k2).add(11); + expect(await ingress.objectClient(counterObj, k1).current()).toBe(5); + expect(await ingress.objectClient(counterObj, k2).current()).toBe(11); }); test("keys(): lists all set keys, drops cleared ones", async () => { @@ -153,7 +185,7 @@ describe.each(modes)("state — $name mode", ({ alwaysReplay }) => { expect(await client.keys()).toEqual(["count"]); }); - test("clearAll: wipes all state for this object", async () => { + test("reset: clears all keys", async () => { const key = `clear-${alwaysReplay ? "replay" : "default"}`; const client = ingress.objectClient(counterObj, key); await client.add(42); @@ -163,4 +195,20 @@ describe.each(modes)("state — $name mode", ({ alwaysReplay }) => { expect(await client.keys()).toEqual([]); expect(await client.current()).toBe(0); }); + + test("factory default: fresh VO key starts with an empty array", async () => { + const key = `factory-${alwaysReplay ? "replay" : "default"}`; + const client = ingress.objectClient(listObj, key); + expect((await client.get()).items).toEqual([]); + await client.append("a"); + await client.append("b"); + expect((await client.get()).items).toEqual(["a", "b"]); + }); + + test("factory default: each VO key gets its own independent array", async () => { + const k1 = `factory-iso1-${alwaysReplay ? "replay" : "default"}`; + const k2 = `factory-iso2-${alwaysReplay ? "replay" : "default"}`; + await ingress.objectClient(listObj, k1).append("x"); + expect((await ingress.objectClient(listObj, k2).get()).items).toEqual([]); + }); }); diff --git a/packages/libs/restate-sdk-gen/examples/tutorial/src/07-state.ts b/packages/libs/restate-sdk-gen/examples/tutorial/src/07-state.ts index c60e701d..4d0096eb 100644 --- a/packages/libs/restate-sdk-gen/examples/tutorial/src/07-state.ts +++ b/packages/libs/restate-sdk-gen/examples/tutorial/src/07-state.ts @@ -11,38 +11,49 @@ // Tier 7: virtual-object state. // -// Maps to guide.md §"Working with state". A virtual object owns a -// per-key state slot the SDK persists between invocations. `state()` -// gives you the read-write store; `sharedState()` is the read-only -// view available from `ObjectSharedContext` handlers (concurrent reads -// while no writer holds the key). +// Two APIs: // -// Typed state: pass a shape (`{ counter: number }`) to get keyof-checked -// names and per-key value types. Without it, names are `string` and -// values are inferred per call (untyped, like the SDK's default). +// state(config) — per-key typed accessors. Each key in the config gets a +// .get() / .set() / .clear() accessor. Keys with a `default` return +// Future (never null); others return Future. +// +// state() — per-key accessors without config. All keys return +// Future. +// +// getState / setState / clearState / clearAllState / getAllStateKeys — +// flat untyped functions for dynamic key names or simple cases. import * as restate from "@restatedev/restate-sdk"; -import { gen, execute, state, sharedState } from "@restatedev/restate-sdk-gen"; +import { + gen, + execute, + state, + getState, + setState, + clearState, + getAllStateKeys, +} from "@restatedev/restate-sdk-gen"; -type CounterState = { - counter: number; -}; +// Description of the state fields used by counter +const counterState = state({ + counter: { default: 0 }, +}); export const counter = restate.object({ name: "counter", handlers: { - // Read-only handler: takes ObjectSharedContext, uses sharedState(). - // Multiple `get` invocations can run concurrently for the same key. + // Read-only handler: shared context, uses state() (same API — write + // methods throw at runtime in a shared context). get: async (ctx: restate.ObjectSharedContext): Promise => execute( ctx, gen(function* () { - return (yield* sharedState().get("counter")) ?? 0; + // Default is 0 — no null-coalescing needed. + return yield* counterState.counter.get(); }) ), - // Read-write handler: takes ObjectContext, uses state(). Exclusive - // access to the key for the duration of the invocation. + // Read-write handler: default applied, so no ?? needed. add: async ( ctx: restate.ObjectContext, addend: number @@ -50,21 +61,57 @@ export const counter = restate.object({ execute( ctx, gen(function* () { - const s = state(); - const oldValue = (yield* s.get("counter")) ?? 0; + const oldValue: number = yield* counterState.counter.get(); // Future const newValue = oldValue + addend; - s.set("counter", newValue); + counterState.counter.set(newValue); return { oldValue, newValue }; }) ), - // Clear the counter back to zero (well, deletes the entry; `get` - // returns 0 by falling through the `?? 0`). reset: async (ctx: restate.ObjectContext): Promise => execute( ctx, gen(function* () { - state().clear("counter"); + counterState.counter.clear(); + }) + ), + + // Flat untyped API — useful when key names are dynamic. + setRaw: async ( + ctx: restate.ObjectContext, + payload: { key: string; value: string } + ): Promise => + execute( + ctx, + gen(function* () { + setState(payload.key, payload.value); + }) + ), + + getRaw: async ( + ctx: restate.ObjectContext, + key: string + ): Promise => + execute( + ctx, + gen(function* () { + return yield* getState(key); + }) + ), + + clearRaw: async (ctx: restate.ObjectContext, key: string): Promise => + execute( + ctx, + gen(function* () { + clearState(key); + }) + ), + + keys: async (ctx: restate.ObjectSharedContext): Promise => + execute( + ctx, + gen(function* () { + return yield* getAllStateKeys(); }) ), }, diff --git a/packages/libs/restate-sdk-gen/examples/tutorial/src/08-clients.ts b/packages/libs/restate-sdk-gen/examples/tutorial/src/08-clients.ts index 5860f272..deb702b7 100644 --- a/packages/libs/restate-sdk-gen/examples/tutorial/src/08-clients.ts +++ b/packages/libs/restate-sdk-gen/examples/tutorial/src/08-clients.ts @@ -36,7 +36,6 @@ import { serviceSendClient, objectClient, state, - sharedState, } from "@restatedev/restate-sdk-gen"; import type { counter } from "./07-state.js"; @@ -70,7 +69,7 @@ export const greeter = restate.service({ // id often surfaces through an external system (webhook, queue) and // the holder VO is just a convenient stash. -type HolderState = { id: string }; +const holderState = state<{ id: string }>(); export const awakeableHolder = restate.object({ name: "awakeableHolder", @@ -79,7 +78,7 @@ export const awakeableHolder = restate.object({ execute( ctx, gen(function* () { - state().set("id", id); + holderState.id.set(id); }) ), @@ -90,12 +89,12 @@ export const awakeableHolder = restate.object({ execute( ctx, gen(function* () { - const id = yield* state().get("id"); + const id = yield* holderState.id.get(); if (!id) { throw new restate.TerminalError("no awakeable registered yet"); } resolveAwakeable(id, payload); - state().clear("id"); + holderState.id.clear(); }) ), @@ -105,7 +104,7 @@ export const awakeableHolder = restate.object({ execute( ctx, gen(function* () { - return (yield* sharedState().get("id")) ?? null; + return yield* holderState.id.get(); }) ), }, diff --git a/packages/libs/restate-sdk-gen/examples/tutorial/src/09-workflows.ts b/packages/libs/restate-sdk-gen/examples/tutorial/src/09-workflows.ts index fdec0cd3..bb190023 100644 --- a/packages/libs/restate-sdk-gen/examples/tutorial/src/09-workflows.ts +++ b/packages/libs/restate-sdk-gen/examples/tutorial/src/09-workflows.ts @@ -27,11 +27,10 @@ import { gen, execute, state, - sharedState, workflowPromise, } from "@restatedev/restate-sdk-gen"; -type WfState = { input: string }; +const wfState = state<{ input: string }>(); export const blockAndWaitWorkflow = restate.workflow({ name: "blockAndWait", @@ -42,7 +41,7 @@ export const blockAndWaitWorkflow = restate.workflow({ execute( ctx, gen(function* () { - state().set("input", input); + wfState.input.set(input); // Park until someone calls `unblock` on this workflow id. const output = workflowPromise("done"); @@ -83,7 +82,7 @@ export const blockAndWaitWorkflow = restate.workflow({ execute( ctx, gen(function* () { - return (yield* sharedState().get("input")) ?? null; + return yield* wfState.input.get(); }) ), }, diff --git a/packages/libs/restate-sdk-gen/src/free.ts b/packages/libs/restate-sdk-gen/src/free.ts index 6265a00b..bf583c17 100644 --- a/packages/libs/restate-sdk-gen/src/free.ts +++ b/packages/libs/restate-sdk-gen/src/free.ts @@ -43,7 +43,11 @@ import type * as restate from "@restatedev/restate-sdk"; import type { Future, FutureValues, FutureSettledResult } from "./future.js"; import type { Channel } from "./channel.js"; -import type { State, SharedState, TypedState, UntypedState } from "./state.js"; +import type { + AnyKeySpec, + StateAccessors, + UntypedStateAccessors, +} from "./state.js"; import type { FluentClient, FluentDurablePromise } from "./clients.js"; import type { RestateOperations, @@ -158,13 +162,80 @@ export const cancel = (invocationId: restate.InvocationId): void => export const channel = (): Channel => currentOps().channel(); -export const state = < - TState extends TypedState = UntypedState, ->(): State => currentOps().state(); +/** + * Per-key typed state accessor for virtual objects and workflows. + * Safe to call at module level — the Restate context is resolved lazily + * when `.get()`, `.set()`, or `.clear()` is first called inside a handler. + * + * @example Keys with defaults return a non-null value; use `typed()` for + * typed keys without defaults. + * ```ts + * const s = state({ + * count: { default: 0 }, // count.get() → Future + * items: { default: () => [] }, // factory default, fresh array each time + * label: typed(), // label.get() → Future + * }); + * + * // In a handler: + * const n: number = yield* s.count.get(); + * s.count.set(n + 1); + * s.count.clear(); + * ``` + * + * @example Without a config, pass an explicit type — all keys return nullable. + * ```ts + * const s = state<{ count: number; label: string }>(); + * const n = (yield* s.count.get()) ?? 0; // Future + * ``` + * + * When key names are only known at runtime, use instead: + * `getState`, `setState`, `clearState`, `clearAllState`, `getAllStateKeys`. + */ +export function state>( + config: TConfig +): StateAccessors; +export function state< + TShape extends Record, +>(): UntypedStateAccessors; +export function state( + config?: Record +): UntypedStateAccessors { + const capturedConfig = config; + return new Proxy({} as never, { + get(_target, prop: string) { + return { + get: (serde?: unknown) => + currentOps() + .stateKey(prop, capturedConfig?.[prop]) + .get(serde as never), + set: (value: unknown, serde?: unknown) => + currentOps() + .stateKey(prop, capturedConfig?.[prop]) + .set(value as never, serde as never), + clear: () => + currentOps().stateKey(prop, capturedConfig?.[prop]).clear(), + }; + }, + }); +} + +export const getState = ( + name: string, + serde?: restate.Serde +): Future => currentOps().getState(name, serde); + +export const setState = ( + name: string, + value: T, + serde?: restate.Serde +): void => currentOps().setState(name, value, serde); + +export const clearState = (name: string): void => currentOps().clearState(name); + +export const clearAllState = (): void => currentOps().clearAllState(); -export const sharedState = < - TState extends TypedState = UntypedState, ->(): SharedState => currentOps().sharedState(); +export const getAllStateKeys = (): Future => + currentOps().getAllStateKeys(); // ---- workflow durable promise ---- diff --git a/packages/libs/restate-sdk-gen/src/index.ts b/packages/libs/restate-sdk-gen/src/index.ts index 24e03073..68d5471b 100644 --- a/packages/libs/restate-sdk-gen/src/index.ts +++ b/packages/libs/restate-sdk-gen/src/index.ts @@ -21,8 +21,12 @@ export { awakeable, cancel, channel, + clearAllState, + clearState, genericCall, genericSend, + getAllStateKeys, + getState, objectClient, objectSendClient, race, @@ -31,7 +35,7 @@ export { run, serviceClient, serviceSendClient, - sharedState, + setState, signal, sleep, state, @@ -62,9 +66,14 @@ export { type RunOpts, wrapActionForCancellation, } from "./restate-operations.js"; -export type { - SharedState, - State, - TypedState, - UntypedState, +export { + typed, + type TypedNoDefault, + type StateKeySpec, + type AnyKeySpec, + type SpecValue, + type SpecHasDefault, + type StateKeyAccessor, + type StateAccessors, + type UntypedStateAccessors, } from "./state.js"; diff --git a/packages/libs/restate-sdk-gen/src/restate-operations.ts b/packages/libs/restate-sdk-gen/src/restate-operations.ts index 875af909..6af0207d 100644 --- a/packages/libs/restate-sdk-gen/src/restate-operations.ts +++ b/packages/libs/restate-sdk-gen/src/restate-operations.ts @@ -33,11 +33,18 @@ import { } from "./operation.js"; import type { Scheduler } from "./scheduler.js"; import { - type State, - type SharedState, - type TypedState, - type UntypedState, - makeState, + type AnyKeySpec, + type StateKeyAccessor, + type StateAccessors, + type UntypedStateAccessors, + makeStateFromConfig, + makeStateFromShape, + makeKeyAccessorFromSpec, + makeGetState, + makeSetState, + makeClearState, + makeClearAllState, + makeGetAllStateKeys, } from "./state.js"; import { type FluentClient, @@ -438,47 +445,95 @@ export class RestateOperations { // ---- state ---- /** - * Per-invocation read-write key-value store. Use from a handler whose - * underlying context is ObjectContext or WorkflowContext. + * Per-key typed state accessor. * - * The optional `TState` generic gives keyof-checked names and per-key - * value types: + * Pass a config object to get per-key accessors with optional defaults + * and serde. Keys with a default return `Future` (never null); others + * return `Future`. * - * ops.state<{count: number; user: User}>() - * // state.get("count") → Future + * const s = ops.state({ count: { default: 0 }, label: {} }); + * s.count.get() // Future + * s.label.get() // Future + * s.count.set(1) + * s.count.clear() * - * Without it, names are `string` and values are inferred per call: + * Pass an explicit shape generic with no config to get per-key accessors + * where all keys return nullable: * - * ops.state() - * // state.get("count") → Future - * - * Calling write methods from a shared (read-only) context throws at - * runtime — for shared handlers, use `sharedState()` below to get a - * narrower type that drops the write methods. + * const s = ops.state<{ count: number; label: string }>(); + * s.count.get() // Future */ - state(): State { - return makeState( + state>( + config: TConfig + ): StateAccessors; + state< + TShape extends Record, + >(): UntypedStateAccessors; + state< + TConfig extends Record, + TShape extends Record, + >(config?: TConfig): StateAccessors | UntypedStateAccessors { + if (config !== undefined) { + return makeStateFromConfig( + config, + this.ctx as unknown as restate.ObjectContext, + this.sched, + adapt + ); + } + return makeStateFromShape( this.ctx as unknown as restate.ObjectContext, this.sched, adapt ); } - /** - * Per-invocation read-only key-value store. Use from a handler whose - * underlying context is ObjectSharedContext or WorkflowSharedContext. - * - * Same `TState` generic as `state()`. Returns the read-only subset - * (`get`, `keys`); attempting to call writes is a type error. - */ - sharedState(): SharedState { - return makeState( - this.ctx as unknown as restate.ObjectSharedContext, + // Build a single key accessor from an optional spec — used by the lazy + // Proxy in free.ts so each method call creates exactly one accessor. + stateKey(name: string, spec?: AnyKeySpec): StateKeyAccessor { + return makeKeyAccessorFromSpec( + name, + spec, + this.ctx as unknown as restate.ObjectContext, + this.sched, + adapt + ); + } + + // ---- flat untyped state operations ---- + + getState(name: string, serde?: restate.Serde): Future { + return makeGetState( + this.ctx as unknown as restate.ObjectContext, this.sched, adapt + )(name, serde); + } + + setState(name: string, value: T, serde?: restate.Serde): void { + makeSetState(this.ctx as unknown as restate.ObjectContext)( + name, + value, + serde ); } + clearState(name: string): void { + makeClearState(this.ctx as unknown as restate.ObjectContext)(name); + } + + clearAllState(): void { + makeClearAllState(this.ctx as unknown as restate.ObjectContext)(); + } + + getAllStateKeys(): Future { + return makeGetAllStateKeys( + this.ctx as unknown as restate.ObjectContext, + this.sched, + adapt + )(); + } + // ---- combinators ---- /** diff --git a/packages/libs/restate-sdk-gen/src/state.ts b/packages/libs/restate-sdk-gen/src/state.ts index b66e2a2a..6a88d543 100644 --- a/packages/libs/restate-sdk-gen/src/state.ts +++ b/packages/libs/restate-sdk-gen/src/state.ts @@ -9,139 +9,290 @@ * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE */ -// State +// State API // ============================================================================= // -// Wrapper around Restate's KeyValueStore — the per-key state attached -// to a virtual object or workflow invocation. Available via -// `ops.state()` (read-write) or `ops.sharedState()` (read-only) from -// within `execute()`. +// Two surfaces: // -// Read-only vs read-write is a real distinction: -// - ObjectContext / WorkflowContext expose the full KeyValueStore -// (get + keys + set + clear + clearAll). -// - ObjectSharedContext / WorkflowSharedContext expose only the -// reads. Calling `set` etc. on the underlying context throws at -// runtime; we surface that at the type level by giving shared -// handlers a narrower interface. +// 1. Per-key typed accessor — `state(config)` / `state()` +// Returns an object whose properties are per-key accessor objects, +// each with `.get()`, `.set()`, `.clear()`. The return type of `.get()` +// is baked in at accessor-creation time, so TypeScript evaluates it +// eagerly rather than deferring a conditional over a free type variable. // -// Both interfaces are generic over `TState extends TypedState`: -// - Default `UntypedState` keeps the loose `(name: string)` shape -// and a per-call value type — same as the SDK's untyped mode. -// - Pass an explicit shape to get keyof-checked names and per-key -// value types: `ops.state<{count: number; user: User}>()` then -// `state.get("count")` infers `Future`. +// 2. Flat untyped functions — `getState / setState / clearState / +// clearAllState / getAllStateKeys`. Used internally and re-exported +// from free.ts for call sites that don't need per-key typing (e.g. +// dynamic key names). +// +// Read-only vs read-write is a runtime distinction (ObjectSharedContext vs +// ObjectContext). The type system does not enforce it here — callers in a +// shared context get the same accessor shape; the SDK throws at runtime if +// they call writes. import type * as restate from "@restatedev/restate-sdk"; import type { Awaitable } from "./awaitable.js"; import type { Future } from "./future.js"; import type { Scheduler } from "./scheduler.js"; -/** - * Marker types matching the SDK's typed-state convention. Pass a - * concrete shape (e.g. `{count: number; name: string}`) to enable - * keyof-checked names; leave the default to keep names as `string` - * with a per-call value generic. - */ -export type TypedState = Record; -export type UntypedState = { _: never }; - -/** - * Read-only state, for handlers running under an - * ObjectSharedContext or WorkflowSharedContext. - */ -export interface SharedState { - /** Read a state value. Returns null if the key isn't set. */ - get( - name: TState extends UntypedState ? string : TKey, - serde?: restate.Serde - ): Future<(TState extends UntypedState ? TValue : TState[TKey]) | null>; - - /** List all currently-known state keys. */ - keys(): Future; -} +// --------------------------------------------------------------------------- +// Public types +// --------------------------------------------------------------------------- /** - * Read-write state, for handlers running under an ObjectContext or - * WorkflowContext. Extends SharedState with mutation methods. + * Marker for a key that is typed but has no default. Use when you want + * keyof-checked names and per-key value types but the key should return + * `Future` (no default substitution). + * + * Create with `typed()` and include in the state config: * - * Writes are synchronous in the SDK — the journal entry is recorded - * immediately, no yield required — so `set` / `clear` / `clearAll` - * return `void` rather than `Operation`. + * state({ count: { default: 0 }, label: typed() }) + * // count.get() → Future + * // label.get() → Future */ -export interface State< - TState extends TypedState = UntypedState, -> extends SharedState { - /** Write a state value. Sync; journal entry recorded immediately. */ - set( - name: TState extends UntypedState ? string : TKey, - value: TState extends UntypedState ? TValue : TState[TKey], - serde?: restate.Serde - ): void; - - /** Clear a single key. */ - clear( - name: TState extends UntypedState ? string : TKey - ): void; - - /** Clear all state for this invocation. */ - clearAll(): void; +export type TypedNoDefault = { readonly _noDefaultType: T }; + +/** Create a typed-but-no-default marker for use in a state config. */ +export function typed(): TypedNoDefault { + return {} as TypedNoDefault; } -// Loose type covering every Restate context flavor that exposes any -// state methods. We only call KeyValueStore methods on this; the rest -// of Context is unused here. +/** Per-key configuration passed to state(). */ +export type StateKeySpec = { + /** + * Default value (or factory) substituted when the store returns null. + * Use a factory `() => value` for mutable defaults (e.g. arrays/objects) + * so each invocation gets a fresh copy. + */ + default?: T | (() => T); + /** Custom serde for this key. */ + serde?: restate.Serde; +}; + +/** Any valid per-key spec: either a StateKeySpec or a TypedNoDefault marker. */ +export type AnyKeySpec = StateKeySpec | TypedNoDefault; + +/** Per-key read-write accessor returned by state(). */ +export type StateKeyAccessor = { + /** + * Read the key. Returns the stored value, or the default if set, or null. + * When the key has a default, the return type is `Future` (never null). + */ + get(serde?: restate.Serde): Future; + /** Write the key. Synchronous; journal entry recorded immediately. */ + set(value: T, serde?: restate.Serde): void; + /** Delete the key. */ + clear(): void; +}; + +// Extract value type from an AnyKeySpec. +// TypedNoDefault → T; factory default → T; static default → T; serde → T; else unknown. +export type SpecValue = + S extends TypedNoDefault + ? T + : S extends { default: (...args: never[]) => infer D } + ? D + : S extends { default: infer D } + ? D + : S extends { serde: restate.Serde } + ? D + : unknown; + +// Whether a spec carries a runtime default (TypedNoDefault never does). +export type SpecHasDefault = + S extends TypedNoDefault + ? false + : S extends { default: unknown } + ? true + : false; + +/** Typed accessor map produced by state(config). */ +export type StateAccessors> = { + [K in keyof TConfig]: StateKeyAccessor< + SpecValue, + SpecHasDefault + >; +}; + +/** Accessor map produced by state() — all keys nullable. */ +export type UntypedStateAccessors> = { + [K in keyof TShape]: StateKeyAccessor; +}; + +// --------------------------------------------------------------------------- +// Context type +// --------------------------------------------------------------------------- + type StateContext = | restate.ObjectContext | restate.ObjectSharedContext | restate.WorkflowContext | restate.WorkflowSharedContext; +// --------------------------------------------------------------------------- +// Runtime implementation +// --------------------------------------------------------------------------- + /** - * Build a `State` over the given context. The runtime delegates - * straight to `ctx.get` / `ctx.set` / etc.; the TState generic is purely - * a TS-level convenience and gets erased at runtime. - * - * For shared (read-only) contexts, the returned State has the same - * runtime shape but the caller should use the `SharedState` - * type to drop the write methods. The convenience method - * `RestateOperations.sharedState()` does this cast. + * Build one per-key accessor. `rawDefault` is either a static value or a + * factory function — we normalize it to a factory (or undefined) here so + * each `.get()` call that returns null produces a fresh default (important + * for mutable defaults like arrays/objects). */ -export function makeState( +function makeKeyAccessor( + name: string, ctx: StateContext, sched: Scheduler, - adapt: (p: restate.RestatePromise) => Awaitable -): State { - // Cast for write methods: the typed shared variants don't have them - // on the type, but the runtime object does carry them in writable - // contexts. Calling write() from a shared handler throws naturally. + adapt: (p: restate.RestatePromise) => Awaitable, + rawDefault: T | (() => T) | undefined, + specSerde: restate.Serde | undefined +): StateKeyAccessor { const writeCtx = ctx as restate.ObjectContext; + // Normalize default to a factory (or undefined when no default). + const defaultFactory: (() => T) | undefined = + rawDefault === undefined + ? undefined + : typeof rawDefault === "function" + ? (rawDefault as () => T) + : () => rawDefault as T; - // Internally we use the loose UntypedState signatures and cast the - // resulting object to the narrower State. Runtime is identical; - // the conditional types only matter at the call site. - const impl: State = { - get(name: string, serde?: restate.Serde): Future { - return sched.makeJournalFuture( - adapt( - ctx.get(name, serde) as unknown as restate.RestatePromise - ) + return { + get(callSerde?: restate.Serde): Future { + const serde = callSerde ?? specSerde; + const p = adapt( + ctx.get(name, serde) as unknown as restate.RestatePromise ); + if (defaultFactory !== undefined) { + const factory = defaultFactory; + return sched.makeJournalFuture( + p.map((v, e) => { + if (e !== undefined) throw e as Error; + return v != null ? v : factory(); + }) + ); + } + return sched.makeJournalFuture(p); }, - keys(): Future { - return sched.makeJournalFuture( - adapt(ctx.stateKeys() as unknown as restate.RestatePromise) - ); - }, - set(name: string, value: T, serde?: restate.Serde): void { - writeCtx.set(name, value, serde); + set(value: T, callSerde?: restate.Serde): void { + writeCtx.set(name, value, callSerde ?? specSerde); }, - clear(name: string): void { + clear(): void { writeCtx.clear(name); }, - clearAll(): void { - writeCtx.clearAll(); - }, }; - return impl as unknown as State; +} + +/** + * Build a typed accessor map from a config object. + * Each key in the config gets its own accessor with the configured default + * and serde. + */ +export function makeStateFromConfig>( + config: TConfig, + ctx: StateContext, + sched: Scheduler, + adapt: (p: restate.RestatePromise) => Awaitable +): StateAccessors { + const result: Record> = {}; + for (const key of Object.keys(config)) { + const spec = config[key] ?? {}; + // TypedNoDefault markers have no default or serde; treat as plain key. + const s: StateKeySpec = "_noDefaultType" in spec ? {} : spec; + result[key] = makeKeyAccessor(key, ctx, sched, adapt, s.default, s.serde); + } + return result as unknown as StateAccessors; +} + +/** + * Build a single per-key accessor, given an optional spec. + * Used by the lazy free-standing state() Proxy so each method call + * creates exactly one accessor rather than the full map. + */ +export function makeKeyAccessorFromSpec( + name: string, + spec: AnyKeySpec | undefined, + ctx: StateContext, + sched: Scheduler, + adapt: (p: restate.RestatePromise) => Awaitable +): StateKeyAccessor { + // TypedNoDefault markers carry no runtime info — treat as plain key. + const s = + spec && "_noDefaultType" in spec + ? {} + : (spec as StateKeySpec | undefined); + return makeKeyAccessor(name, ctx, sched, adapt, s?.default, s?.serde); +} + +/** + * Build an untyped accessor map for state() calls (no config). + * Returns a Proxy that creates per-key accessors on demand — since TShape + * is erased at runtime we can't enumerate keys ahead of time. + */ +export function makeStateFromShape>( + ctx: StateContext, + sched: Scheduler, + adapt: (p: restate.RestatePromise) => Awaitable +): UntypedStateAccessors { + const cache: Record> = {}; + return new Proxy({} as UntypedStateAccessors, { + get(_target, prop: string) { + if (!(prop in cache)) { + cache[prop] = makeKeyAccessor( + prop, + ctx, + sched, + adapt, + undefined, + undefined + ); + } + return cache[prop]; + }, + }); +} + +// --------------------------------------------------------------------------- +// Flat untyped state operations (used by RestateOperations directly) +// --------------------------------------------------------------------------- + +export function makeGetState( + ctx: StateContext, + sched: Scheduler, + adapt: (p: restate.RestatePromise) => Awaitable +): (name: string, serde?: restate.Serde) => Future { + return (name: string, serde?: restate.Serde) => + sched.makeJournalFuture( + adapt( + ctx.get(name, serde) as unknown as restate.RestatePromise + ) + ); +} + +export function makeSetState( + ctx: StateContext +): (name: string, value: T, serde?: restate.Serde) => void { + const writeCtx = ctx as restate.ObjectContext; + return (name: string, value: T, serde?: restate.Serde) => + writeCtx.set(name, value, serde); +} + +export function makeClearState(ctx: StateContext): (name: string) => void { + const writeCtx = ctx as restate.ObjectContext; + return (name: string) => writeCtx.clear(name); +} + +export function makeClearAllState(ctx: StateContext): () => void { + const writeCtx = ctx as restate.ObjectContext; + return () => writeCtx.clearAll(); +} + +export function makeGetAllStateKeys( + ctx: StateContext, + sched: Scheduler, + adapt: (p: restate.RestatePromise) => Awaitable +): () => Future { + return () => + sched.makeJournalFuture( + adapt(ctx.stateKeys() as unknown as restate.RestatePromise) + ); } diff --git a/packages/libs/restate-sdk-gen/test-services/src/awakeable-holder.ts b/packages/libs/restate-sdk-gen/test-services/src/awakeable-holder.ts index 9e9a73a0..14ba18e7 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/awakeable-holder.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/awakeable-holder.ts @@ -19,13 +19,10 @@ import { gen, execute, state, - sharedState, resolveAwakeable, } from "@restatedev/restate-sdk-gen"; -type HolderState = { - id: string; -}; +const holderState = state<{ id: string }>(); export const awakeableHolder = restate.object({ name: "AwakeableHolder", @@ -34,7 +31,7 @@ export const awakeableHolder = restate.object({ execute( ctx, gen(function* () { - state().set("id", id); + holderState.id.set(id); }) ), @@ -42,8 +39,7 @@ export const awakeableHolder = restate.object({ execute( ctx, gen(function* () { - const id = yield* sharedState().get("id"); - return id != null; + return (yield* holderState.id.get()) != null; }) ), @@ -54,7 +50,7 @@ export const awakeableHolder = restate.object({ execute( ctx, gen(function* () { - const id = yield* state().get("id"); + const id = yield* holderState.id.get(); if (id == null) { throw new restate.TerminalError("No awakeable is registered"); } diff --git a/packages/libs/restate-sdk-gen/test-services/src/block-and-wait-workflow.ts b/packages/libs/restate-sdk-gen/test-services/src/block-and-wait-workflow.ts index bd2f3cde..f399ee79 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/block-and-wait-workflow.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/block-and-wait-workflow.ts @@ -18,11 +18,10 @@ import { gen, execute, state, - sharedState, workflowPromise, } from "@restatedev/restate-sdk-gen"; -type WfState = { "my-state": string }; +const wfState = state<{ "my-state": string }>(); export const blockAndWaitWorkflow = restate.workflow({ name: "BlockAndWaitWorkflow", @@ -31,7 +30,7 @@ export const blockAndWaitWorkflow = restate.workflow({ execute( ctx, gen(function* () { - state().set("my-state", input); + wfState["my-state"].set(input); const output = workflowPromise("durable-promise"); const value = yield* output.get(); @@ -63,7 +62,7 @@ export const blockAndWaitWorkflow = restate.workflow({ execute( ctx, gen(function* () { - return (yield* sharedState().get("my-state")) ?? null; + return (yield* wfState["my-state"].get()) ?? null; }) ), }, diff --git a/packages/libs/restate-sdk-gen/test-services/src/cancel-test.ts b/packages/libs/restate-sdk-gen/test-services/src/cancel-test.ts index 1951b217..e2405cce 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/cancel-test.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/cancel-test.ts @@ -19,7 +19,6 @@ import { gen, execute, state, - sharedState, objectClient, awakeable, sleep, @@ -30,7 +29,7 @@ const AwakeableHolderApi: restate.VirtualObjectDefinitionFrom< typeof awakeableHolder > = { name: "AwakeableHolder" }; -type RunnerState = { state: boolean }; +const runnerState = state<{ state: boolean }>(); export const cancelTestRunner = restate.object({ name: "CancelTestRunner", @@ -43,7 +42,7 @@ export const cancelTestRunner = restate.object({ yield* objectClient(CancelTestBlockingApi, ctx.key).block(op); } catch (e) { if (e instanceof restate.TerminalError && e.code === 409) { - state().set("state", true); + runnerState.state.set(true); return; } throw e; @@ -55,8 +54,7 @@ export const cancelTestRunner = restate.object({ execute( ctx, gen(function* () { - const v = yield* sharedState().get("state"); - return v === true; + return (yield* runnerState.state.get()) === true; }) ), }, diff --git a/packages/libs/restate-sdk-gen/test-services/src/counter.ts b/packages/libs/restate-sdk-gen/test-services/src/counter.ts index cd0bafea..6e429632 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/counter.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/counter.ts @@ -10,16 +10,14 @@ */ // Counter — virtual object exercising basic state I/O. -// Written in the free-standing style: handler bodies call `state()` / -// `sharedState()` directly, no `ops` parameter. +// Written in the free-standing style: handler bodies call `state()` +// directly, no `ops` parameter. // Mirrors sdk-ruby/test-services/services/counter.rb. import * as restate from "@restatedev/restate-sdk"; -import { gen, execute, state, sharedState } from "@restatedev/restate-sdk-gen"; +import { gen, execute, state } from "@restatedev/restate-sdk-gen"; -type CounterState = { - counter: number; -}; +const counterState = state({ counter: { default: 0 } }); export const counterObject = restate.object({ name: "Counter", @@ -28,7 +26,7 @@ export const counterObject = restate.object({ execute( ctx, gen(function* () { - state().clear("counter"); + counterState.counter.clear(); }) ), @@ -36,7 +34,7 @@ export const counterObject = restate.object({ execute( ctx, gen(function* () { - return (yield* sharedState().get("counter")) ?? 0; + return yield* counterState.counter.get(); }) ), @@ -47,10 +45,9 @@ export const counterObject = restate.object({ execute( ctx, gen(function* () { - const s = state(); - const oldValue = (yield* s.get("counter")) ?? 0; + const oldValue: number = yield* counterState.counter.get(); const newValue = oldValue + addend; - s.set("counter", newValue); + counterState.counter.set(newValue); return { oldValue, newValue }; }) ), @@ -62,9 +59,8 @@ export const counterObject = restate.object({ execute( ctx, gen(function* () { - const s = state(); - const oldValue = (yield* s.get("counter")) ?? 0; - s.set("counter", oldValue + addend); + const oldValue: number = yield* counterState.counter.get(); + counterState.counter.set(oldValue + addend); throw new restate.TerminalError(ctx.key); }) ), diff --git a/packages/libs/restate-sdk-gen/test-services/src/interpreter.ts b/packages/libs/restate-sdk-gen/test-services/src/interpreter.ts index 4b2add2c..0e563b8d 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/interpreter.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/interpreter.ts @@ -19,8 +19,9 @@ import { type Future, gen, execute, - state, - sharedState, + getState, + setState, + clearState, awakeable, resolveAwakeable, rejectAwakeable, @@ -220,17 +221,17 @@ function makeInterpretHandler(layer: 0 | 1 | 2) { ): Generator { switch (cmd.kind) { case SET_STATE: - state().set(`key-${cmd.key}`, `value-${cmd.key}`); + setState(`key-${cmd.key}`, `value-${cmd.key}`); return; case GET_STATE: - yield* state().get(`key-${cmd.key}`); + yield* getState(`key-${cmd.key}`); return; case CLEAR_STATE: - state().clear(`key-${cmd.key}`); + clearState(`key-${cmd.key}`); return; case INCREMENT_STATE_COUNTER: { - const c = (yield* state().get("counter")) ?? 0; - state().set("counter", c + 1); + const c = (yield* getState("counter")) ?? 0; + setState("counter", c + 1); return; } case SLEEP: @@ -404,7 +405,7 @@ const sharedCounter = restate.handlers.object.shared( execute( ctx, gen(function* () { - return (yield* sharedState().get("counter")) ?? 0; + return (yield* getState("counter")) ?? 0; }) ) ); diff --git a/packages/libs/restate-sdk-gen/test-services/src/list-object.ts b/packages/libs/restate-sdk-gen/test-services/src/list-object.ts index 87287086..a9684160 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/list-object.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/list-object.ts @@ -13,11 +13,9 @@ // Mirrors sdk-ruby/test-services/services/list_object.rb. import * as restate from "@restatedev/restate-sdk"; -import { gen, execute, state, sharedState } from "@restatedev/restate-sdk-gen"; +import { gen, execute, state } from "@restatedev/restate-sdk-gen"; -type ListState = { - list: string[]; -}; +const listState = state({ list: { default: [] as string[] } }); export const listObject = restate.object({ name: "ListObject", @@ -26,9 +24,8 @@ export const listObject = restate.object({ execute( ctx, gen(function* () { - const s = state(); - const list = (yield* s.get("list")) ?? []; - s.set("list", [...list, value]); + const list = yield* listState.list.get(); + listState.list.set([...list, value]); }) ), @@ -36,7 +33,7 @@ export const listObject = restate.object({ execute( ctx, gen(function* () { - return (yield* sharedState().get("list")) ?? []; + return yield* listState.list.get(); }) ), @@ -44,9 +41,8 @@ export const listObject = restate.object({ execute( ctx, gen(function* () { - const s = state(); - const result = (yield* s.get("list")) ?? []; - s.clear("list"); + const result: string[] = yield* listState.list.get(); + listState.list.clear(); return result; }) ), diff --git a/packages/libs/restate-sdk-gen/test-services/src/map-object.ts b/packages/libs/restate-sdk-gen/test-services/src/map-object.ts index 0018af40..16de22fd 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/map-object.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/map-object.ts @@ -13,7 +13,14 @@ // Mirrors sdk-ruby/test-services/services/map_object.rb. import * as restate from "@restatedev/restate-sdk"; -import { gen, execute, state, sharedState } from "@restatedev/restate-sdk-gen"; +import { + gen, + execute, + getState, + setState, + clearState, + getAllStateKeys, +} from "@restatedev/restate-sdk-gen"; type Entry = { key: string; value: string }; @@ -24,7 +31,7 @@ export const mapObject = restate.object({ execute( ctx, gen(function* () { - state().set(entry.key, entry.value); + setState(entry.key, entry.value); }) ), @@ -35,8 +42,7 @@ export const mapObject = restate.object({ execute( ctx, gen(function* () { - const v = yield* sharedState().get(key); - return v ?? ""; + return (yield* getState(key)) ?? ""; }) ), @@ -44,13 +50,12 @@ export const mapObject = restate.object({ execute( ctx, gen(function* () { - const s = state(); - const keys = yield* s.keys(); + const keys = yield* getAllStateKeys(); const entries: Entry[] = []; for (const key of keys) { - const value = (yield* s.get(key)) ?? ""; + const value = (yield* getState(key)) ?? ""; entries.push({ key, value }); - s.clear(key); + clearState(key); } return entries; }) diff --git a/packages/libs/restate-sdk-gen/test-services/src/non-determinism.ts b/packages/libs/restate-sdk-gen/test-services/src/non-determinism.ts index 4479031f..468eff2e 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/non-determinism.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/non-determinism.ts @@ -18,7 +18,7 @@ import * as restate from "@restatedev/restate-sdk"; import { gen, execute, - state, + setState, sleep, objectClient, objectSendClient, @@ -45,9 +45,9 @@ export const nonDeterministic = restate.object({ ctx, gen(function* () { if (doLeftAction(ctx.key)) { - state().set("a", "my-state"); + setState("a", "my-state"); } else { - state().set("b", "my-state"); + setState("b", "my-state"); } yield* sleep(100); objectSendClient(CounterApi, ctx.key).add(1); diff --git a/packages/libs/restate-sdk-gen/test-services/src/vo-command-interpreter.ts b/packages/libs/restate-sdk-gen/test-services/src/vo-command-interpreter.ts index 9b0bf8d9..6891cf83 100644 --- a/packages/libs/restate-sdk-gen/test-services/src/vo-command-interpreter.ts +++ b/packages/libs/restate-sdk-gen/test-services/src/vo-command-interpreter.ts @@ -19,8 +19,8 @@ import { gen, execute, select, - state, - sharedState, + getState, + setState, awakeable, sleep, run, @@ -68,11 +68,6 @@ type RunThrowTerminalSub = { }; type SubCommand = CreateAwakeableSub | SleepSub | RunThrowTerminalSub; -type State = { - results: string[]; - [k: `awk-${string}`]: string; -}; - type SubFutureKind = "awakeable" | "sleep" | "run"; type SubEntry = { kind: SubFutureKind; future: Future }; @@ -84,7 +79,7 @@ export const virtualObjectCommandInterpreter = restate.object({ execute( ctx, gen(function* () { - return (yield* sharedState().get("results")) ?? []; + return (yield* getState("results")) ?? []; }) ) ), @@ -97,7 +92,7 @@ export const virtualObjectCommandInterpreter = restate.object({ execute( ctx, gen(function* () { - const id = yield* sharedState().get(`awk-${awakeableKey}`); + const id = yield* getState(`awk-${awakeableKey}`); return id != null; }) ) @@ -111,9 +106,7 @@ export const virtualObjectCommandInterpreter = restate.object({ execute( ctx, gen(function* () { - const id = yield* sharedState().get( - `awk-${req.awakeableKey}` - ); + const id = yield* getState(`awk-${req.awakeableKey}`); if (!id) { throw new restate.TerminalError("No awakeable is registered"); } @@ -130,9 +123,7 @@ export const virtualObjectCommandInterpreter = restate.object({ execute( ctx, gen(function* () { - const id = yield* sharedState().get( - `awk-${req.awakeableKey}` - ); + const id = yield* getState(`awk-${req.awakeableKey}`); if (!id) { throw new restate.TerminalError("No awakeable is registered"); } @@ -156,7 +147,7 @@ export const virtualObjectCommandInterpreter = restate.object({ switch (cmd.type) { case "createAwakeable": { const { id, promise } = awakeable(); - state().set(`awk-${cmd.awakeableKey}`, id); + setState(`awk-${cmd.awakeableKey}`, id); return { kind: "awakeable", future: promise }; } case "sleep": @@ -190,7 +181,7 @@ export const virtualObjectCommandInterpreter = restate.object({ switch (cmd.type) { case "awaitAwakeableOrTimeout": { const { id, promise } = awakeable(); - state().set(`awk-${cmd.awakeableKey}`, id); + setState(`awk-${cmd.awakeableKey}`, id); const sleepFuture = sleep(cmd.timeoutMillis); const r = yield* select({ awk: promise, @@ -205,7 +196,7 @@ export const virtualObjectCommandInterpreter = restate.object({ break; } case "resolveAwakeable": { - const id = yield* state().get(`awk-${cmd.awakeableKey}`); + const id = yield* getState(`awk-${cmd.awakeableKey}`); if (!id) { throw new restate.TerminalError("No awakeable is registered"); } @@ -214,7 +205,7 @@ export const virtualObjectCommandInterpreter = restate.object({ break; } case "rejectAwakeable": { - const id = yield* state().get(`awk-${cmd.awakeableKey}`); + const id = yield* getState(`awk-${cmd.awakeableKey}`); if (!id) { throw new restate.TerminalError("No awakeable is registered"); } @@ -281,8 +272,8 @@ export const virtualObjectCommandInterpreter = restate.object({ } } - const last = (yield* state().get("results")) ?? []; - state().set("results", [...last, result]); + const last = (yield* getState("results")) ?? []; + setState("results", [...last, result]); } return result; diff --git a/packages/libs/restate-sdk-gen/test/abort-signal.test.ts b/packages/libs/restate-sdk-gen/test/abort-signal.test.ts index b3be712e..5f9892a9 100644 --- a/packages/libs/restate-sdk-gen/test/abort-signal.test.ts +++ b/packages/libs/restate-sdk-gen/test/abort-signal.test.ts @@ -36,13 +36,8 @@ // aborted signal. import { describe, expect, test } from "vitest"; -import { - gen, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { cancellingLib, deferred } from "./test-promise.js"; class CancelError extends Error { diff --git a/packages/libs/restate-sdk-gen/test/adversarial.test.ts b/packages/libs/restate-sdk-gen/test/adversarial.test.ts index 4fba406b..88024894 100644 --- a/packages/libs/restate-sdk-gen/test/adversarial.test.ts +++ b/packages/libs/restate-sdk-gen/test/adversarial.test.ts @@ -14,15 +14,8 @@ // asking "what would break if X happened in order Y?" import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("adversarial — spawn-and-immediately-await ordering", () => { diff --git a/packages/libs/restate-sdk-gen/test/all-settled.test.ts b/packages/libs/restate-sdk-gen/test/all-settled.test.ts index 0743608f..7292b2f9 100644 --- a/packages/libs/restate-sdk-gen/test/all-settled.test.ts +++ b/packages/libs/restate-sdk-gen/test/all-settled.test.ts @@ -17,9 +17,7 @@ import { type FutureSettledResult, type FutureRejectedResult, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, rejected, testLib } from "./test-promise.js"; describe("allSettled — journal sources (fast path)", () => { diff --git a/packages/libs/restate-sdk-gen/test/all.test.ts b/packages/libs/restate-sdk-gen/test/all.test.ts index 1163bdda..de764dd1 100644 --- a/packages/libs/restate-sdk-gen/test/all.test.ts +++ b/packages/libs/restate-sdk-gen/test/all.test.ts @@ -10,14 +10,8 @@ */ import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, rejected, testLib } from "./test-promise.js"; describe("all — journal sources (fast path)", () => { diff --git a/packages/libs/restate-sdk-gen/test/any.test.ts b/packages/libs/restate-sdk-gen/test/any.test.ts index 55a1f7c9..b447e766 100644 --- a/packages/libs/restate-sdk-gen/test/any.test.ts +++ b/packages/libs/restate-sdk-gen/test/any.test.ts @@ -10,14 +10,8 @@ */ import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, rejected, testLib } from "./test-promise.js"; describe("any — journal sources (fast path)", () => { diff --git a/packages/libs/restate-sdk-gen/test/basic.test.ts b/packages/libs/restate-sdk-gen/test/basic.test.ts index c1ec1dd4..2c3e94cd 100644 --- a/packages/libs/restate-sdk-gen/test/basic.test.ts +++ b/packages/libs/restate-sdk-gen/test/basic.test.ts @@ -10,13 +10,8 @@ */ import { describe, expect, test } from "vitest"; -import { - gen, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { testLib } from "./test-promise.js"; describe("gen — basics", () => { diff --git a/packages/libs/restate-sdk-gen/test/cancellation.test.ts b/packages/libs/restate-sdk-gen/test/cancellation.test.ts index 205692b0..0ce36ddb 100644 --- a/packages/libs/restate-sdk-gen/test/cancellation.test.ts +++ b/packages/libs/restate-sdk-gen/test/cancellation.test.ts @@ -27,15 +27,8 @@ // (constructed after cancellation is delivered) are unaffected. import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { cancellingLib, deferred } from "./test-promise.js"; // Stand-in for restate.TerminalError with the cancellation code. The diff --git a/packages/libs/restate-sdk-gen/test/channel.test.ts b/packages/libs/restate-sdk-gen/test/channel.test.ts index 976298b3..61b110ae 100644 --- a/packages/libs/restate-sdk-gen/test/channel.test.ts +++ b/packages/libs/restate-sdk-gen/test/channel.test.ts @@ -33,9 +33,7 @@ import { type Future, type Channel, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { testLib, deferred } from "./test-promise.js"; // Helper: an Operation that yields the send (firing it). Spawn this diff --git a/packages/libs/restate-sdk-gen/test/cross-routine.test.ts b/packages/libs/restate-sdk-gen/test/cross-routine.test.ts index 27283b9f..930ef71f 100644 --- a/packages/libs/restate-sdk-gen/test/cross-routine.test.ts +++ b/packages/libs/restate-sdk-gen/test/cross-routine.test.ts @@ -15,15 +15,8 @@ // that show up in real workflows. import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, testLib } from "./test-promise.js"; describe("cross-routine — one signals another", () => { diff --git a/packages/libs/restate-sdk-gen/test/edge-cases.test.ts b/packages/libs/restate-sdk-gen/test/edge-cases.test.ts index a83b89df..987c17bc 100644 --- a/packages/libs/restate-sdk-gen/test/edge-cases.test.ts +++ b/packages/libs/restate-sdk-gen/test/edge-cases.test.ts @@ -16,15 +16,8 @@ // - rejection propagation through deep combinator trees import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("edge — error propagation through synthesized combinator bodies", () => { diff --git a/packages/libs/restate-sdk-gen/test/future.test.ts b/packages/libs/restate-sdk-gen/test/future.test.ts index eab83a86..a70143cd 100644 --- a/packages/libs/restate-sdk-gen/test/future.test.ts +++ b/packages/libs/restate-sdk-gen/test/future.test.ts @@ -10,14 +10,8 @@ */ import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, rejected, testLib } from "./test-promise.js"; describe("Future — journal-backed", () => { diff --git a/packages/libs/restate-sdk-gen/test/mixed-deep.test.ts b/packages/libs/restate-sdk-gen/test/mixed-deep.test.ts index 9904cb9f..0230676d 100644 --- a/packages/libs/restate-sdk-gen/test/mixed-deep.test.ts +++ b/packages/libs/restate-sdk-gen/test/mixed-deep.test.ts @@ -29,9 +29,7 @@ import { type Future, type Operation, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; // ----------------------------------------------------------------------------- diff --git a/packages/libs/restate-sdk-gen/test/mixing-select.test.ts b/packages/libs/restate-sdk-gen/test/mixing-select.test.ts index 6bb61bca..355286ac 100644 --- a/packages/libs/restate-sdk-gen/test/mixing-select.test.ts +++ b/packages/libs/restate-sdk-gen/test/mixing-select.test.ts @@ -28,9 +28,7 @@ import { type Future, type Operation, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("select — mixing journal and routine branches", () => { diff --git a/packages/libs/restate-sdk-gen/test/mixing.test.ts b/packages/libs/restate-sdk-gen/test/mixing.test.ts index 0e9959de..5b5d4ed5 100644 --- a/packages/libs/restate-sdk-gen/test/mixing.test.ts +++ b/packages/libs/restate-sdk-gen/test/mixing.test.ts @@ -26,9 +26,7 @@ import { type Future, type Operation, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("mixing — flat journal+routine inputs", () => { diff --git a/packages/libs/restate-sdk-gen/test/observability.test.ts b/packages/libs/restate-sdk-gen/test/observability.test.ts index 2dc1ee02..37483aec 100644 --- a/packages/libs/restate-sdk-gen/test/observability.test.ts +++ b/packages/libs/restate-sdk-gen/test/observability.test.ts @@ -26,9 +26,7 @@ import { type Future, type Operation, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; // A trace recorder. Each entry tags its origin so tests can assert on the diff --git a/packages/libs/restate-sdk-gen/test/race.test.ts b/packages/libs/restate-sdk-gen/test/race.test.ts index 943a632a..dca09170 100644 --- a/packages/libs/restate-sdk-gen/test/race.test.ts +++ b/packages/libs/restate-sdk-gen/test/race.test.ts @@ -10,14 +10,8 @@ */ import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("race — journal sources (fast path)", () => { diff --git a/packages/libs/restate-sdk-gen/test/run-wrap.test.ts b/packages/libs/restate-sdk-gen/test/run-wrap.test.ts index 65b6640e..b62289c6 100644 --- a/packages/libs/restate-sdk-gen/test/run-wrap.test.ts +++ b/packages/libs/restate-sdk-gen/test/run-wrap.test.ts @@ -67,9 +67,7 @@ describe("wrapActionForCancellation — success path", () => { try { // Simulate an abort-aware operation. await new Promise((res, rej) => { - signal.addEventListener("abort", () => - rej(abortError("aborted")) - ); + signal.addEventListener("abort", () => rej(abortError("aborted"))); }); return "never"; } catch { @@ -203,9 +201,7 @@ describe("wrapActionForCancellation — reason swallowing scenarios", () => { const wrapped = wrapActionForCancellation(signal, async ({ signal }) => { try { await new Promise((_res, rej) => { - signal.addEventListener("abort", () => - rej(abortError("aborted")) - ); + signal.addEventListener("abort", () => rej(abortError("aborted"))); }); return "never"; } catch (e) { @@ -225,9 +221,7 @@ describe("wrapActionForCancellation — reason swallowing scenarios", () => { const wrapped = wrapActionForCancellation(signal, async ({ signal }) => { try { await new Promise((_res, rej) => { - signal.addEventListener("abort", () => - rej(abortError("aborted")) - ); + signal.addEventListener("abort", () => rej(abortError("aborted"))); }); return "never"; } catch { diff --git a/packages/libs/restate-sdk-gen/test/scheduler.test.ts b/packages/libs/restate-sdk-gen/test/scheduler.test.ts index cf6c9aef..81f0186a 100644 --- a/packages/libs/restate-sdk-gen/test/scheduler.test.ts +++ b/packages/libs/restate-sdk-gen/test/scheduler.test.ts @@ -15,15 +15,8 @@ // behavior under simultaneous settles, and edge cases in dispatch. import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("scheduler — deadlock detection", () => { diff --git a/packages/libs/restate-sdk-gen/test/select.test.ts b/packages/libs/restate-sdk-gen/test/select.test.ts index e536557e..7018efe2 100644 --- a/packages/libs/restate-sdk-gen/test/select.test.ts +++ b/packages/libs/restate-sdk-gen/test/select.test.ts @@ -17,9 +17,7 @@ import { type Future, type Operation, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, rejected, resolved, testLib } from "./test-promise.js"; describe("select — basics", () => { diff --git a/packages/libs/restate-sdk-gen/test/spawn.test.ts b/packages/libs/restate-sdk-gen/test/spawn.test.ts index 3930af76..a9d20a9b 100644 --- a/packages/libs/restate-sdk-gen/test/spawn.test.ts +++ b/packages/libs/restate-sdk-gen/test/spawn.test.ts @@ -10,15 +10,8 @@ */ import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("spawn — concurrency", () => { diff --git a/packages/libs/restate-sdk-gen/test/stress.test.ts b/packages/libs/restate-sdk-gen/test/stress.test.ts index 386c3ce8..4fa9f8d1 100644 --- a/packages/libs/restate-sdk-gen/test/stress.test.ts +++ b/packages/libs/restate-sdk-gen/test/stress.test.ts @@ -17,15 +17,8 @@ // - leaks in the waiter-list bookkeeping import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("stress — many concurrent spawns", () => { diff --git a/packages/libs/restate-sdk-gen/test/won-flag.test.ts b/packages/libs/restate-sdk-gen/test/won-flag.test.ts index 6ff1067d..ddf4e7da 100644 --- a/packages/libs/restate-sdk-gen/test/won-flag.test.ts +++ b/packages/libs/restate-sdk-gen/test/won-flag.test.ts @@ -25,15 +25,8 @@ // 3. AwaitAny over the same future referenced multiple times. import { describe, expect, test } from "vitest"; -import { - gen, - spawn, - type Future, - type Operation, -} from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { gen, spawn, type Future, type Operation } from "../src/index.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("won-flag — multiple journal sources ready at once", () => { diff --git a/packages/libs/restate-sdk-gen/test/workflow-patterns.test.ts b/packages/libs/restate-sdk-gen/test/workflow-patterns.test.ts index 53694860..3890c7da 100644 --- a/packages/libs/restate-sdk-gen/test/workflow-patterns.test.ts +++ b/packages/libs/restate-sdk-gen/test/workflow-patterns.test.ts @@ -25,9 +25,7 @@ import { type Future, type Operation, } from "../src/index.js"; -import { - Scheduler, -} from "../src/internal.js"; +import { Scheduler } from "../src/internal.js"; import { deferred, resolved, testLib } from "./test-promise.js"; describe("workflow-patterns — retry with bounded attempts", () => {