feat: serverless typescript sdk - #4949
Draft
abelanger5 wants to merge 64 commits into
Draft
abelanger5 wants to merge 64 commits into
abelanger5 wants to merge 64 commits into
Conversation
`computeMemoKey` now hashes with `crypto.subtle.digest('SHA-256', ...)`
instead of Node's `createHash`, so `DurableContext` no longer needs the
`crypto` builtin. The function is async and exported; `memo()` (the only
caller, behind `now()`) awaits it. The digest covers the same bytes as
before (task run id followed by the JSON-serialised dependencies), which
the new unit test proves against the old implementation, so durable event
logs recorded by earlier SDKs keep replaying.
Runtimes without `globalThis.crypto` get a clear HatchetError naming the
Node 20 requirement.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
`Context` and `DurableContext` no longer reach into `HatchetClient` and `InternalWorker`. They depend on two seams defined in the new, Node-free `v1/client/worker/runtime.ts`: - `ContextRuntime`: a logger factory, the namespace, and the engine-facing operations a context performs (cancelRun/cancelBatch, putLog, refreshTimeout, releaseSlot, putStream, runWorkflow(s)) plus the worker facts `ctx.worker` reports (workerId, hasWorkflow, workerLabels, upsertWorkerLabels). - `DurableTransport`: the six listener members a durable task talks to (sendEvent, waitForCallback, consumeCallbackWithoutBlocking, sendMemoCompletedNotification, cleanupTaskState, sendEvictInvocation). The worker supplies adapters over its client and durable listener (`worker-runtime.ts`); the existing `(action, client, worker[, listener, ...])` constructors remain as overloads and build the adapter, so the worker's call sites and tests are unchanged. `ctx.v1` is still set when a context comes from a worker. A `DurableContext` on a transport without the legacy unary RPCs gets a clear error instead of a crash when the engine predates eviction. To keep the context module free of Node imports on the way: - `Action`, `ActionKey`, `createAction`, `workflowNameFromAction` and a new `createActionId` move to the pure `clients/dispatcher/action.ts`; `action-listener.ts` re-exports them. - The durable ack/event shapes move to `durable-events.ts`; the listener client re-exports them. - `bindAbortSignalHandler` (the only `events` user) moves from `util/abort-error` to `util/abort-signal`. - `ParentRunContextManager` gets a pluggable store; the default store has no context, and the worker installs the `AsyncLocalStorage` store at module load through `parent-run-context-storage.ts`, so worker semantics are unchanged while declaration code no longer imports `async_hooks`. - `util/logger` and `dispatcher-client` import their types from leaf modules instead of the `v1` barrel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
`registerWorkflow` built the `CreateWorkflowVersionRequest` inline and
called `putWorkflow` in the same breath. The request construction now
lives in the pure `v1/client/worker/workflow-proto.ts`:
- `normalizeWorkflowDefinition(definition, { namespace, durable })`
applies the namespace, lowercases the name and appends the on-success
task as a regular task with the DAG leaves as parents. It returns a new
definition (the declaration is no longer mutated) and is idempotent.
- `workflowToProto(definition, { namespace, durable })` accepts a
declaration or a definition and returns the request. The mapping helpers
(`mapWorkerLabelPb`, `mapSlotRequestsPb`, `mapRateLimitPb`,
`mapConcurrencyPb`, `mapBatchConfigPb`, `mapStickyStrategyPb`,
`taskConcurrencyArr`, `resolveExecutionTimeout`,
`resolveScheduleTimeout`, `onFailureTaskName`, `getLeaves`) are
exported from it; `worker-internal.ts` re-exports the ones it exported
before.
The worker normalizes once, registers through `workflowToProto`, and keys
its registries by the same normalized definition.
Action ids are unified on one canonical form, `createActionId`:
`<workflow>:<task>` fully lowercased. Registration previously kept the
task name's case while the worker's action registries lowercased it; the
engine lowercases on insert (`workflows.sql`) and the Go SDK sends
lowercase, so the wire form is unchanged for the engine and mixed-case
task names now register the id the worker actually serves. A unit test
registers a workflow with mixed-case task names through a stubbed
`putWorkflow` and checks the registered ids equal the registry keys.
The fixture test covers standalone, durable, DAG with parents and
conditions, on-failure and on-success in both forms, event and cron
triggers with a namespace, input validator to JSON schema, concurrency
(array and single), rate limits, sticky, worker labels, default filters,
idempotency and task defaults, and checks that `toJSON` yields protojson
(lowerCamelCase keys, base64 bytes) that round-trips.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Adds `@hatchet-dev/typescript-sdk/edge` (`src/edge/index.ts`): the
declaration factories and classes with their option types, `task.ts`,
`types.ts`, durations, conditions, `applyNamespace`, `createAction`,
`createActionId`, `workflowNameFromAction`, `Context`, `DurableContext`,
the `ContextRuntime` and `DurableTransport` interfaces with their event
types, `HatchetError`, `NonDeterminismError`, `TaskRunTerminatedError`,
the abort helpers, eviction policy types, `workflowToProto` and the wire
types it needs (`CreateWorkflowVersionRequest`, `AssignedAction`,
`DurableTaskRequest`, `DurableTaskResponse`). `declarations()` returns
`{ task, durableTask, workflow, batchTask }` bound to no client, mirroring
the `HatchetClient` factories.
`scripts/check-edge-entry.mjs` bundles `dist/edge/index.js` with esbuild
(`platform: 'browser'`, `conditions: ['workerd', 'worker', 'browser']`)
and fails on any `node:` specifier or Node builtin reached transitively.
The two agent SDKs are external: `mcpTool()` requires them lazily and they
are optional peers. The entry currently bundles 40 SDK modules and only
`@bufbuild/protobuf` and `zod`. `pnpm check:edge` builds and runs it; the
typescript workflow's lint job runs it after the type check.
package.json gains an `exports` map written for the published layout
(publishing happens from inside `dist`): explicit entries for `.`,
`./edge`, `./v1`, `./v1/embedded`, `./package.json` and every directory
that has an `index.ts` (Node stops resolving directory indexes once
`exports` exists), plus `./*.js` and `./*` passthroughs so deep file
imports such as `util/sleep` or `protoc/v1/workflows` keep working.
`main` and `types` now point at `index.js` / `index.d.ts`; the previous
`dist/index.d.ts` did not exist in the published package. A unit test
keeps the map in step with the `index.ts` directories.
Verified by `pnpm pack` from `dist` and installing the tarball into a
scratch project: CJS `require` and ESM `import` of the root, `/edge`,
`/v1`, `/v1/embedded`, `/protoc/v1/workflows`, `/protoc/dispatcher`,
`/util/sleep`, `/util/sleep.js`, `/v1/conditions`, `/util/logger` and
`/package.json` all resolve, and `tsc` resolves the types under `node16`,
`bundler` and `node10`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Bumps the TypeScript SDK to 1.32.0, adds the changelog entry for the edge entry point, the exports map, `workflowToProto`, the runtime interfaces, the action id casing change and the WebCrypto memo keys, syncs it into the docs changelog, and regenerates the TypeScript reference docs (the `Context` page picks up the runtime constructors). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
`@hatchet-dev/typescript-sdk/edge` resolves the same way every other subpath of the package does: the package publishes from inside `dist`, so `edge/index.js` and `edge/index.d.ts` sit at the published root and Node, TypeScript and bundlers find them by file layout, exactly as they find `v1/embedded`, `protoc/v1/workflows` or `util/sleep`. `package.json` carries no `exports` map and no `main`; `types` is `index.d.ts`, which is the file that exists at the published root. Consumer behaviour, checked against the packed tarball: CJS `require` resolves the root, `/edge`, `/v1`, `/v1/embedded`, `/protoc/dispatcher`, `/protoc/v1/workflows` and `/util/sleep`; `tsc` resolves their types under `node10`, `node16` (CommonJS) and `bundler`. Native ESM needs the full file path (`/edge/index.js`, `/v1/embedded.js`), which is the rule for every subpath of this package. `scripts/check-edge-entry.mjs` and `pnpm check:edge` still enforce that the edge entry reaches no Node builtin. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
…langer/serverless-ts-core # Conflicts: # go.mod
Turns sdks/typescript-serverless from a generation target into a buildable package: tsup produces ESM and CJS with declarations for the root, ./cloudflare and ./testing entries, the exports map lists types first for each, and the runtime entries are checked by scripts/check-edge-entry.mjs, which bundles them for a workerd-like target and fails on any Node builtin or any SDK module outside the edge entry. The package depends on @hatchet-dev/typescript-sdk 1.32 for declarations only. 1.32 is unreleased, so development uses file:../typescript/dist, a copy of the SDK's published layout with only its runtime dependencies installed. A link: to the same directory would resolve the SDK's devDependencies too, and its lazily required agent SDKs then drag Node builtins into a Workers bundle; the copy behaves like a real install. task generate-serverless-proto builds the SDK dist before installing for the same reason. The ts-proto options drop the service stubs: the package never speaks gRPC and the stubs referenced nice-grpc types it does not depend on. Vitest runs the unit tests; eslint and prettier mirror the SDK's config. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
createHandler turns declarations into the endpoint the serverless operator polls and invokes: POST <basePath>/healthcheck answers the registration (every workflow through the SDK's workflowToProto, passed through protojson into the package's generated types, plus the served action ids, durable.supported false and the runtime), and POST <basePath>/trigger verifies the HMAC-SHA256 signature over the raw body, decodes ServerlessTriggerRequest with the generated fromJSON, strips the namespace from the action id and workflow name, runs the task under a Context and maps the outcome the way delivery.go reads it: 200 with the output, 204 for undefined, 422 retry false for NonRetryableError and ServerlessLimitationError, 500 retry true for anything else, 404 for an action not served here, 401 for a bad signature and 426 for a durable upgrade, which the next phase serves. ServerlessRuntime is the ContextRuntime behind that Context: logging goes to the console, every engine-facing call throws ServerlessLimitationError naming the feature, workerId is undefined and hasWorkflow answers from the declarations. Declaration options that need a worker (slotCost, slotRequests, batch, desiredWorkerLabels, sticky, evictionPolicy) are removed from the registration with one warning each; the declaration object itself is left untouched. The root entry exports hatchet, the SDK's declaration factories bound to no client, together with the handler, the signature helpers (the upgrade verification is already there for the relay) and the SDK types a task author needs. Nothing in it imports from Node or reads process.env. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
@hatchet-dev/serverless/cloudflare exports cloudflare(options), which returns an ExportedHandler<Env>: requests under basePath go to the generic handler with the secret resolved per call from env.HATCHET_SIGNING_SECRET (or the secret option) and waitUntil wired to ctx.waitUntil for the relay to come; every other path goes to the user's fetch option or gets a 404. Typed against @cloudflare/workers-types with no runtime import from Cloudflare. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
@hatchet-dev/serverless/testing invokes a handler the way the operator would, without Hatchet: createTestOperator signs each request with the endpoint secret, namespaces the action id and workflow name with a UUID prefix, POSTs a ServerlessTriggerRequest built with the generated bindings, and classifies the response with the rules of delivery.go. invoke returns the output or throws InvocationFailedError; deliver returns the classification; request sends a raw signed request; healthcheck decodes the body with the generated fromJSON and fails when the body is not canonical protojson. invokeDurable throws the limitation error until the relay lands. The unit tests cover the healthcheck shape and the served subset, the warnings for ignored options, every row of the outcome table, namespace stripping, the Context accessors and the Cloudflare adapter's routing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The README opens with the one hard limitation: no Hatchet client and no token in the serverless runtime, why, and exactly which ctx members throw. Then the usage from the plan (declarations, the Cloudflare adapter, registration through the REST endpoint until hatchet serverless register exists, tests without Hatchet), the outcome table and the development steps. LIMITATIONS.md carries the full list, the ignored declaration options and the serve caveat before namespaces span endpoints. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The example drops its hand-written contract shim (src/hatchet.ts) for
@hatchet-dev/serverless: src/tasks.ts declares echo with hatchet.task and
keeps sleep-then-echo as a durable declaration that the healthcheck
advertises but this version cannot serve, and src/index.ts is
export default cloudflare({ workflows }). The package is linked from the
repository until it is published; the README explains the build order,
the new echo input ({"message": ...}) and that the durable task waits
for the relay phase.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
A serverless job in the TypeScript workflow builds the SDK dist the package depends on, installs the package, runs lint, the type check, the unit tests and check:edge, then type-checks the Cloudflare example and bundles it with wrangler deploy --dry-run so a Node builtin sneaking into the runtime entries fails the build the way a real deploy would. The workflow also triggers on the package and example paths. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
FrameTransport implements the SDK's DurableTransport over a DurableSocket:
sendEvent builds the same DurableTaskRequest the worker's
DurableListenerClient builds for memo, wait_for and trigger_runs, sends it
as a request frame with a sequence id and resolves the matching ack;
waitForCallback resolves on the entry_completed for its (branchId, nodeId)
in delivery order, with the SDK's ordered-completion rule; complete_memo
goes out without an ack; evict_invocation resolves on eviction_ack. One
ack-bearing request is in flight at a time, the rest queue locally, so a
fanned-out task never trips the operator's 4006. Error frames reject
everything pending with NonDeterminismError for code nondeterminism and
a plain Error otherwise.
The inline wait budget is measured from the ack that created an entry;
when an awaited completion outlives it the invocation evicts: evict_invocation,
its ack, done { status: evicted } as the last frame, then the task is aborted
with TaskRunTerminatedError so user code unwinds without touching the
socket. A server_evict aborts the same way without the round trip and
sends nothing more, since the operator closes with 4001.
runDurableInvocation is the runner: wait for the first frame (bounded),
strip the namespace, look the durable task up (a non-durable action over
the socket is done { error, retry: false }), build a DurableContext on the
ServerlessRuntime and the transport with the minimum engine version that
enables eviction, run the function under the parent run context, and
finish with done { output } or done { error, retry } where retry is false
for NonRetryableError and after an engine error frame. The socket is
closed with 1000 after the done frame; an operator close aborts the task
and sends nothing.
createHandler verifies the upgrade signature (with the optional nonce set)
before handing the socket to the adapter's upgrade hook, still answers 426
without one, and the healthcheck advertises durable support only for a
handler whose adapter supplies the hook and that has a durable task.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The Cloudflare adapter supplies the upgrade hook: WebSocketPair, accept the server side, run the invocation under ctx.waitUntil so it outlives the 101, and answer with the client side. cloudflareSocket adapts the Workers WebSocket to the relay's DurableSocket (text frames only, one close notification). The adapter declares durable support, so the healthcheck advertises it for Workers that declare a durable task. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The test operator dials durable tasks the way the operator does: a signed upgrade through the handler's DurableHooks.upgrade seam, an in-memory socket pair, the first frame, and the frame protocol with a durable event log per task run. Memo entries replay with memoAlreadyExisted, wait entries complete when the virtual clock passes a sleep or an emitted event matches, trigger_runs children run the registered task function through the trigger route, and a replay whose events diverge from the log gets a nondeterminism error frame. invokeDurable returns the outcome with every frame that crossed the socket; startDurable exposes the invocation for mid-flight server evictions, error frames and closes; resume re-invokes against the same log with the next invocation count. The fake enforces the operator's rules so a misbehaving endpoint fails a test: one ack-bearing request in flight (4006), nothing after done (4005), done evicted only after an eviction ack (4005), JSON output only. Tests cover the frame sequences for an inline sleep, an eviction and its resume with the memoized value, waitForEvent, spawnChild, server eviction, engine error frames, a diverging replay, an operator close mid-task, the failure mapping, a non-durable action over the socket, the upgrade signature checks and the healthcheck's durable flag, with fake timers and a check that no timer survives a case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The durable task in the Cloudflare example is now served over the relay: its output carries the invocation count so a run shows the eviction and the resumed invocation, and the README describes the expected result and the ws:// development switch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The README and LIMITATIONS drop the "not served yet" durable limitation: durable tasks run on the websocket the operator dials, ctx.now, sleepFor, sleepUntil, waitFor, waitForEvent, spawnChild and spawnChildren work over the relay, and the inline wait budget is the only eviction rule. The README gains the durable declaration from the plan, the test operator's invokeDurable and resume flow, and the done-frame outcomes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Durable memo keys are computed with WebCrypto, which is a global from Node 20. The engines field lets package managers warn on older runtimes at install time instead of failing on the first memoized call. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
…to belanger/serverless-ts-core # Conflicts: # examples/serverless/cloudflare-workers/README.md # examples/serverless/cloudflare-workers/src/hatchet.ts # examples/serverless/cloudflare-workers/src/index.ts
…langer/serverless-ts-core
The contract gained freshness rules the package did not enforce. Signed POST bodies (healthcheck and trigger) are now verified in the contract's order: the HMAC over the raw body, the body as JSON, its timestamp (protojson int64 as a decimal string, or a number) within REQUEST_MAX_AGE_SECONDS of the handler's clock in either direction (401, retry false), then the endpoint id when one is configured (403). The upgrade check uses the same either-direction window and consumes the nonce only after the signature verified, from a bounded in-memory NonceSet by default (4096 entries, expiring with the window, per isolate) that the seenNonce option still replaces. The first durable frame must name the task id and invocation the upgrade headers were signed for; a mismatch closes the socket with 1008 before any task code runs and sends no done frame. The test operator sends fresh healthcheck-shaped bodies for raw requests, accepts timestamp and endpoint id overrides on deliver, and can rewrite the first frame; the tests cover stale and future timestamps on both routes, both boundaries of the window, the 403 on a foreign endpoint id, the replayed and unsigned-then-signed nonce cases, NonceSet expiry and capacity, and the 1008 close. The lockfile picks up the merged SDK's dependency list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Ports the operator branch's "What the Worker verifies" section into the package README, rewritten for the package: the POST checks and the idempotency key, the upgrade checks and the per-isolate nonce set with the production advice, and the first-frame identity check. The example README points at it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The package publishes under the alpha dist-tag with public access. The publish script rebuilds the SDK it links, then lints, tests and builds before publishing, so a release never ships without the checks CI runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
…e policy The lockfile is resolved with pnpm's minimum release age at one day, so installs that enforce that policy accept every entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
….yaml pnpm 10 runs dependency install scripts only for allowlisted packages and, under strict-dep-builds, fails on any package left undecided. The workspace file allows esbuild and grpc-tools, which need their binaries, and ignores the SDK's postinstall and protobufjs, which need nothing at install time; it also carries the one day minimum release age. esbuild follows tsup's range so one copy is installed, and the SDK build step installs without strict-dep-builds since building its dist needs no install scripts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Workflows and actions travel under their declared names: the package no longer strips a namespace from action ids, workflow names or event keys, and the test operator sends plain names. The signed envelopes carry timestamp_unix_seconds, and the healthcheck parses that field for the freshness window. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
The tasks are registered under their declared names, the healthcheck envelope carries timestampUnixSeconds, and the README documents a run against a local engine: the tenant entitlement, the signing secret in .dev.vars, a public https tunnel in front of wrangler dev (the API's endpoint URL check accepts https on 443 only), endpoint registration over REST, and triggering runs over REST or the SDK with tls_strategy none. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
4 tasks done
`@hatchet-dev/serverless/vercel` exports `vercel({ workflows })`, which returns
the `{ GET, POST }` handlers for a Next.js App Router catch-all route: POST is
the healthcheck and non-durable trigger, GET accepts the durable websocket
through `experimental_upgradeWebSocket` from `@vercel/functions` and keeps the
relay alive with `waitUntil`. The route is read from the catch-all segment so
the route file can live anywhere. `vercelServer({ workflows })` is the same
endpoint as an `http.Server` for a standalone `api/*.ts` function.
`@hatchet-dev/serverless/node` exports `nodeHandler({ workflows })`, an
`http.Server` request listener that doubles as Express middleware and carries
an `upgrade` listener backed by `ws`, and `createServer({ workflows })` with
both wired.
Both adapters read the secret from `process.env.HATCHET_SIGNING_SECRET`, treat
`ws` and `@vercel/functions` as optional peers, and advertise
`durable.supported: false` when those cannot load, so the operator never dials
an endpoint that cannot accept the socket. The edge check keeps covering only
the runtime-agnostic entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
Adds the Vercel and Next.js section (route handlers, the waitUntil choice over after(), how an unavailable upgrade is answered, Vercel's limits, the standalone vercelServer shape), the Node section (createServer, nodeHandler as an Express middleware with the upgrade listener) and an Adapters table across the three runtimes. The Vercel route context is typed the way Next.js checks it (params as a promise), which next build enforces. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
A Next.js 15 App Router app serving echo and sleep-then-echo (the Cloudflare example's tasks, same names and outputs) through app/api/hatchet/[...hatchet]/route.ts with the Vercel adapter, and triggering them from a server action and a curl route with the SDK's core client. The README covers the local engine run, the tunnel the operator needs, endpoint registration as GENERIC_HTTP, vc dev versus next dev for the websocket path, maxDuration and the deploy to Vercel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
…v/serverless.
The handler takes `client` (a HatchetCore, its config, or a function of the
runtime's env returning either) and builds the SDK's core client from it on
first use, one per handler or per env object. With it, `ctx.runNoWaitChild`,
`ctx.bulkRunNoWaitChildren`, `ctx.putStream`, `ctx.cancel` and `ctx.log`
delegate to unary calls; without it they keep throwing, now worded "configure
`client` to enable this". The token comes from the platform's secret store and
is never sent by the operator.
Awaiting a child (`ctx.runChild`, `ctx.bulkRunChildren`, `result()` on a
reference made inside a task) goes over the invocation websocket: a stream
multiplexer sends stream_open/stream_message/stream_close frames with
endpoint-chosen ids, mirrors the operator's allowed procedures and its
16-stream cap, and closes every stream before the done frame or fails them
when the socket goes away. A run watcher keeps every child of a task on one
/Dispatcher/SubscribeToWorkflowRuns stream and resolves each waiter by run id;
an empty terminal event falls back to GetRunDetails. A task gets a socket only
when the catalog asks for one, so the handler takes `streams` (declarations or
action ids) and advertises them as `tasks: [{ action, streams: true }]`; the
invocation loop runs a non-durable task on the socket under a plain Context as
invocation 1. Over a POST the wait throws and names the option; inside a
durable task `ctx.runChild` throws and names `ctx.spawnChild`.
The test operator answers run subscriptions from a scripted engine
(`completeRun`, `runStream.workflowRun`), routes flagged tasks over the socket
the way the operator does, refuses durable requests on a non-durable socket and
exposes `closeStream`/`openStreams` for mid-flight tests. The Cloudflare adapter
reads HATCHET_CLIENT_TOKEN by default. README and LIMITATIONS.md describe the
rules; the package is 0.1.0-alpha.2 and needs the SDK's /core entry
(1.34.0-alpha.2).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
`parent-echo` triggers `echo` through the Worker's client and awaits it with
`ctx.runChild`; `src/index.ts` lists it in `streams` and builds the client from
HATCHET_CLIENT_TOKEN, with HATCHET_CLIENT_SERVER_URL and `tls: { strategy:
'none' }` for a local engine over plain HTTP. The README covers both secrets,
the .dev.vars for a local run and what the outputs and errors look like.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
# Conflicts: # sdks/typescript-serverless/README.md # sdks/typescript-serverless/package.json # sdks/typescript-serverless/scripts/check-edge-entry.mjs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gXd9VgU8FwoN3DijfprEK
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(WIP)
Description
Fixes # (issue)
Type of change
Checklist
Changes have been:
🤖 AI Disclosure