diff --git a/.gitignore b/.gitignore index 208222e..a8f6ed3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,9 @@ coverage/ # (run by its `test` / `typecheck` scripts). Ignored at the root so a checkout # predating the package cannot leave it visible and sweepable into a commit. **/src/generated/prisma/ + +# The 64 MB Temporal time-skipping test server, downloaded on a cold cache by +# the Temporal example's suite. Pinned here rather than left in the OS temp +# directory so it survives across runs and is a stable path CI can cache. +# See examples/order-temporal/README.md. +.cache/ diff --git a/CLAUDE.md b/CLAUDE.md index 2bc13c8..521162a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,10 +20,13 @@ throws to callers: every fallible operation returns an [`unthrown`](https://github.com/btravstack/unthrown) `Result`. pnpm workspace + turbo monorepo. `packages/start` is the single published -package; `examples/` holds five private ones — a clean-architecture application -(`order-domain` → `order-application` → `order-infrastructure`) booted under two -different runtimes (`order-api`, `order-worker`). They are consumers, not -fixtures: they are part of the gate, and `examples/README.md` is their index. +package; `examples/` holds eight private ones — a clean-architecture application +(`order-domain` → `order-application` → `order-infrastructure`) booted under +three different runtimes (`order-api`, `order-worker`, `order-temporal`), with +each transport's contract in a package of its own (`order-api-contract`, +`order-temporal-contract`) because a client must be able to take a contract +without the server. They are consumers, not fixtures: they are part of the gate, +and `examples/README.md` is their index. ## Commands @@ -63,11 +66,22 @@ hook). User-facing changes need a changeset. question of how two runtimes in one process share a drain deadline, or whose failure takes the process down. `StartOptions.runtime` is therefore a single value, not an array, and no future option should make it plural. - `examples/order-api` and `examples/order-worker` make this testable rather - than asserted: the same `ApplicationModule` + `PersistenceModule` - composition under two runtimes, with the same `DuplicateOrder` arriving as a - typed `CONFLICT` on one and as a dead-letter on the other — and neither - mapping anywhere near the kernel. + `examples/order-api`, `examples/order-worker` and `examples/order-temporal` + make this testable rather than asserted: the same `ApplicationModule` + + `PersistenceModule` composition under three runtimes, with the same + `DuplicateOrder` arriving as a typed `CONFLICT` on the first, a dead-letter + on the second and a `nonRetryable` typed contract error on the third — and + no mapping anywhere near the kernel. The third is also where + `Serving.drain` first meets a transport with real drain semantics of its + own: `worker.shutdown()` stops polling immediately and `run()` resolves only + once the in-flight activity has finished, so `drain` is a genuine wait + rather than the "stop accepting, nothing left to await" the other two are. + It is also the first runtime that has to **honour** the deadline + `AbortSignal` rather than merely note it: `run()` settles on Temporal's own + `shutdownForceTime`, so an activity that never finishes would hold + `Serving.stop` well past the kernel's `drainTimeoutMs` unless the signal is + raced against it — and `@temporalio/worker` exposes no public forced + shutdown to escalate to, so "stop waiting" is the escalation. 2. **Ambient carries DATA. The DI `Context` carries CAPABILITIES.** The kernel opens one `AsyncLocalStorage` store per unit holding a small, fixed record — @@ -567,13 +581,34 @@ Source layout (`packages/start/src/`), one concept per file: `ambient.ts` ## Toolchain & conventions -- **`examples/` is part of the gate, not a folder of illustrations.** All five - workspaces run under the same six commands as the kernel — 59 specs plus two - `needs-gate.test-d.ts` files — so an example that stops compiling, stops - linting or stops passing fails CI exactly as `packages/start` would. They are - also the only place a runtime with a **non-empty `needs`** meets a real - module, which is what exercises `start`'s phantom rest-tuple gate and +- **`examples/` is part of the gate, not a folder of illustrations.** All eight + workspaces run under the same six commands as the kernel — 82 specs plus + three `needs-gate.test-d.ts` files and three `layering.test-d.ts` ones — so an + example that stops compiling, stops linting or stops passing fails CI exactly + as `packages/start` would. + They are also the only place a runtime with a **non-empty `needs`** meets a + real module, which is what exercises `start`'s phantom rest-tuple gate and `RuntimeHost`'s `Context>` end to end. +- **`examples/order-temporal` is the one workspace whose suite needs the + network, and only on a cold cache.** It runs a real `@temporalio/worker` + Worker against `@temporalio/testing`'s **time-skipping test server** — a + 64 MB local binary, not a container — so the whole Workflow-Task / + Activity-Task loop is exercised with **no Docker daemon**, which is the + objection that kept a real Temporal cluster out. The binary is fetched once, + keyed by the `@temporalio` SDK version, into + **`/.cache/temporal-test-server`** (gitignored) with **`ttl: "365d"`, + set in `src/test-fixtures.ts`. Both are deliberate: the SDK's defaults are + the OS temp directory — which CI wipes between jobs and macOS purges on its + own schedule — and a one-day ttl, so a developer running the suite twice in a + week downloads it twice. A cold cache with no network fails loudly at + `createTimeSkipping()`, naming the URL. Measured with Docker quit: **7.4 s + cold** (download included), **3.8–3.9 s warm** — the slowest package in the + repo and still under four seconds. **CI does not yet cache that directory**: + `.github/workflows/ci.yml` delegates wholly to + `btravstack/config`'s `ci-reusable.yml@workflows-v1`, and a caller cannot + inject an `actions/cache` step into a reusable workflow's jobs. Closing it + means adding a cache-path input there, not here; until then every test job + pays the ~3.5 s download. - **The Prisma client is generated at test time, and there is nothing to install.** `@btravstack/start-example-order-infrastructure`'s `test` and `typecheck` scripts both begin with `prisma generate`, writing a gitignored @@ -586,6 +621,14 @@ Source layout (`packages/start/src/`), one concept per file: `ambient.ts` the **1.x** line, while `@unthrown/orpc` peers on `^2.0.0-beta`: an unpinned range resolves 1.x and fails `strictPeerDependencies`. The exact beta is the contract until v2 goes stable; raise it deliberately, not on a bot bump. +- **`temporal-contract` is pinned to an exact beta, for the same shape of + reason.** `@temporal-contract/{client,contract,testing,worker}` sit at + `8.0.0-beta.5` because the `latest` dist-tag is the **7.x** line, which peers + on `unthrown@^4` while this repo pins 5.2.0 — and 7.x ships neither the + `test-rig` nor the `workflow-bundle` subpath the Temporal example's specs are + built on. `testcontainers` is an **optional** peer of + `@temporal-contract/testing` and is deliberately not installed: the + Docker-free path is not merely unused here, it is unresolvable. - **Runtime dependencies: none.** `unthrown` and `@btravstack/di` are **peer** dependencies — the dual-copy hazard is real for both (di's port identity and unthrown's `isResult` each compare across copies). `node:` builtins only @@ -717,9 +760,23 @@ A sixth rule is about production code that tests keep honest: issues are the modeled `E`, folded by the entry point into a message and a non-zero exit code. A schema's own `.parse()` **throws**, which `unthrown/no-throw` bans and which would contradict the example it appears in. - The schema reads **strings** rather than `z.coerce.number()`: coercion is - `Number()` underneath, so `PORT=abc` binds `NaN` and `PORT=` binds the - ephemeral port `0` — the exact silent failure the module exists to remove. + A numeric variable is a **non-empty string piped into a coercion** — + `z.string().trim().min(1).pipe(z.coerce.number().int().min(min).max(max)).default(f)` + — never a bare `z.coerce.number()`: coercion is `Number()` underneath, so + `PORT=abc` binds `NaN` and `PORT=` binds the ephemeral port `0` — the exact + silent failure the module exists to remove. The bounds catch the first (and + `3.5`, and out-of-range); they cannot catch the second, because a **port's + `min` is `0`** so that an ephemeral bind stays expressible, which is why the + string guard is not optional. An empty or whitespace-only value is a + configuration **error**, not an absent one — `.default(...)` applies only + when the variable is genuinely missing. Each of the three `env.ts` files + spells it the same way and each spec pins all seven cases (absent, `""`, + whitespace, `abc`, `3.5`, valid, out of range). The `` type argument + is needed because `z.coerce.number()`'s input is `unknown`, which `.pipe` + will not accept from a `string`. The earlier digits-only regex plus + `.transform(Number)` was the over-built form of this; it was simplified in + the PR #7 review, and a bare `z.coerce.number()` was tried there and reverted + for the `min(0)` hole above. Note `fromSchema` is **curried** — `fromSchema(schema)(input)`, not `fromSchema(schema, input)`. @@ -728,15 +785,27 @@ A sixth rule is about production code that tests keep honest: Shipped: the whole kernel — phase tracker, injectable clock, ambient record, unit registry, `Runtime` contract, `start`, draining, signals, uncaught handling, probes, `runMain`, the testing entry point, and the invariants suite. -Plus the five `examples/` workspaces: the clean-architecture application and its -**two** deployments, `order-api` (oRPC) and `order-worker` (an in-memory queue), -which together are the proof of Thesis #1. +Plus the eight `examples/` workspaces: the clean-architecture application and its +**three** deployments, `order-api` (oRPC), `order-worker` (an in-memory queue) +and `order-temporal` (a Temporal worker over `temporal-contract`), which +together are the proof of Thesis #1 — and `order-api-contract` / +`order-temporal-contract`, each transport's contract as a shared artifact both +the server and any client can depend on, with a `layering.test-d.ts` proving it +depends on neither. Deferred, deliberately: - `@btravstack/start-http`, `-amqp`, `-temporal` — the runtime implementations. **They do not exist**; the `Runtime` contract is the whole of what this package owes them. Do not write as though they ship. + `examples/order-temporal` is an _example_ of one, not `-temporal`: it is + `private`, application-specific (it names `PlaceOrder`), and models a single + contract rather than a general Temporal adapter. +- **Caching the Temporal test-server binary in CI.** The path is stable and + gitignored; what is missing is the `actions/cache` step, which cannot be + written from `start`'s `ci.yml` while it delegates to + `btravstack/config`'s reusable workflow (see Toolchain & conventions). It + costs ~3.5 s per test job, not correctness. - The `@btravstack/oxlint` rule banning `currentUnit()` outside infrastructure adapters (Thesis #2) — it needs a way to identify an adapter. - Per-unit ports: the `unit` module wired into `run`'s fork. `RunUnit` is typed diff --git a/README.md b/README.md index 311f046..6fb3200 100644 --- a/README.md +++ b/README.md @@ -561,8 +561,8 @@ complete one. ## Documentation See [`packages/start`](./packages/start) for the package README, -[`examples/`](./examples) for a five-package clean-architecture application -booted under two different runtimes, and [`CLAUDE.md`](./CLAUDE.md) for the +[`examples/`](./examples) for a six-package clean-architecture application +booted under three different runtimes, and [`CLAUDE.md`](./CLAUDE.md) for the authoritative spec: the theses, the load-bearing invariants with the test that guards each, and the internal design notes. diff --git a/examples/README.md b/examples/README.md index 8ac42fa..0ddf273 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,31 +1,35 @@ # Examples -Five small packages that are **one application booted two ways**: a clean -architecture split across four layers, deployed once as an oRPC API and once as -a queue worker — and, at the same time, exercising `@btravstack/start` end to -end from a consumer's own workspace, `workspace:*` and all. - -| Package | Layer | Shows | -| ------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| [`order-domain`](./order-domain) | domain | Entities and rules with no dependencies at all: branded fields, an `Entity.invariant` re-checked on every path, failures as values. | -| [`order-application`](./order-application) | use cases | Ports declared by the caller, interactors, and an `ApplicationModule` whose `OrderRepository` is deliberately an **unmet need**. | -| [`order-infrastructure`](./order-infrastructure) | adapters | A Prisma-backed repository over in-memory SQLite, translating P-codes into the domain's vocabulary and closing the application's one need. | -| [`order-api`](./order-api) | runtime | The first deployment: an oRPC router over `node:http`, a scope forked per request, and `Result` → `ORPCError`. | -| [`order-worker`](./order-worker) | runtime | The second deployment: an in-memory queue worker over the **same** composition, and `Result` → ack / retry / dead-letter. | +Eight small packages that are **one application booted three ways**: a clean +architecture split across four layers, deployed once as an oRPC API, once as a +queue worker and once as a Temporal worker, with each transport's contract in a +package of its own — and, at the same time, exercising `@btravstack/start` end +to end from a consumer's own workspace, `workspace:*` and all. + +| Package | Layer | Shows | +| ------------------------------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`order-domain`](./order-domain) | domain | Entities and rules with no dependencies at all: branded fields, an `Entity.invariant` re-checked on every path, failures as values. | +| [`order-application`](./order-application) | use cases | Ports declared by the caller, interactors, and an `ApplicationModule` whose `OrderRepository` is deliberately an **unmet need**. | +| [`order-infrastructure`](./order-infrastructure) | adapters | A Prisma-backed repository over in-memory SQLite, translating P-codes into the domain's vocabulary and closing the application's one need. | +| [`order-api-contract`](./order-api-contract) | contract | The oRPC contract on its own — wire shapes and declared error codes — taken by the server that implements it **and** by any client. | +| [`order-api`](./order-api) | runtime | The first deployment: an oRPC router over `node:http`, a scope forked per request, and `Result` → `ORPCError`. | +| [`order-worker`](./order-worker) | runtime | The second deployment: an in-memory queue worker over the **same** composition, and `Result` → ack / retry / dead-letter. | +| [`order-temporal-contract`](./order-temporal-contract) | contract | The Temporal contract on its own — one workflow, one activity, two declared `nonRetryable` errors — read by the worker, the sandbox and the client. | +| [`order-temporal`](./order-temporal) | runtime | The third deployment: a Temporal worker whose **activity is the kernel unit**, `Result` → typed contract error, and a drain that waits. | ## The layering, and which way the arrows point ``` - order-api order-worker ← one runtime each; one process each - └───────────┬───────────────┘ - ▼ - order-infrastructure ← Prisma, SQLite, P-codes - │ provides OrderRepository - ▼ - order-application ← use cases, and the ports they declare - │ - ▼ - order-domain ← entities and rules; depends on nothing + order-api order-worker order-temporal ← one runtime each; one process each + └────────────────┬─────────────────┘ + ▼ + order-infrastructure ← Prisma, SQLite, P-codes + │ provides OrderRepository + ▼ + order-application ← use cases, and the ports they declare + │ + ▼ + order-domain ← entities and rules; depends on nothing ``` Every arrow points **inwards**, and the one that looks like it goes the wrong @@ -36,9 +40,45 @@ happens to satisfy it. `ApplicationModule` therefore leaves that need **unmet**, which is not documentation but a type: `Module.scoped(ApplicationModule, …)` does not compile until an outer module provides one. -## One application, two deployments +## The contract tier, which depends on nothing and is depended upon -`OrderApiModule` and `OrderWorkerModule` are the same three lines: +A transport's contract is a **shared artifact**, so each one is a package of its +own: + +``` + order-api any API client order-temporal any workflow client + └──────────────┬───────┘ └───────────────┬───────┘ + ▼ ▼ + order-api-contract order-temporal-contract + ← @orpc/contract ← @temporal-contract/contract, zod +``` + +Every arrow points _at_ a contract and none points out of one. That is the whole +of contract-first design: a client is entitled to the wire shapes and the +declared errors without the router or activity that implements them, the di +wiring behind it, the Prisma-backed repository behind that, or the kernel +booting the lot. While the contracts sat inside `order-api/src/contract.ts` and +`order-temporal/src/contract.ts`, no client could take one without the others. +Neither package depends on `@btravstack/start`, on `@btravstack/di`, or on any +other example — the transports depend on **them**. + +The rule is enforced by the compiler rather than by review: each contract +package's `src/layering.test-d.ts` imports its transport package under a +`@ts-expect-error`, so adding the implementation to the contract's dependencies +makes the directive unused and fails `test:types` — the same shape +`order-domain` uses to keep the application layer out of the domain. + +And the payoff is demonstrated rather than asserted. `order-api-contract`'s own +spec builds a real oRPC client from `RouterContractClient` +and drives it over a stub `fetch`, with nothing from `order-api` in scope; +`order-temporal-contract`'s runs the workflow's input schema as a validator +returning a `Result`, which is the check a caller makes before starting an +execution — and all a Temporal client can do without a running service. + +## One application, three deployments + +`OrderApiModule`, `OrderWorkerModule` and `OrderTemporalModule` are the same +three lines: ```ts imports: [ApplicationModule, PersistenceModule], @@ -51,37 +91,61 @@ and nothing could: the use cases return a `Result`, and what a `Result` means to a transport is the transport's business. The kernel's headline claim — several runtime _kinds_, one per process, over the same module — is proved here rather than asserted, and the sharpest form of the proof is that **the same `Err` -becomes two different outcomes**: +becomes three different outcomes**: + +| unthrown | `order-api` | `order-worker` | `order-temporal` | +| ---------------------- | ----------------------- | --------------------------- | --------------------------------------- | +| `Ok(order)` | the procedure's output | **ack** | the workflow's output | +| `Err(InvalidQuantity)` | `INVALID_QUANTITY` | **dead-letter** | `InvalidQuantity`, **non-retryable** | +| `Err(DuplicateOrder)` | `CONFLICT` | **dead-letter** | `OrderAlreadyPlaced`, **non-retryable** | +| `Defect` | `INTERNAL_SERVER_ERROR` | **retry**, then dead-letter | **retried by the platform**, then fails | + +The kernel appears in none of the three columns. `RunUnit` hands a runtime the +work's own `Result` and stays out of what it means. + +The fourth column carries something the other two do not. Naming a failure on a +Temporal contract decides not only what the caller sees but **whether the +platform retries it** — both domain errors are declared `nonRetryable`, so +Temporal asks exactly once, while an unmodelled failure stays unnamed and the +retry policy takes over. `order-worker` hand-rolls that distinction with an +attempt budget; on Temporal it is a line of contract. + +## What each runtime calls a "unit" + +The three deployments disagree about what one piece of work is, and the kernel +does not care — which is the point of `RunUnit` being parameterised by nothing +but `UnitMeta`: -| unthrown | `order-api` | `order-worker` | -| ---------------------- | ----------------------- | --------------------------- | -| `Ok(order)` | the procedure's output | **ack** | -| `Err(InvalidQuantity)` | `INVALID_QUANTITY` | **dead-letter** | -| `Err(DuplicateOrder)` | `CONFLICT` | **dead-letter** | -| `Defect` | `INTERNAL_SERVER_ERROR` | **retry**, then dead-letter | +| | one unit is | `id` | `traceId` | +| ---------------- | ------------------------ | ---------------------- | --------------------------------- | +| `order-api` | one HTTP request | a fresh `randomUUID()` | an inbound `x-request-id`, if any | +| `order-worker` | one **delivery** | `job#attempt` | the message id | +| `order-temporal` | one **activity attempt** | Temporal's task token | the workflow id | -The kernel appears in neither column. `RunUnit` hands a runtime the work's own -`Result` and stays out of what it means. +All three are answering the same obligation — `UnitMeta.id` must be unique per +unit, because `traceId` defaults to it — and all three land on "the attempt, not +the logical thing", because a retry is a second unit and the same trace. ## The only non-empty `needs` in the repo -`orpcRuntime` declares `[PlaceOrder, FindOrder, Logger]` and -`queueWorkerRuntime` declares `[PlaceOrder, Logger]` — two of the three the -module exports, because a runtime declares what _it_ needs. They are the **only -runtimes in this repository with a non-empty `needs`**: the kernel's own -`testRuntime` needs nothing, so `start`'s phantom rest-tuple gate — and -`RuntimeHost`'s `Context>`, where a runtime names port -_classes_ while di parameterises contexts by port _instances_ — are exercised -against a real module here and nowhere else. - -Both directions are pinned, in `order-api/src/needs-gate.test-d.ts` and -`order-worker/src/needs-gate.test-d.ts`: the wired call is an ordinary +`orpcRuntime` declares `[PlaceOrder, FindOrder, Logger]`, while +`queueWorkerRuntime` and `temporalWorkerRuntime` each declare +`[PlaceOrder, Logger]` — two of the three the module exports, because a runtime +declares what _it_ needs. They are the **only runtimes in this repository with a +non-empty `needs`**: the kernel's own `testRuntime` needs nothing, so `start`'s +phantom rest-tuple gate — and `RuntimeHost`'s `Context>`, +where a runtime names port _classes_ while di parameterises contexts by port +_instances_ — are exercised against a real module here and nowhere else. + +All three directions are pinned, in `order-api/src/needs-gate.test-d.ts`, +`order-worker/src/needs-gate.test-d.ts` and +`order-temporal/src/needs-gate.test-d.ts`: the wired call is an ordinary two-argument one, and a module one port short fails on **arity**, naming the missing need. ## Why these are tests, not just illustrations -Each package reads as application code, and each is covered by real specs — 59 +Each package reads as application code, and each is covered by real specs — 74 of them, run by the repository's own `pnpm test`: ```sh @@ -94,9 +158,20 @@ Nothing is faked at the boundaries that matter. `order-infrastructure` runs against a real Prisma client over in-memory SQLite, so a `DuplicateOrder` comes from an actual `UNIQUE` index raising an actual P2002. `order-api` runs a real `node:http` server and a real oRPC client over it, so the collapse of a `Defect` -to `INTERNAL_SERVER_ERROR` happens where it really happens. **No Docker, and -nothing to install**: the Prisma client is generated by the `test` script -itself. +to `INTERNAL_SERVER_ERROR` happens where it really happens. `order-temporal` +runs a real `TypedWorker` polling a real task queue, so a drain that lets an +in-flight activity finish is the SDK's own `DRAINING` state and not a mock of +it. **No Docker**: the Prisma client is generated by the `test` script itself, +and Temporal's time-skipping test server is a local binary rather than a +container. + +One caveat, and it is the only one in this directory: `order-temporal` needs +**network access on a cold cache** to fetch that 64 MB binary once (cached at +`/.cache/temporal-test-server`, gitignored, with a year-long ttl). Every +other example is entirely self-contained. The trade was taken deliberately — +the alternative, a real Temporal cluster under `testcontainers`, needs a network +pull _and_ a daemon — and it costs about 3.5 s, once. See +[`order-temporal`'s README](./order-temporal#running-it--and-the-one-thing-this-example-needs-that-the-others-do-not). Where a guarantee is compile-time only — an unmet port, a runtime's `needs` — the assertion is a `@ts-expect-error` in a `*.test-d.ts` file, checked by `tsc` diff --git a/examples/order-api-contract/README.md b/examples/order-api-contract/README.md new file mode 100644 index 0000000..6c9ea4b --- /dev/null +++ b/examples/order-api-contract/README.md @@ -0,0 +1,45 @@ +# `@btravstack/start` example: the order API contract + +The oRPC contract — the wire shapes and the declared error codes — in a package +of its own, depending on `@orpc/contract` and nothing else. + +``` +src/contract.ts the contract: inputs, outputs, and the errors a client may branch on +src/layering.test-d.ts the dependency rule, as a compile error +src/test-fixtures.ts a client built from this package alone, over a stub `fetch` +``` + +## Why it is not part of `order-api` + +A contract is a **shared artifact**: the point of declaring one before any +implementation exists is that a client and a server both depend on it. While it +sat in `order-api/src/contract.ts`, a would-be client could only reach it by +depending on the whole transport — the router, the di wiring, the Prisma-backed +repository, the kernel — none of which it has any business installing. + +So the arrow points the other way from every other one in this example set: + +``` + order-api any client + └────────┬─────────┘ + ▼ + order-api-contract ← depends on nothing but @orpc/contract +``` + +`src/layering.test-d.ts` is that sentence as a compile error: it imports +`@btravstack/start-example-order-api` under a `@ts-expect-error`, so the day +this package gains a dependency on the server it implements, `test:types` fails +because the directive stops being used. + +## The proof is a client, not a claim + +`src/client.spec.ts` builds a real oRPC client whose types come from +`RouterContractClient` — the contract, not +`RouterClient` — and drives it over a stub `fetch` that +answers the RPC protocol with a `Map` behind it. Nothing from `order-api` is +imported, and both channels survive: the declared `NOT_FOUND` arrives as an +`Err` carrying its `data`, because the response marks it _inferable_ exactly as +a real server would. + +`@orpc/client` and `@unthrown/orpc` are **dev** dependencies here. A client +picks them itself; what it takes from this package is the contract. diff --git a/examples/order-api-contract/package.json b/examples/order-api-contract/package.json new file mode 100644 index 0000000..7b739f0 --- /dev/null +++ b/examples/order-api-contract/package.json @@ -0,0 +1,30 @@ +{ + "name": "@btravstack/start-example-order-api-contract", + "private": true, + "description": "The oRPC contract of the clean-architecture example: the wire shapes and the declared error codes, shared by the server that implements it and any client that calls it", + "license": "MIT", + "author": "Benoit TRAVERS ", + "type": "module", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run", + "test:types": "tsc --noEmit -p tsconfig.test-d.json", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json" + }, + "dependencies": { + "@orpc/contract": "catalog:" + }, + "devDependencies": { + "@btravstack/tsconfig": "catalog:", + "@orpc/client": "catalog:", + "@types/node": "catalog:", + "@unthrown/orpc": "catalog:", + "@unthrown/vitest": "catalog:", + "typescript": "catalog:", + "unthrown": "catalog:", + "vitest": "catalog:" + } +} diff --git a/examples/order-api-contract/src/client.spec.ts b/examples/order-api-contract/src/client.spec.ts new file mode 100644 index 0000000..e7b6fba --- /dev/null +++ b/examples/order-api-contract/src/client.spec.ts @@ -0,0 +1,39 @@ +import { ORPCError } from "@orpc/client"; +import { describe, expect } from "vitest"; + +import { it } from "./test-fixtures.js"; + +describe("orderContract", () => { + it("types and carries a call made by a client built from the contract alone", async ({ + client, + }) => { + // GIVEN a client whose types come from the contract and whose transport is + // a stub — no router, no `@orpc/server`, no composition root + + // WHEN a procedure the contract declares is called + // THEN the contract's own output shape comes back as a value + await expect(client.orders.place({ id: "o-1", quantity: 2 })).toBeOkWith({ + id: "o-1", + quantity: 2, + }); + }); + + it("hands a declared error back to that client as a typed value", async ({ client }) => { + // GIVEN the same client, and an order the stub does not hold + + // WHEN it is looked up + const missing = await client.orders.find({ id: "o-404" }); + + // THEN the code and payload the contract declares arrive on the error + // channel, inferable — the half of contract-first design a client is + // buying, and it needed nothing from the implementation package + expect(missing).toBeErrWith( + expect.objectContaining({ + constructor: ORPCError, + code: "NOT_FOUND", + data: { id: "o-404" }, + inferable: true, + }), + ); + }); +}); diff --git a/examples/order-api/src/contract.ts b/examples/order-api-contract/src/contract.ts similarity index 100% rename from examples/order-api/src/contract.ts rename to examples/order-api-contract/src/contract.ts diff --git a/examples/order-api-contract/src/index.ts b/examples/order-api-contract/src/index.ts new file mode 100644 index 0000000..155355e --- /dev/null +++ b/examples/order-api-contract/src/index.ts @@ -0,0 +1 @@ +export { orderContract, type OrderRef, type OrderView } from "./contract.js"; diff --git a/examples/order-api-contract/src/layering.test-d.ts b/examples/order-api-contract/src/layering.test-d.ts new file mode 100644 index 0000000..b94d650 --- /dev/null +++ b/examples/order-api-contract/src/layering.test-d.ts @@ -0,0 +1,13 @@ +/** + * What makes a contract a *shared* artifact, enforced by the package boundary + * rather than by review: a client has to be able to take the contract without + * the server that implements it — the router, its di wiring, its persistence, + * the kernel. This file fails to resolve on purpose, and `test:types` fails if + * it ever stops failing, which is what would happen the moment someone added + * the transport package to this package's dependencies. + */ + +// @ts-expect-error — the contract must not be able to reach the server that +// implements it: order-api-contract does not depend on order-api, so the +// specifier does not resolve. +import type {} from "@btravstack/start-example-order-api"; diff --git a/examples/order-api-contract/src/test-fixtures.ts b/examples/order-api-contract/src/test-fixtures.ts new file mode 100644 index 0000000..58263ea --- /dev/null +++ b/examples/order-api-contract/src/test-fixtures.ts @@ -0,0 +1,83 @@ +import { createORPCClient } from "@orpc/client"; +import { RPCLink, type RPCLinkOptions } from "@orpc/client/fetch"; +import type { RouterContractClient } from "@orpc/contract"; +import { createResultClient, type ResultClient } from "@unthrown/orpc/client"; +import { test, type TestAPI } from "vitest"; + +import type { orderContract, OrderView } from "./contract.js"; + +/** + * The caller's view of the API, derived from the **contract** — + * `RouterContractClient`, not `RouterClient`. `order-api`'s own client type is the router-derived one + * because that package holds the router; this is what a consumer who has only + * this package gets, and it types the same two calls, the same inputs and the + * same declared error codes. + */ +type OrderApiClient = ResultClient>; + +type StubFetch = NonNullable["fetch"]>; + +const inputOf = (init: RequestInit): T => + (JSON.parse(String(init.body)) as { readonly json: T }).json; + +const rpc = (status: number, json: unknown): Response => + new Response(JSON.stringify({ json, meta: [] }), { + status, + headers: { "content-type": "application/json" }, + }); + +/** + * An error the way the wire carries a **declared** one. `inferable` is the flag + * oRPC sets when an error matches the procedure's `.errors({...})` map, and it + * is what decides whether the client hands it back as a value or collapses it + * into a defect. Answering the protocol rather than poking the field is the + * point: the client under test deserializes this exactly as it would a real + * server's response. + */ +const declared = (status: number, code: string, id: string): Response => + rpc(status, { defined: true, inferable: true, code, message: code, data: { id } }); + +/** + * A server substitute that is not a server: a `fetch` over a `Map`, answering + * the two procedures the contract declares. It stands in for the half of + * contract-first design this package exists to make possible — the client half, + * which must work with nothing from the implementation side but these shapes. + */ +const stubServer = (): StubFetch => { + const stored = new Map(); + + return async (_url, init, _options, path) => { + if (path.join(".") === "orders.place") { + const order = inputOf<{ readonly id: string; readonly quantity: number }>(init); + if (stored.has(order.id)) return declared(409, "CONFLICT", order.id); + stored.set(order.id, order); + return rpc(200, order); + } + + const { id } = inputOf<{ readonly id: string }>(init); + const found = stored.get(id); + return found === undefined ? declared(404, "NOT_FOUND", id) : rpc(200, found); + }; +}; + +export type ContractFixtures = { + /** + * A client built from the contract alone — `@orpc/contract` for the types, + * `@orpc/client` for the transport, and nothing from `order-api`. + */ + readonly client: OrderApiClient; +}; + +export const it: TestAPI = test.extend({ + // oxlint-disable-next-line no-empty-pattern -- Vitest fixtures require a destructuring pattern; this one depends on no other fixture + client: async ({}, use) => { + await use( + createResultClient( + createORPCClient>( + new RPCLink({ origin: "http://stub", url: "/rpc", fetch: stubServer() }), + ), + ), + ); + }, +}); diff --git a/examples/order-api-contract/src/vitest.d.ts b/examples/order-api-contract/src/vitest.d.ts new file mode 100644 index 0000000..ad36daf --- /dev/null +++ b/examples/order-api-contract/src/vitest.d.ts @@ -0,0 +1 @@ +import type {} from "@unthrown/vitest"; diff --git a/examples/order-api-contract/tsconfig.json b/examples/order-api-contract/tsconfig.json new file mode 100644 index 0000000..f4b5b3b --- /dev/null +++ b/examples/order-api-contract/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@btravstack/tsconfig/base.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "src/**/*.test-d.ts"] +} diff --git a/examples/order-api-contract/tsconfig.test-d.json b/examples/order-api-contract/tsconfig.test-d.json new file mode 100644 index 0000000..619908b --- /dev/null +++ b/examples/order-api-contract/tsconfig.test-d.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "noUnusedLocals": false, "noUnusedParameters": false }, + "include": ["src/**/*.test-d.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/order-api-contract/vitest.config.ts b/examples/order-api-contract/vitest.config.ts new file mode 100644 index 0000000..fb76260 --- /dev/null +++ b/examples/order-api-contract/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.spec.ts"], + setupFiles: ["@unthrown/vitest"], + }, +}); diff --git a/examples/order-api/README.md b/examples/order-api/README.md index 71e30d3..7d67610 100644 --- a/examples/order-api/README.md +++ b/examples/order-api/README.md @@ -1,10 +1,11 @@ # `@btravstack/start` example: the order API layer -The transport. An oRPC contract, a router, and a `Runtime` that serves them over -`node:http` under the kernel's lifecycle. +The transport. A router implementing +[`order-api-contract`](../order-api-contract), and a `Runtime` that serves it +over `node:http` under the kernel's lifecycle. The contract itself lives in its +own package, because a client needs it and needs none of this. ``` -src/contract.ts the oRPC contract — the wire shapes and the declared error codes src/router.ts the implementation, and the one place a domain error becomes an ORPCError src/request-scope.ts RequestModule — a scope forked per request over the application's src/orpc-runtime.ts the Runtime: start / drain / stop @@ -123,9 +124,11 @@ const client = createOrderApiClient("http://127.0.0.1:3000"); const named = (await client.orders.place({ id, quantity })).match({ ok: () => "placed", errCases: (matcher) => - matcher - .with({ code: "INVALID_QUANTITY" }, (error) => error.code) - .with({ code: "CONFLICT" }, (error) => error.code), + matcher.with( + { code: "INVALID_QUANTITY" }, + { code: "CONFLICT" }, + (error) => error.code, + ), defect: () => "bug", }); ``` @@ -137,7 +140,7 @@ the server's `mapErrCases`. ## Running it ```bash -pnpm --filter @btravstack/start-example-order-api test # 15 runtime specs + 4 env specs +pnpm --filter @btravstack/start-example-order-api test # 15 runtime specs + 6 env specs ``` The specs run against a real HTTP server and a real oRPC client — genuine JSON @@ -183,9 +186,24 @@ because `.parse()` throws — which `unthrown/no-throw` bans, and which would contradict the example it appears in. The issues are the modeled `E`, folded above into a message and a non-zero exit code. -The schema reads **strings**, not `z.coerce.number()`: coercion is `Number()` -underneath, so `PORT=abc` would bind `NaN` and `PORT=` would bind `0`, the -ephemeral port. A malformed value is a validation issue instead. +A port is a **non-empty string piped into a coercion**, never a bare +`z.coerce.number()`: + +```ts +z.string() + .trim() + .min(1) + .pipe(z.coerce.number().int().min(0).max(65_535)) + .default(fallback); +``` + +Coercion is `Number()` underneath, so `PORT=abc` would bind `NaN` and `PORT=` +would bind `0`, the ephemeral port. The bounds catch the first — and every +`PORT=3.5` or `PORT=99999` after it — but they cannot catch the second, because +a port's `min` **is** `0` so that an ephemeral bind stays expressible. The +non-empty string in front is what closes it: an empty value is a configuration +error, not an absent one, and `.default(...)` applies only when the variable is +genuinely missing. A malformed value is a validation issue instead. It is typechecked by the gate rather than executed by it: the example packages are source-only — no build step, `main` pointing straight at `src/` — so there diff --git a/examples/order-api/package.json b/examples/order-api/package.json index f774774..a9153ec 100644 --- a/examples/order-api/package.json +++ b/examples/order-api/package.json @@ -17,11 +17,11 @@ "dependencies": { "@btravstack/di": "catalog:", "@btravstack/start": "workspace:*", + "@btravstack/start-example-order-api-contract": "workspace:*", "@btravstack/start-example-order-application": "workspace:*", "@btravstack/start-example-order-domain": "workspace:*", "@btravstack/start-example-order-infrastructure": "workspace:*", "@orpc/client": "catalog:", - "@orpc/contract": "catalog:", "@orpc/server": "catalog:", "@unthrown/orpc": "catalog:", "@unthrown/standard-schema": "catalog:", diff --git a/examples/order-api/src/env.spec.ts b/examples/order-api/src/env.spec.ts index 184e09f..8cee4d6 100644 --- a/examples/order-api/src/env.spec.ts +++ b/examples/order-api/src/env.spec.ts @@ -42,6 +42,33 @@ describe("readEnv", () => { ]); }); + it("rejects a port that is present but blank, rather than defaulting it", () => { + // GIVEN two variables set to whitespace — the shape `Number()` reads as + // `0`, which `min(0)` cannot catch because an ephemeral bind is legal + const source = { PORT: " ", PROBE_PORT: "\t\n" }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN both are configuration errors rather than absent variables: the + // default is for a variable nobody set, not one set to nothing + expect(env).toBeErrWith([ + expect.objectContaining({ path: ["PORT"] }), + expect.objectContaining({ path: ["PROBE_PORT"] }), + ]); + }); + + it("rejects a port that is a number but not a whole one", () => { + // GIVEN a value `Number()` reads happily and no socket could ever bind + const source = { PORT: "3.5" }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN it is an issue rather than a silent truncation + expect(env).toBeErrWith([expect.objectContaining({ path: ["PORT"] })]); + }); + it("rejects a port outside the range a socket can take", () => { // GIVEN a number that parses but cannot be bound const source = { PORT: "99999" }; diff --git a/examples/order-api/src/env.ts b/examples/order-api/src/env.ts index e875bba..6ff2197 100644 --- a/examples/order-api/src/env.ts +++ b/examples/order-api/src/env.ts @@ -5,17 +5,23 @@ import { z } from "zod"; /** * A port, read the way an environment variable actually arrives: as a string. * - * `z.coerce.number()` would be shorter and wrong — it is `Number()` underneath, - * so `PORT=abc` becomes `NaN` and `PORT=` becomes `0`, an ephemeral bind. A - * digits-only string is the honest model, and anything else is a validation - * issue rather than a number nobody asked for. + * The non-empty string in front of the coercion is the load-bearing part. + * Coercion is `Number()` underneath, and `Number("")` is `0` — so a bare + * `PORT=` would bind the ephemeral port `0` rather than be reported, and a + * port's `min` **is** `0` because an ephemeral bind has to stay expressible, so + * the bounds cannot catch it. An empty value is a configuration error, not an + * absent one: `.default(...)` applies only when the variable is genuinely + * missing. + * + * With that guard, the bounds handle the rest — `PORT=abc` is `NaN`, + * `PORT=3.5` is not an integer, `PORT=99999` is out of range. */ const port = (fallback: number) => z .string() - .regex(/^\d+$/u, "must be a whole number of decimal digits") - .transform(Number) - .pipe(z.int().min(0).max(65_535)) + .trim() + .min(1) + .pipe(z.coerce.number().int().min(0).max(65_535)) .default(fallback); const environment = z.object({ diff --git a/examples/order-api/src/index.ts b/examples/order-api/src/index.ts index d696e17..adbf48e 100644 --- a/examples/order-api/src/index.ts +++ b/examples/order-api/src/index.ts @@ -1,5 +1,4 @@ export { createOrderApiClient, type OrderApiClient } from "./client.js"; -export { orderContract, type OrderRef, type OrderView } from "./contract.js"; export { OrderApiModule } from "./module.js"; export { orpcRuntime, type OrderApiInfo, type OrderApiOptions } from "./orpc-runtime.js"; export { RequestModule, RequestSpan } from "./request-scope.js"; diff --git a/examples/order-api/src/orpc-runtime.spec.ts b/examples/order-api/src/orpc-runtime.spec.ts index 9102757..2c6de84 100644 --- a/examples/order-api/src/orpc-runtime.spec.ts +++ b/examples/order-api/src/orpc-runtime.spec.ts @@ -106,13 +106,13 @@ describe("orpcRuntime", () => { const invalid = await client.orders.place({ id: "o-2", quantity: 0 }); // WHEN the channel is folded — the mirror of the `mapErrCases` that - // produced it, with no wildcard to fall back on + // produced it, with no wildcard to fall back on. Both codes are named and + // grouped into one arm because they share a handler, which is what a + // wildcard would look like if it were still a decision const named = invalid.match({ ok: () => "WRONGLY ACCEPTED", errCases: (matcher) => - matcher - .with({ code: "INVALID_QUANTITY" }, (error) => error.code) - .with({ code: "CONFLICT" }, (error) => error.code), + matcher.with({ code: "INVALID_QUANTITY" }, { code: "CONFLICT" }, (error) => error.code), defect: () => "defect", }); diff --git a/examples/order-api/src/router.ts b/examples/order-api/src/router.ts index 41422d7..4372723 100644 --- a/examples/order-api/src/router.ts +++ b/examples/order-api/src/router.ts @@ -1,12 +1,11 @@ import type { Context } from "@btravstack/di"; +import { orderContract, type OrderView } from "@btravstack/start-example-order-api-contract"; import { FindOrder, PlaceOrder } from "@btravstack/start-example-order-application"; import type { Order } from "@btravstack/start-example-order-domain"; import { implement } from "@orpc/server"; import { handlerResult } from "@unthrown/orpc/server"; import { P } from "unthrown"; -import { orderContract, type OrderView } from "./contract.js"; - /** * What every procedure is handed: the request's own di `Context`, seeded from * the application scope the kernel built. Passing the context rather than the diff --git a/examples/order-temporal-contract/README.md b/examples/order-temporal-contract/README.md new file mode 100644 index 0000000..9fa8bed --- /dev/null +++ b/examples/order-temporal-contract/README.md @@ -0,0 +1,50 @@ +# `@btravstack/start` example: the order Temporal contract + +The Temporal contract — one workflow, one activity, and the two errors a caller +may branch on — in a package of its own, depending on +`@temporal-contract/contract` and `zod`. + +``` +src/contract.ts the contract: schemas, declared errors, activity options, task queue +src/layering.test-d.ts the dependency rule, as a compile error +src/test-fixtures.ts the contract's own schema, as a validator returning a Result +``` + +## Why it is not part of `order-temporal` + +A contract is a **shared artifact**. Temporal's version of the point is sharper +than oRPC's, because three parties read this file: the worker that implements +the activity, the workflow that runs in the sandbox, and the client that starts +the execution. Only the first of those wants a di container, a Prisma-backed +repository and the kernel. + +``` + order-temporal any client starting a workflow + └──────────┬──────────┘ + ▼ + order-temporal-contract ← @temporal-contract/contract and zod, nothing else +``` + +`src/layering.test-d.ts` is that sentence as a compile error: it imports +`@btravstack/start-example-order-temporal` under a `@ts-expect-error`, so the +day this package gains a dependency on the worker it describes, `test:types` +fails because the directive stops being used. + +## The schemas are the demonstration + +Where the oRPC contract's proof is a client built from it, this one's is that +the contract is **executable**: `src/contract.spec.ts` runs the workflow's own +input schema through `@unthrown/standard-schema`'s `fromSchema` and gets a +`Result`, with no worker, no connection and no activity implementation in +scope — which is exactly the check a caller makes before starting an execution. + +There is no client-side test beyond that, and that is a property of Temporal +rather than an omission: a `TypedClient` needs a running service to talk to, so +"a client built from the contract alone" is what `order-temporal`'s own suite +already exercises against the time-skipping test server. + +`zod` is a runtime dependency because the schemas **are** the contract — they +travel to the client, which validates against them. `unthrown` is a dev +dependency only: it satisfies the optional peer `@temporal-contract/contract` +declares (keeping one copy of it across the workspace) and backs the spec's +matchers. diff --git a/examples/order-temporal-contract/package.json b/examples/order-temporal-contract/package.json new file mode 100644 index 0000000..2117803 --- /dev/null +++ b/examples/order-temporal-contract/package.json @@ -0,0 +1,30 @@ +{ + "name": "@btravstack/start-example-order-temporal-contract", + "private": true, + "description": "The Temporal contract of the clean-architecture example: the workflow, the activity and the errors a caller may branch on, shared by the worker that implements it and any client that starts it", + "license": "MIT", + "author": "Benoit TRAVERS ", + "type": "module", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run", + "test:types": "tsc --noEmit -p tsconfig.test-d.json", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json" + }, + "dependencies": { + "@temporal-contract/contract": "catalog:", + "zod": "catalog:" + }, + "devDependencies": { + "@btravstack/tsconfig": "catalog:", + "@types/node": "catalog:", + "@unthrown/standard-schema": "catalog:", + "@unthrown/vitest": "catalog:", + "typescript": "catalog:", + "unthrown": "catalog:", + "vitest": "catalog:" + } +} diff --git a/examples/order-temporal-contract/src/contract.spec.ts b/examples/order-temporal-contract/src/contract.spec.ts new file mode 100644 index 0000000..2d247c2 --- /dev/null +++ b/examples/order-temporal-contract/src/contract.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect } from "vitest"; + +import { it } from "./test-fixtures.js"; + +describe("orderContract", () => { + it("validates a workflow input from the contract alone", ({ validate }) => { + // GIVEN the contract's own schema, and nothing else — no worker, no + // connection, no activity implementation + + // WHEN a caller checks the payload it is about to start a workflow with + // THEN it is accepted, in the shape Temporal will persist in the history + expect(validate({ orderId: "o-1", quantity: 2 })).toBeOkWith({ + orderId: "o-1", + quantity: 2, + }); + }); + + it("rejects a payload the contract does not describe", ({ validate }) => { + // GIVEN the same schema + + // WHEN the quantity arrives as a string, the way an untyped caller sends it + // THEN the issues come back as a value, naming the field — the contract is + // executable, not documentation, and a client can run it + expect(validate({ orderId: "o-1", quantity: "2" })).toBeErrWith([ + expect.objectContaining({ path: ["quantity"] }), + ]); + }); +}); diff --git a/examples/order-temporal-contract/src/contract.ts b/examples/order-temporal-contract/src/contract.ts new file mode 100644 index 0000000..796e80a --- /dev/null +++ b/examples/order-temporal-contract/src/contract.ts @@ -0,0 +1,75 @@ +import { defineActivity, defineContract, defineWorkflow } from "@temporal-contract/contract"; +import { z } from "zod"; + +/** + * What an order looks like on the wire — the same shape `order-api`'s + * `OrderView` is, and for the same reason: `Order`'s fields are branded + * (`OrderId`, `Quantity`), and a brand is a compile-time fiction that does not + * survive serialization. Temporal persists every activity input and output in + * an event history, so the transport's shape has to be a real one. + */ +const orderView = z.object({ id: z.string(), quantity: z.number() }); + +/** The payload every declared error carries — which order it was about. */ +const orderRef = z.object({ id: z.string() }); + +/** + * The activity: one call into the application layer. + * + * Its `errors` map is the transport half of the errors-as-values story, and it + * carries something neither oRPC nor a queue expresses natively — + * **`nonRetryable`**. A modeled domain failure is a permanent answer, so + * declaring it here is what stops Temporal's retry policy asking the same + * impossible thing five more times. Anything NOT declared here is retried + * according to `activityOptions.retry`, which is exactly the treatment a + * `Defect` deserves: an unmodelled failure is the infrastructure one, and + * infrastructure comes back. + */ +const place = defineActivity({ + input: z.object({ orderId: z.string(), quantity: z.number() }), + output: orderView, + errors: { + InvalidQuantity: { data: orderRef, nonRetryable: true }, + OrderAlreadyPlaced: { data: orderRef, nonRetryable: true }, + }, + activityOptions: { + startToCloseTimeout: "1 minute", + retry: { maximumAttempts: 3, initialInterval: "10 milliseconds" }, + }, +}); + +/** + * The workflow re-declares the same two errors, and that is not duplication. + * + * A contract error declared on an **activity** is rehydrated inside the + * *workflow* — it never reaches the client on its own. A contract error + * declared on the **workflow** is rehydrated at the *client*. So a domain + * failure that a caller is entitled to branch on has to be named at both + * boundaries, which is Temporal's version of the triage `order-api` performs + * once in `router.ts`. `workflows.ts` is where the hand-off happens. + */ +const placeOrder = defineWorkflow({ + input: z.object({ orderId: z.string(), quantity: z.number() }), + output: orderView, + idempotency: "allow-duplicate", + errors: { + InvalidQuantity: { data: orderRef, nonRetryable: true }, + OrderAlreadyPlaced: { data: orderRef, nonRetryable: true }, + }, + activities: { place }, +}); + +/** + * The contract, declared before any implementation exists — the same discipline + * `order-api/src/contract.ts` follows. + * + * `taskQueue` is part of it because a Temporal worker's identity *is* its task + * queue: `TypedWorker.create` reads it off the contract rather than taking it + * as an option. Specs scope a per-test queue with `withTaskQueue`. + */ +export const orderContract = defineContract({ + taskQueue: "orders", + workflows: { placeOrder }, +}); + +export type OrderContract = typeof orderContract; diff --git a/examples/order-temporal-contract/src/index.ts b/examples/order-temporal-contract/src/index.ts new file mode 100644 index 0000000..d705ce5 --- /dev/null +++ b/examples/order-temporal-contract/src/index.ts @@ -0,0 +1 @@ +export { orderContract, type OrderContract } from "./contract.js"; diff --git a/examples/order-temporal-contract/src/layering.test-d.ts b/examples/order-temporal-contract/src/layering.test-d.ts new file mode 100644 index 0000000..241e48f --- /dev/null +++ b/examples/order-temporal-contract/src/layering.test-d.ts @@ -0,0 +1,14 @@ +/** + * What makes a contract a *shared* artifact, enforced by the package boundary + * rather than by review: a client has to be able to start a workflow with + * nothing but the contract — not the worker that implements the activity, its + * di wiring, its persistence or the kernel. This file fails to resolve on + * purpose, and `test:types` fails if it ever stops failing, which is what would + * happen the moment someone added the worker package to this package's + * dependencies. + */ + +// @ts-expect-error — the contract must not be able to reach the worker that +// implements it: order-temporal-contract does not depend on order-temporal, so +// the specifier does not resolve. +import type {} from "@btravstack/start-example-order-temporal"; diff --git a/examples/order-temporal-contract/src/test-fixtures.ts b/examples/order-temporal-contract/src/test-fixtures.ts new file mode 100644 index 0000000..0ab4669 --- /dev/null +++ b/examples/order-temporal-contract/src/test-fixtures.ts @@ -0,0 +1,23 @@ +import { fromSchema } from "@unthrown/standard-schema"; +import { test, type TestAPI } from "vitest"; + +import { orderContract } from "./contract.js"; + +type PlaceOrderInput = typeof orderContract.workflows.placeOrder.input; + +export type ContractFixtures = { + /** + * The contract's own workflow-input schema, as a validator returning a + * `Result` — what a caller holding nothing but this package can check a + * payload with before it ever reaches a worker. + */ + readonly validate: ReturnType>; +}; + +export const it: TestAPI = test.extend({ + // oxlint-disable-next-line no-empty-pattern -- Vitest fixtures require a destructuring pattern; this one depends on no other fixture + validate: async ({}, use) => { + // `fromSchema` is CURRIED — it takes the schema and hands back the validator. + await use(fromSchema(orderContract.workflows.placeOrder.input)); + }, +}); diff --git a/examples/order-temporal-contract/src/vitest.d.ts b/examples/order-temporal-contract/src/vitest.d.ts new file mode 100644 index 0000000..ad36daf --- /dev/null +++ b/examples/order-temporal-contract/src/vitest.d.ts @@ -0,0 +1 @@ +import type {} from "@unthrown/vitest"; diff --git a/examples/order-temporal-contract/tsconfig.json b/examples/order-temporal-contract/tsconfig.json new file mode 100644 index 0000000..f4b5b3b --- /dev/null +++ b/examples/order-temporal-contract/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@btravstack/tsconfig/base.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "src/**/*.test-d.ts"] +} diff --git a/examples/order-temporal-contract/tsconfig.test-d.json b/examples/order-temporal-contract/tsconfig.test-d.json new file mode 100644 index 0000000..619908b --- /dev/null +++ b/examples/order-temporal-contract/tsconfig.test-d.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "noUnusedLocals": false, "noUnusedParameters": false }, + "include": ["src/**/*.test-d.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/order-temporal-contract/vitest.config.ts b/examples/order-temporal-contract/vitest.config.ts new file mode 100644 index 0000000..fb76260 --- /dev/null +++ b/examples/order-temporal-contract/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.spec.ts"], + setupFiles: ["@unthrown/vitest"], + }, +}); diff --git a/examples/order-temporal/README.md b/examples/order-temporal/README.md new file mode 100644 index 0000000..0fc9e82 --- /dev/null +++ b/examples/order-temporal/README.md @@ -0,0 +1,357 @@ +# `@btravstack/start` example: the order Temporal worker + +The third deployment. The same application, the same persistence, the same +composition — driven by a durable execution engine instead of an HTTP server or +a queue. The contract it implements lives in +[`order-temporal-contract`](../order-temporal-contract), because a client that +starts these workflows needs it and needs none of this. + +``` +src/workflows.ts the workflow body, in its own module because the sandbox is bundled separately +src/temporal-runtime.ts the Runtime: start / drain / stop, and the activity that is the kernel unit +src/module.ts OrderTemporalModule — the composition root +src/env.ts process.env validated through a schema, as a Result +src/main.ts the process: readEnv + connect + start + runMain +src/test-fixtures.ts testEnv / serve / gate / tapped / unmodelled, as Vitest fixtures +``` + +## The point of this package + +`OrderTemporalModule` is `OrderApiModule` and `OrderWorkerModule` with a +different name: + +```ts +export const OrderTemporalModule = Module("OrderTemporal")({ + imports: [ApplicationModule, PersistenceModule], + provides: [], + exports: [PlaceOrder, FindOrder, Logger], +}); +``` + +Nothing in `order-application` or `order-infrastructure` changed to make this +work, and nothing could have. **One process, one runtime** was already two +composition roots and two `Runtime` values; a third one, over a transport as +unlike HTTP as durable execution is, is what turns "two" into a pattern. + +## The same `Err`, three transports + +`DuplicateOrder` is one value. Over HTTP there is a caller waiting to be told, +so it becomes a `CONFLICT` the client receives **as a value**. On a queue there +is no caller, so the message is **parked**. Here there is a caller again — a +workflow, and behind it a client — so it becomes a **typed contract error**, +rehydrated by name with its payload intact. + +| unthrown | oRPC (`order-api`) | queue (`order-worker`) | Temporal (this package) | +| ---------------------- | ----------------------- | --------------------------- | --------------------------------------- | +| `Ok(order)` | the procedure's output | **ack** | the workflow's output | +| `Err(InvalidQuantity)` | `INVALID_QUANTITY` | **dead-letter** | `InvalidQuantity`, **non-retryable** | +| `Err(DuplicateOrder)` | `CONFLICT` | **dead-letter** | `OrderAlreadyPlaced`, **non-retryable** | +| `Defect` | `INTERNAL_SERVER_ERROR` | **retry**, then dead-letter | **retried by the platform**, then fails | + +The last column carries something the other two do not. Naming a failure here +decides not only what the caller sees but **whether the platform retries it**: +the contract declares both domain errors `nonRetryable`, so Temporal asks +exactly once. An unmodelled failure stays unnamed and the contract's +`retry.maximumAttempts` takes over — the platform doing for free what +`order-worker` hand-rolls with an attempt budget, and the sharpest form of "a +defect is the infrastructure failure, and infrastructure comes back". + +## The mapping happens twice, and that is Temporal's shape + +An activity-declared contract error is rehydrated **inside the workflow** and +never reaches the client on its own. A workflow-declared one is rehydrated **at +the client**. So a domain failure a caller is entitled to branch on has to be +named at both boundaries — where `order-api` triages once in `router.ts`. + +Boundary one, in `src/temporal-runtime.ts` — the domain's vocabulary stops here: + +```ts +ctx + .get(PlaceOrder) + .execute(args.orderId, args.quantity) + .map((order) => ({ id: order.id, quantity: order.quantity })) + .mapErrCases((matcher) => + matcher + .with(P.tag("InvalidQuantity"), (error) => + errors.InvalidQuantity({ id: error.id }), + ) + .with(P.tag("DuplicateOrder"), (error) => + errors.OrderAlreadyPlaced({ id: error.id }), + ), + ); +``` + +Boundary two, in `src/workflows.ts` — the workflow re-mints the two domain +failures against its own declared errors and hands everything else to +`propagateActivityFailure`, which re-raises Temporal's original failure so the +platform classifies the execution exactly as it would have if the activity call +had thrown: + +```ts +propagateActivityFailure( + context.activities + .place({ orderId: args.orderId, quantity: args.quantity }) + .mapErrCases((matcher) => + matcher + .with({ errorName: "InvalidQuantity" }, (e) => + context.errors.InvalidQuantity({ id: e.data.id }), + ) + .with({ errorName: "OrderAlreadyPlaced" }, (e) => + context.errors.OrderAlreadyPlaced({ id: e.data.id }), + ) + .with( + P.tag(ACTIVITY_ERROR_TAG), + P.tag(ACTIVITY_CANCELLED_ERROR_TAG), + (e) => e, + ), + ), +); +``` + +Every case is named at both — this repo bans `P._`, and there is no +`.otherwise()`. The two activity-machinery tags share a handler, so they are +**grouped** into one arm rather than duplicated: grouping is what +`no-catch-all-pattern` steers you toward instead of a wildcard, and it is still +an enumeration — a third machinery tag would not compile. A new domain error is +a compile error in **both** files, plus +`order-api/src/router.ts` and `order-worker/src/queue-runtime.ts`: four places, +each of which has to decide what it means. + +The client's half is what makes the claim testable rather than asserted: + +```ts +await client.executeWorkflow("placeOrder", { workflowId, args }).match({ + ok: () => "placed", + errCases: (matcher) => + matcher + .with( + { errorName: "InvalidQuantity" }, + (error) => `invalid:${error.data.id}`, + ) + .with( + { errorName: "OrderAlreadyPlaced" }, + (error) => `conflict:${error.data.id}`, + ) + .with( + ...tagPatterns(WORKFLOW_START_ERROR_TAGS), + (error) => `start:${error._tag}`, + ) + .with( + ...tagPatterns(WORKFLOW_RESULT_ERROR_TAGS), + (error) => `result:${error._tag}`, + ), + defect: (cause) => `defect:${String(cause)}`, +}); +``` + +## The activity is the unit + +A Temporal worker polls two kinds of task. Workflow code runs in a +deterministic V8 sandbox and may not touch a database, a clock or a di +container; the **activity** is where the process leaves that sandbox and reaches +real services. So the activity is where a kernel unit belongs, and the +activity implementations are built **inside** `Runtime.start`, closing over the +`RuntimeHost` the kernel handed over: + +```ts +const placeActivity = + (host: RuntimeHost): ActivityImplementationFor => + (args, { errors }) => + host.run(metaFor(), (ctx, _signal) => /* … */); +``` + +That is also why `needs` is `[PlaceOrder, Logger]` rather than empty: the +activity resolves both out of the application context, and `start`'s phantom +rest-tuple gate proves the module exports them before anything runs. + +## A task token is the unit; the workflow id is the trace + +```ts +const metaFor = (): UnitMeta => { + const info = activityInfo(); + return { + kind: "activity", + id: info.base64TaskToken, + traceId: info.workflowExecution?.workflowId ?? info.activityId, + }; +}; +``` + +`UnitMeta.id` must be unique per unit, and the obvious candidate — the workflow +id — is wrong twice over: an activity is retried under the same execution, and +Temporal lets a workflow id be reused once an execution has closed. `order-worker` +answers the same problem by making the _delivery_ the id rather than the +message; here the platform already mints exactly that value. A **task token** +identifies one activity task attempt, so its uniqueness is Temporal's guarantee +rather than an argument of ours. + +The workflow id becomes the `traceId`, which is what `traceId` is for: the +correlation id, minted outside this process by whoever started the execution, +and stable across every retry so all three attempts join up in the log. + +## Draining is real here + +This is the first runtime in the repository where `Serving.drain` meets a +transport with genuine drain semantics of its own, and the two line up exactly: + +```ts +const running = worker.run(); + +const stopPolling = (): void => { + if (worker.raw.getState() === "RUNNING") worker.shutdown(); +}; + +// The kernel's deadline, kept from `drain` so `stop` is released by it too. +let deadline: AbortSignal | undefined; + +const stopped = (): AsyncResult => + deadline === undefined ? running : releasedBy(deadline, running); + +return { + info: { taskQueue, namespace }, + drain: (signal) => { + deadline = signal; + stopPolling(); + return stopped(); + }, + stop: () => { + stopPolling(); + return stopped(); + }, +}; +``` + +`worker.shutdown()` moves the worker to `DRAINING` **immediately**: polling for +new Workflow and Activity Tasks stops at once, in-flight activities run to +completion, and `run()` resolves when the last of them has. So `drain` is +`shutdown()` plus the wait, and it is a genuine wait rather than a courtesy — +`order-api`'s and `order-worker`'s drains stop accepting and have nothing left +to wait for. + +`stop` is the same call made idempotent. After a drain, `running` has already +settled and awaiting it again returns at once; on the `stop()`-without-drain +path it is what shuts the worker down, with `shutdownForceTime` as the ceiling. +`stopPolling` guards on `getState()` because `shutdown()` on a worker that is +not `RUNNING` throws Temporal's `IllegalStateError`, and `stop` always runs +after `drain` on the signal path. + +Three details worth writing down: + +- **`running` is held, not dropped.** `TypedWorker.run()` is + `AsyncResult`, and an empty _error_ channel is not an empty + _defect_ channel — a worker that dies mid-run reports a `TechnicalError` on the + defect channel. Both methods hand it back to the kernel, which consumes it. +- **`shutdownGraceTime` is set explicitly** because Temporal's default is `0`. + Cancellation is cooperative, so a `0` changes nothing for an activity that + ignores the signal — but it would fight the kernel's own drain deadline for + the same decision, and one component should own it. +- **The kernel's deadline `AbortSignal` is honoured, because waiting on `run()` + alone cannot honour it.** `run()` settles on Temporal's clock — + `shutdownForceTime`, 30 seconds by default — so an activity that never + finishes would hold `stop()` well past the kernel's `drainTimeoutMs`, and the + kernel would have no way to release this runtime. `releasedBy` races `running` + against the signal, and the signal is kept so `stop()` is released by the same + abort; the losing branch's `Result` is dropped, exactly as `drainApp` + documents for its own race. `@temporalio/worker` 1.22 offers no public forced + shutdown to escalate to — `Worker.forceShutdown$` is `protected` and + `Runtime.shutdown()` is process-global — so what a runtime can do is stop + waiting, leaving the worker to `shutdownForceTime` and to the entry point + closing the connection underneath it. + +The spec asserts both halves directly — the drain that completes: + +``` +{"type":"drained","report":{"inFlightAtStart":1,"completed":1,"abandoned":0}} +``` + +and the drain that runs out of time, where the exit still arrives on the +kernel's deadline rather than Temporal's: + +``` +{"type":"drained","report":{"inFlightAtStart":1,"completed":0,"abandoned":1}} +``` + +## `Serving.info` with no port and no queue in it + +```ts +const info = (await app.runtimeInfo()).get(); // { taskQueue: "orders", namespace: "default" } +``` + +`order-api` publishes `{ port, prefix }` on the same channel and `order-worker` +`{ queue, concurrency }`. No two of the three shapes share a field, which is +exactly why `Info` is the runtime's own type parameter rather than anything the +kernel models. A Temporal worker's identity **is** its task queue and namespace: +the pair an operator needs to find it in the Web UI, and the pair that decides +which work it will ever be handed. + +## Running it — and the one thing this example needs that the others do not + +```bash +pnpm --filter @btravstack/start-example-order-temporal test # 8 runtime specs + 7 env specs +pnpm --filter @btravstack/start-example-order-temporal test:types # the needs gate +``` + +**No Docker.** A real `TypedWorker` polls a real task queue against +`@temporalio/testing`'s time-skipping test server, which is a local binary +rather than a container — the whole worker loop, real Workflow Tasks and real +Activity Tasks, with the Docker daemon quit. + +**But it does need the network once.** That binary is 64 MB, downloaded on +first use and keyed by the `@temporalio` SDK version. Every other example in +this repository is entirely self-contained; this one is not, and the trade was +accepted deliberately because the alternative — `testcontainers` against a real +Temporal cluster — needs a **network pull _and_ a daemon**, which is the +objection that keeps Docker out of this repo in the first place. A cold cache +with no network fails loudly at environment creation, naming the URL. + +Two things keep that cost to once: + +```ts +createTimeSkippingTest({ + server: { executable: { type: "cached-download", downloadDir, ttl: "365d" } }, +}); +``` + +`downloadDir` is `/.cache/temporal-test-server` — gitignored, and a stable +path rather than the OS temp directory, which CI wipes between jobs and macOS +purges on its own schedule. `ttl` is a year rather than the default one day, so +a developer who runs the suite on Monday and again on Wednesday does not +download it twice. + +Measured on this machine, Docker quit: + +| | Wall clock | +| ------------------------------------ | ------------- | +| Cold cache (64 MB download included) | **7.4 s** | +| Warm | **3.8–3.9 s** | + +So the binary costs about 3.5 s, once. Warm, this package is the slowest in the +repository and still under four seconds. + +`src/needs-gate.test-d.ts` pins the compile-time half: `temporalWorkerRuntime` +declares `[PlaceOrder, Logger]` — two of the three ports the module exports, +because a runtime declares what _it_ needs — and a module missing either fails +`start`'s arity gate before anything runs. + +Every helper the specs need is a Vitest fixture in `src/test-fixtures.ts`, so +each file opens on `describe` and each test names its dependencies in its own +parameter list. Shutting an app down is the `serve` fixture's job, which is why +no test here has a `try`/`finally`. `serve` also scopes a **fresh task queue per +test** (`withTaskQueue` + `nextTaskQueueId`): the time-skipping environment is +shared by every test in the worker process, and two workers polling one queue +would race for each other's tasks. + +`src/main.ts` is the process itself. It opens the `NativeConnection`, because a +runtime is handed its transport rather than owning its lifetime — the same +reason `order-worker`'s `main.ts` creates its queue. Like both siblings' it is +typechecked by the gate rather than executed by it. + +It also **closes** it, in a `.finally` on `runMain`'s promise. Whoever opens it +closes it: the runtime is handed a connection it did not open and has no claim +on, and `src/test-fixtures.ts` is the proof — every test in the file boots a +fresh worker against the _one_ `testEnv.nativeConnection` it shares, so a +runtime closing what it was given would tear the environment down under the next +test. `.finally` rather than a `flatTap` because an open `NativeConnection` +holds the event loop, so the defect path is exactly the one that must still +close it; and a close that fails is written to stderr rather than surfaced, so +that teardown cannot rewrite the exit code `runMain` just set. diff --git a/examples/order-temporal/package.json b/examples/order-temporal/package.json new file mode 100644 index 0000000..0eddedc --- /dev/null +++ b/examples/order-temporal/package.json @@ -0,0 +1,44 @@ +{ + "name": "@btravstack/start-example-order-temporal", + "private": true, + "description": "The third deployment of the clean-architecture example: the same application module, served by a Temporal worker whose activities are kernel units", + "license": "MIT", + "author": "Benoit TRAVERS ", + "type": "module", + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run", + "test:types": "tsc --noEmit -p tsconfig.test-d.json", + "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test-d.json" + }, + "dependencies": { + "@btravstack/di": "catalog:", + "@btravstack/start": "workspace:*", + "@btravstack/start-example-order-application": "workspace:*", + "@btravstack/start-example-order-domain": "workspace:*", + "@btravstack/start-example-order-infrastructure": "workspace:*", + "@btravstack/start-example-order-temporal-contract": "workspace:*", + "@temporal-contract/client": "catalog:", + "@temporal-contract/worker": "catalog:", + "@temporalio/activity": "catalog:", + "@temporalio/client": "catalog:", + "@temporalio/common": "catalog:", + "@temporalio/worker": "catalog:", + "@temporalio/workflow": "catalog:", + "@unthrown/standard-schema": "catalog:", + "unthrown": "catalog:", + "zod": "catalog:" + }, + "devDependencies": { + "@btravstack/tsconfig": "catalog:", + "@temporal-contract/testing": "catalog:", + "@temporalio/testing": "catalog:", + "@types/node": "catalog:", + "@unthrown/vitest": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + } +} diff --git a/examples/order-temporal/src/env.spec.ts b/examples/order-temporal/src/env.spec.ts new file mode 100644 index 0000000..e3cd9f4 --- /dev/null +++ b/examples/order-temporal/src/env.spec.ts @@ -0,0 +1,108 @@ +import { P } from "unthrown"; +import { describe, expect, it } from "vitest"; + +import { describeEnvIssues, readEnv } from "./env.js"; + +describe("readEnv", () => { + it("falls back to the documented defaults when nothing is set", () => { + // GIVEN an environment with none of the three variables set + const source = {}; + + // WHEN it is validated + const env = readEnv(source); + + // THEN all three carry their defaults, the port as a number + expect(env).toBeOkWith({ + PROBE_PORT: 9000, + TEMPORAL_ADDRESS: "127.0.0.1:7233", + TEMPORAL_NAMESPACE: "default", + }); + }); + + it("reads what a deployment actually supplies", () => { + // GIVEN all three set, as the strings an environment always holds + const source = { + PROBE_PORT: "0", + TEMPORAL_ADDRESS: "temporal.internal:7233", + TEMPORAL_NAMESPACE: "orders", + }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN they arrive parsed, and `0` survives as the ephemeral bind it is + expect(env).toBeOkWith({ + PROBE_PORT: 0, + TEMPORAL_ADDRESS: "temporal.internal:7233", + TEMPORAL_NAMESPACE: "orders", + }); + }); + + it("reports a malformed value rather than binding a probe nobody can find", () => { + // GIVEN the value `Number()` would silently turn into `NaN`, and a + // namespace that is present but empty + const source = { PROBE_PORT: "abc", TEMPORAL_NAMESPACE: "" }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN neither reaches the runtime: both are issues in the error channel, + // named and in order, asserted on the `Err` itself rather than behind an + // `env.isErr() &&` guard that evaluates to `false` when it does not hold + expect(env).toBeErrWith([ + expect.objectContaining({ path: ["PROBE_PORT"] }), + expect.objectContaining({ path: ["TEMPORAL_NAMESPACE"] }), + ]); + }); + + it("rejects a probe port that is present but empty, rather than defaulting it", () => { + // GIVEN the variable set to nothing — `Number("")` is `0`, which `min(0)` + // cannot catch because an ephemeral bind is legal + const source = { PROBE_PORT: "" }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN it is a configuration error rather than an absent variable: the + // default is for a variable nobody set, not one set to nothing + expect(env).toBeErrWith([expect.objectContaining({ path: ["PROBE_PORT"] })]); + }); + + it("rejects a probe port that is present but blank", () => { + // GIVEN whitespace, which `Number()` reads as `0` just as readily + const source = { PROBE_PORT: " " }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN the guard trims before it measures, so this is the empty case + expect(env).toBeErrWith([expect.objectContaining({ path: ["PROBE_PORT"] })]); + }); + + it("rejects a probe port that is a number but not a whole one", () => { + // GIVEN a value `Number()` reads happily and no socket could ever bind + const source = { PROBE_PORT: "3.5" }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN it is an issue rather than a silent truncation + expect(env).toBeErrWith([expect.objectContaining({ path: ["PROBE_PORT"] })]); + }); + + it("rejects a port number no socket could ever bind", () => { + // GIVEN a number that parses but that nothing could listen on + const source = { PROBE_PORT: "70000" }; + + // WHEN its error channel is folded into the message a deployment reads + const described = readEnv(source).match({ + ok: () => "WRONGLY ACCEPTED", + // oxlint-disable-next-line unthrown/no-catch-all-pattern -- `E` is one type (the schema's list of issues), not a union of cases to enumerate + errCases: (matcher) => matcher.with(P._, describeEnvIssues), + defect: () => "defect", + }); + + // THEN the ceiling is the schema's business, not the socket's + expect(described).toContain("PROBE_PORT: Too big"); + }); +}); diff --git a/examples/order-temporal/src/env.ts b/examples/order-temporal/src/env.ts new file mode 100644 index 0000000..fdfe0e0 --- /dev/null +++ b/examples/order-temporal/src/env.ts @@ -0,0 +1,64 @@ +import { fromSchema, type SchemaIssues } from "@unthrown/standard-schema"; +import type { Result } from "unthrown"; +import { z } from "zod"; + +/** + * A whole number, read the way an environment variable actually arrives: as a + * string. + * + * The non-empty string in front of the coercion is the load-bearing part. + * Coercion is `Number()` underneath, and `Number("")` is `0` — so a bare + * `PROBE_PORT=` would bind the ephemeral port `0`, a probe endpoint nobody can + * find, and `min(0)` cannot catch it because an ephemeral bind has to stay + * expressible. An empty value is a configuration error, not an absent one: + * `.default(...)` applies only when the variable is genuinely missing. + * + * With that guard, the bounds handle the rest — `abc` is `NaN`, `3.5` is not an + * integer, and anything outside `min`/`max` is out of range. + */ +const wholeNumber = (fallback: number, min: number, max: number) => + z + .string() + .trim() + .min(1) + .pipe(z.coerce.number().int().min(min).max(max)) + .default(fallback); + +const environment = z.object({ + PROBE_PORT: wholeNumber(9000, 0, 65_535), + /** `host:port` of the Temporal frontend service. */ + TEMPORAL_ADDRESS: z.string().min(1).default("127.0.0.1:7233"), + /** + * The namespace this worker polls in. Half of the pair the runtime publishes + * on `Serving.info`, and half of what decides which work it is ever handed. + */ + TEMPORAL_NAMESPACE: z.string().min(1).default("default"), +}); + +/** The validated environment: every field present, typed, and in range. */ +export type Env = z.infer; + +// `fromSchema` is CURRIED — it takes the schema and hands back the validator. +const validate = fromSchema(environment); + +/** + * Validates the process environment **as a value**. + * + * A schema's own `.parse()` throws, which `unthrown/no-throw` bans and which + * would contradict the example it appears in. `@unthrown/standard-schema` makes + * the issues the modeled `E`, so the entry point folds a bad environment the + * same way it folds any other anticipated failure. + */ +export const readEnv = (source: typeof process.env = process.env): Result => + validate(source); + +const nameOf = (segment: NonNullable[number]): string => + String(typeof segment === "object" ? segment.key : segment); + +/** One line per issue, each naming the variable it is about. */ +export const describeEnvIssues = (issues: SchemaIssues): string => + issues + .map( + (issue) => `${(issue.path ?? []).map(nameOf).join(".") || "(environment)"}: ${issue.message}`, + ) + .join("\n"); diff --git a/examples/order-temporal/src/index.ts b/examples/order-temporal/src/index.ts new file mode 100644 index 0000000..32d898a --- /dev/null +++ b/examples/order-temporal/src/index.ts @@ -0,0 +1,7 @@ +export { OrderTemporalModule } from "./module.js"; +export { + temporalWorkerRuntime, + type OrderTemporalInfo, + type OrderTemporalOptions, + type WorkflowSource, +} from "./temporal-runtime.js"; diff --git a/examples/order-temporal/src/main.ts b/examples/order-temporal/src/main.ts new file mode 100644 index 0000000..91cadb6 --- /dev/null +++ b/examples/order-temporal/src/main.ts @@ -0,0 +1,94 @@ +import { runMain, start } from "@btravstack/start"; +import { orderContract } from "@btravstack/start-example-order-temporal-contract"; +import { workflowsPathFromURL } from "@temporal-contract/worker/worker"; +import { NativeConnection } from "@temporalio/worker"; +import { OkAsync, P, fromSafePromise, type AsyncResult } from "unthrown"; + +import { describeEnvIssues, readEnv, type Env } from "./env.js"; +import { OrderTemporalModule } from "./module.js"; +import { temporalWorkerRuntime } from "./temporal-runtime.js"; + +/** + * The third process, and — apart from the runtime it names and the connection + * it opens — the same one the other two `main.ts` files are: validate the + * environment, build the graph, serve it, and turn the exit report into a + * process exit code. + * + * The connection is opened here for the reason `order-worker` creates its queue + * here: it is the *transport*, and a runtime is handed one rather than owning + * its lifetime. `workflowsPathFromURL` points Temporal at the workflow module + * so it can bundle it for the sandbox; a spec hands over a prebuilt bundle + * instead, which is why `WorkflowSource` has two arms. + * + * Whoever opens it closes it, which is why the close is **here** and not in the + * runtime's `stop()`. The runtime is handed a connection it did not open and + * has no claim on: `src/test-fixtures.ts` boots a fresh worker per test against + * the *one* `testEnv.nativeConnection` the whole file shares, so a runtime that + * closed what it was given would tear the test environment down under the next + * test. The asymmetry a runtime closing it would introduce is the smell; the + * symmetry is the rule. + * + * Typechecked by the gate, not executed by it — the example packages are + * source-only, and every spec drives `start` directly. + */ +const work = (env: Env): AsyncResult => + // A frontend service that will not answer is not an anticipated outcome of + // *this* deployment — there is no domain error to name it — so it rides the + // defect channel and the fold below turns it into an exit code. + fromSafePromise(NativeConnection.connect({ address: env.TEMPORAL_ADDRESS })).flatMap( + (connection) => + fromSafePromise( + runMain( + start(OrderTemporalModule, { + runtime: temporalWorkerRuntime({ + contract: orderContract, + connection, + namespace: env.TEMPORAL_NAMESPACE, + workflows: { workflowsPath: workflowsPathFromURL(import.meta.url, "./workflows.js") }, + }), + probes: { port: env.PROBE_PORT }, + }), + // `.finally`, not a `flatTap`: an open `NativeConnection` holds the + // event loop, so a startup that ends in a defect is exactly the path + // that must still close it. `runMain`'s bare `Promise` is the one + // place a native combinator belongs — it is the documented boundary + // where the Result world ends — and `close` never rejects, so the + // exit code `runMain` just set survives. + ).finally(() => close(connection)), + ), + ); + +/** + * Closing is teardown, and teardown must not rewrite the outcome: `runMain` has + * already set the exit code by the time this runs, so a connection that will + * not close is reported and no more. Letting it become a defect would replace a + * clean `0` — or a considered `2` for abandoned work — with `EX_CONFIG`. + * + * The kernel makes the same distinction with `ExitReport.teardownErrors`, which + * are collected and never mask the reason the application stopped. + */ +const close = (connection: NativeConnection): AsyncResult => + fromSafePromise(connection.close()).recoverDefect((cause) => { + process.stderr.write(`the Temporal connection did not close cleanly: ${String(cause)}\n`); + return OkAsync(); + }); + +/** sysexits(3) `EX_CONFIG`: the deployment is wrong, not the code. */ +const abort = (reason: string): void => { + process.stderr.write(`${reason}\n`); + process.exitCode = 78; +}; + +await readEnv().match({ + ok: (env) => + work(env).match({ + ok: () => {}, + // Nothing can land in the error channel — `work` is typed + // `AsyncResult` — so the matcher has no case to name. + errCases: (matcher) => matcher, + defect: (cause) => abort(`could not reach the Temporal service: ${String(cause)}`), + }), + // oxlint-disable-next-line unthrown/no-catch-all-pattern -- `E` is the issues array: one type with no discriminant, so there is nothing to enumerate and the single arm IS the enumeration + errCases: (matcher) => matcher.with(P._, (issues) => abort(describeEnvIssues(issues))), + defect: (cause) => abort(`the environment could not be validated: ${String(cause)}`), +}); diff --git a/examples/order-temporal/src/module.ts b/examples/order-temporal/src/module.ts new file mode 100644 index 0000000..a806d5c --- /dev/null +++ b/examples/order-temporal/src/module.ts @@ -0,0 +1,27 @@ +import { Module } from "@btravstack/di"; +import { + ApplicationModule, + FindOrder, + Logger, + PlaceOrder, +} from "@btravstack/start-example-order-application"; +import { PersistenceModule } from "@btravstack/start-example-order-infrastructure"; + +/** + * The composition root of the third deployment — and, imports and exports + * alike, the same one `OrderApiModule` and `OrderWorkerModule` are. That is the + * claim this package exists to make: `ApplicationModule` and `PersistenceModule` + * are booted here unchanged, under a runtime that speaks a durable execution + * engine instead of HTTP or a queue. + * + * Declared here rather than imported from a sibling for the reason + * `order-worker` states: sharing the module would share that deployment's + * transport dependency, and a Temporal worker that installs a web server to + * reach its use cases would falsify the very thing this demonstrates. Three + * processes, three composition roots, one application. + */ +export const OrderTemporalModule = Module("OrderTemporal")({ + imports: [ApplicationModule, PersistenceModule], + provides: [], + exports: [PlaceOrder, FindOrder, Logger], +}); diff --git a/examples/order-temporal/src/needs-gate.test-d.ts b/examples/order-temporal/src/needs-gate.test-d.ts new file mode 100644 index 0000000..b9c0c36 --- /dev/null +++ b/examples/order-temporal/src/needs-gate.test-d.ts @@ -0,0 +1,57 @@ +/** + * The compile-time half of the third deployment: `temporalWorkerRuntime` + * declares two ports in `needs`, and `start`'s phantom rest-tuple gate turns a + * module that does not export both into a call-site arity error. Type-checked + * by this package's `test:types` script, never executed. + * + * Together with `order-api`'s and `order-worker`'s, this is what makes the + * claim testable rather than asserted: three runtimes with non-empty `needs`, + * all proven against the same application graph at the `start(...)` call site. + */ +import { Module } from "@btravstack/di"; +import { start } from "@btravstack/start"; +import { + ApplicationModule, + FindOrder, + Logger, + PlaceOrder, +} from "@btravstack/start-example-order-application"; +import { PersistenceModule } from "@btravstack/start-example-order-infrastructure"; +import { orderContract } from "@btravstack/start-example-order-temporal-contract"; +import type { NativeConnection } from "@temporalio/worker"; + +import { OrderTemporalModule } from "./module.js"; +import { temporalWorkerRuntime } from "./temporal-runtime.js"; + +// Never dereferenced: this file is checked, not run. The runtime's `needs` are +// what the gate reads, and they do not depend on a live connection. +declare const connection: NativeConnection; + +const options = { + runtime: temporalWorkerRuntime({ + contract: orderContract, + connection, + workflows: { workflowsPath: "./workflows.js" }, + }), + signals: false, + probes: false, +} as const; + +// Positive: the composition root exports both ports the runtime needs (and a +// third it does not), so the gate collapses to an empty tuple and this is an +// ordinary two-argument call. +const _wired = start(OrderTemporalModule, options); + +// The same graph, one port short: `Logger` is provided (the interactors depend +// on it) but not exported, so it is not in the application context the runtime +// is handed. +const PartialTemporal = Module("PartialTemporal")({ + imports: [ApplicationModule, PersistenceModule], + provides: [], + exports: [PlaceOrder, FindOrder], +}); + +// Negative: the gate becomes a required two-element tuple naming the unmet need, +// and the call fails on arity. +// @ts-expect-error — UNSATISFIED RUNTIME NEEDS: the module does not export Logger. +const _missingLogger = start(PartialTemporal, options); diff --git a/examples/order-temporal/src/temporal-runtime.spec.ts b/examples/order-temporal/src/temporal-runtime.spec.ts new file mode 100644 index 0000000..1aa7d48 --- /dev/null +++ b/examples/order-temporal/src/temporal-runtime.spec.ts @@ -0,0 +1,227 @@ +import { + tagPatterns, + WORKFLOW_RESULT_ERROR_TAGS, + WORKFLOW_START_ERROR_TAGS, +} from "@temporal-contract/client"; +import { describe, expect, vi } from "vitest"; + +import { OrderTemporalModule } from "./module.js"; +import { it } from "./test-fixtures.js"; + +describe("temporalWorkerRuntime", () => { + it("places an order through a workflow and an activity", async ({ serve }) => { + // GIVEN the same composition the API and the queue worker boot — + // `ApplicationModule` and `PersistenceModule`, unchanged — under a Temporal + // worker instead + const { client } = await serve(OrderTemporalModule); + + // WHEN a workflow execution is driven to completion + // THEN the order came back through a real Workflow Task, a real Activity + // Task, and the use case behind both + await expect( + client.executeWorkflow("placeOrder", { + workflowId: "wf-place-1", + args: { orderId: "o-1", quantity: 2 }, + }), + ).toBeOkWith({ id: "o-1", quantity: 2 }); + }); + + it("hands the client the DuplicateOrder the API answers CONFLICT for, as a typed contract error", async ({ + serve, + }) => { + // GIVEN an order already placed by a first execution + const { client } = await serve(OrderTemporalModule); + + // WHEN a second execution asks for the same order id — chained, so the + // first execution's result is consumed and a failure there cannot be + // mistaken for the duplicate + const outcome = await client + .executeWorkflow("placeOrder", { + workflowId: "wf-dup-1", + args: { orderId: "o-1", quantity: 2 }, + }) + .flatMap(() => + client.executeWorkflow("placeOrder", { + workflowId: "wf-dup-2", + args: { orderId: "o-1", quantity: 2 }, + }), + ) + .match({ + ok: () => "WRONGLY PLACED", + // THEN the load-bearing assertion of this package: the identical `Err` + // the oRPC runtime turns into an inferable CONFLICT and the queue + // worker parks is here a **branchable value at the client**, rehydrated + // by name with its payload intact. Folded through an exhaustive match + // rather than shape-matched, because "the client can branch on it" is + // the claim and this is what branching on it looks like. + errCases: (matcher) => + matcher + .with({ errorName: "InvalidQuantity" }, (error) => `invalid:${error.data.id}`) + .with({ errorName: "OrderAlreadyPlaced" }, (error) => `conflict:${error.data.id}`) + .with(...tagPatterns(WORKFLOW_START_ERROR_TAGS), (error) => `start:${error._tag}`) + .with(...tagPatterns(WORKFLOW_RESULT_ERROR_TAGS), (error) => `result:${error._tag}`), + defect: (cause) => `defect:${String(cause)}`, + }); + + expect(outcome).toBe("conflict:o-1"); + }); + + it("hands the client the InvalidQuantity the domain rejects, by its own name", async ({ + serve, + }) => { + // GIVEN a quantity the domain invariant rejects + const { client } = await serve(OrderTemporalModule); + + // WHEN the execution runs + const outcome = await client + .executeWorkflow("placeOrder", { + workflowId: "wf-invalid-1", + args: { orderId: "o-1", quantity: 0 }, + }) + .match({ + ok: () => "WRONGLY PLACED", + errCases: (matcher) => + matcher + .with({ errorName: "InvalidQuantity" }, (error) => `invalid:${error.data.id}`) + .with({ errorName: "OrderAlreadyPlaced" }, (error) => `conflict:${error.data.id}`) + .with(...tagPatterns(WORKFLOW_START_ERROR_TAGS), (error) => `start:${error._tag}`) + .with(...tagPatterns(WORKFLOW_RESULT_ERROR_TAGS), (error) => `result:${error._tag}`), + defect: (cause) => `defect:${String(cause)}`, + }); + + // THEN it arrives under its own name rather than the duplicate's or an + // opaque failure, carrying the order it was about + expect(outcome).toBe("invalid:o-1"); + }); + + it("lets Temporal retry an unmodelled failure, which is what a retry policy is for", async ({ + serve, + unmodelled, + }) => { + // GIVEN a repository whose failure nobody modelled, so it is a `Defect` + const { client } = await serve(unmodelled.module); + + // WHEN an execution reaches it and runs out of attempts + const placed = await client.executeWorkflow("placeOrder", { + workflowId: "wf-defect-1", + args: { orderId: "o-1", quantity: 1 }, + }); + + // THEN the third channel takes a third route again, and this time the + // platform owns it: an unnamed failure is retried up to the contract's + // `maximumAttempts` of 3, where the two named ones are declared + // `nonRetryable` and are asked exactly once. The queue worker hand-rolls + // this with an attempt budget; here it is a line of contract. + expect({ failed: placed.isErr(), attempts: unmodelled.attempts() }).toEqual({ + failed: true, + attempts: 3, + }); + }); + + it("publishes the task queue and namespace it polls on Serving.info", async ({ serve }) => { + // GIVEN a worker on a task queue of this test's own + const { app } = await serve(OrderTemporalModule); + + // WHEN the kernel is asked what the runtime published about itself + const info = app.runtimeInfo(); + + // THEN the same channel the API publishes `{ port, prefix }` on and the + // queue worker `{ queue, concurrency }` carries the pair that identifies a + // Temporal worker — and no two of the three shapes share a field + await expect(info).toBeOkWith({ + taskQueue: expect.stringMatching(/^orders-\d+$/u), + namespace: "default", + }); + }); + + it("runs each workflow execution in its own unit, with its own trace id", async ({ + serve, + tapped, + }) => { + // GIVEN the real graph with the very `Logger` instance the use cases write to + const { client } = await serve(tapped.module); + + // WHEN two executions are driven to completion — chained, so neither result + // is dropped + const placed = await client + .executeWorkflow("placeOrder", { + workflowId: "wf-trace-1", + args: { orderId: "o-1", quantity: 1 }, + }) + .flatMap(() => + client.executeWorkflow("placeOrder", { + workflowId: "wf-trace-2", + args: { orderId: "o-2", quantity: 1 }, + }), + ); + + // THEN two executions, two units, two distinct trace ids — each the workflow + // id its activity ran under, and never the out-of-unit `[-]`. The unit id is + // Temporal's own task token, unique per *attempt*; the workflow id is the + // correlation id, which deliberately is not. + expect(placed.map(() => tapped.traces())).toBeOkWith(["[wf-trace-1]", "[wf-trace-2]"]); + }); + + it("lets an in-flight activity finish while draining", async ({ serve, gate }) => { + // GIVEN an activity held open inside the repository. `startWorkflow`, not + // `executeWorkflow`: a drained worker stops polling Workflow Tasks too, so + // the execution never reaches a terminal state and awaiting its result would + // hang the test on a fact that is not under test. + const { app, client } = await serve(gate.module); + const started = client.startWorkflow("placeOrder", { + workflowId: "wf-drain-1", + args: { orderId: "o-1", quantity: 1 }, + }); + await gate.arrived; + + // WHEN the drain starts and the activity is released only once the phase + // moved. `vi.waitUntil` synchronises rather than asserts — the drain samples + // `inFlightAtStart` in the same synchronous turn that advances the phase, so + // releasing afterwards is what makes the report exact rather than racy. + app.requestDrain(); + await vi.waitUntil(() => app.phase() === "draining"); + gate.release(); + + // THEN the payoff, and the first time the kernel's drain contract meets a + // real worker's shutdown semantics: `worker.shutdown()` stops polling for + // new tasks the instant it is called, `run()` resolves only once the + // in-flight activity has finished, and the kernel's own accounting agrees — + // nothing abandoned. Read through the started execution so its `Result` is + // consumed rather than dropped. + const report = await started.flatMap(() => app.exited); + + expect(report).toBeOkWith( + expect.objectContaining({ drain: { inFlightAtStart: 1, completed: 1, abandoned: 0 } }), + ); + }); + + it("releases the runtime at the kernel's deadline when an activity will not finish", async ({ + serve, + gate, + }) => { + // GIVEN an activity held open with nothing to release it, and a drain + // budget of a tenth of a second — so `worker.run()` cannot settle inside + // it and the only way out is the deadline signal + const { app, client } = await serve(gate.module, { drainTimeoutMs: 100 }); + const started = client.startWorkflow("placeOrder", { + workflowId: "wf-stuck-1", + args: { orderId: "o-1", quantity: 1 }, + }); + await gate.arrived; + + // WHEN the drain runs out of time + app.requestDrain(); + const askedAt = Date.now(); + const report = await started.flatMap(() => app.exited); + const elapsedMs = Date.now() - askedAt; + + // THEN the kernel's exit is not held hostage by a worker that cannot stop: + // the activity is reported abandoned and the process is released on the + // kernel's own deadline rather than on Temporal's 30-second `forceAfter`, + // which is what `Serving.drain(signal)` promises the kernel. + expect(report.map((exit) => ({ drain: exit.drain, promptly: elapsedMs < 5_000 }))).toBeOkWith({ + drain: { inFlightAtStart: 1, completed: 0, abandoned: 1 }, + promptly: true, + }); + }); +}); diff --git a/examples/order-temporal/src/temporal-runtime.ts b/examples/order-temporal/src/temporal-runtime.ts new file mode 100644 index 0000000..0d32a27 --- /dev/null +++ b/examples/order-temporal/src/temporal-runtime.ts @@ -0,0 +1,294 @@ +import { + RuntimeStartFailed, + type Runtime, + type RuntimeHost, + type Serving, + type UnitMeta, +} from "@btravstack/start"; +import { Logger, PlaceOrder } from "@btravstack/start-example-order-application"; +import type { OrderContract } from "@btravstack/start-example-order-temporal-contract"; +import { + declareActivitiesHandler, + type ActivityImplementationFor, +} from "@temporal-contract/worker/activity"; +import { TypedWorker } from "@temporal-contract/worker/worker"; +import { activityInfo } from "@temporalio/activity"; +import type { Duration } from "@temporalio/common"; +import type { NativeConnection, WorkflowBundleWithSourceMap } from "@temporalio/worker"; +import { ErrAsync, OkAsync, P, fromSafePromise, type AsyncResult } from "unthrown"; + +/** + * What the worker publishes about itself once it is polling, read back through + * `RunningApp.runtimeInfo()`. + * + * A Temporal worker's identity **is** its task queue and namespace — the pair + * an operator needs to find it in the Web UI, and the pair that decides which + * work it will ever be handed. The API publishes `{ port, prefix }` and the + * queue worker `{ queue, concurrency }` on the same channel; no two of the + * three shapes share a field, which is why `Info` is the runtime's own type + * parameter rather than anything the kernel models. + */ +export type OrderTemporalInfo = { readonly taskQueue: string; readonly namespace: string }; + +/** + * Where the workflow sandbox's code comes from. Two arms because the two + * callers genuinely differ: a process points at the module and lets Temporal + * bundle it, while a spec hands over a bundle `bundleFor` already built and + * memoised — bundling per test is the single most expensive thing in this suite. + */ +export type WorkflowSource = + | { readonly workflowsPath: string } + | { readonly workflowBundle: WorkflowBundleWithSourceMap }; + +export type OrderTemporalOptions = { + /** + * The contract, and with it the task queue this worker polls — + * `TypedWorker.create` reads `taskQueue` off it rather than taking one. + */ + readonly contract: OrderContract; + /** + * An open connection to the Temporal service. Handed in rather than opened + * here, exactly as the queue runtime is handed its broker: `main.ts` builds + * one from the environment, and a spec passes the test environment's. + */ + readonly connection: NativeConnection; + readonly namespace?: string; + readonly workflows: WorkflowSource; + /** + * How long an activity that is *cancellation-aware* gets to notice the + * shutdown. Set explicitly because Temporal's default is `0`, and cancelling + * the instant `drain` is called would fight the kernel's own drain deadline + * for the same decision. Default `10 seconds`. + */ + readonly gracePeriod?: Duration; + /** + * The hard stop. After it, `run()` settles with a + * `GracefulShutdownPeriodExpiredError` and in-flight work is abandoned where + * it stands. + * + * It is Temporal's **own** clock, not the kernel's, and the two are not the + * same deadline: the kernel releases this runtime the moment its + * `drainTimeoutMs` passes (see `poll` below), whatever the worker is still + * doing. Set this at or below `drainTimeoutMs` if you want the worker to have + * finished forcing itself down by the time the kernel gives up on it. + * Default `30 seconds`. + */ + readonly forceAfter?: Duration; +}; + +/** + * The ports this runtime resolves out of the application context — the same two + * the queue worker needs, and for the same reason: `FindOrder` is not part of + * any activity this worker registers, and a runtime declares what *it* needs + * rather than what the module happens to export. + * + * Non-empty on purpose: it is what makes `start`'s arity gate mean something + * (`src/needs-gate.test-d.ts` pins both directions). + */ +type TemporalNeeds = typeof PlaceOrder | typeof Logger; + +/** + * A `Runtime` serving the order application as a Temporal worker. + * + * - `start` builds the worker (bundling the workflow module, connecting the + * poller) and begins the worker loop. A bundle that will not compile or a + * service that will not answer is a modeled `Err(RuntimeStartFailed)`, never + * a throw — `TypedWorker.create` reports both as a defect carrying a + * `TechnicalError`, and this is the one place where that is a *startup* + * failure rather than an unmodelled one. + * - `Serving.drain` calls `worker.shutdown()`, which moves the worker to + * `DRAINING` immediately: polling for new Workflow and Activity Tasks stops + * at once, in-flight activities run to completion, and `run()` resolves when + * the last of them has. So the drain is a genuine wait, not a courtesy — and + * it is bounded by the kernel's deadline signal rather than only by the + * worker's own `forceAfter`. + * - `Serving.stop` is the same call made idempotent. Once the worker has + * drained, `running` has already settled and awaiting it again returns at + * once; when `stop()` is the *only* call — the path a `stop()` that skipped + * the drain takes — it is what shuts the worker down, with `forceAfter` as + * the ceiling. + */ +export const temporalWorkerRuntime = ( + options: OrderTemporalOptions, +): Runtime => ({ + name: "temporal-worker", + needs: [PlaceOrder, Logger], + start: (host) => createWorker(host, options), +}); + +const createWorker = ( + host: RuntimeHost, + options: OrderTemporalOptions, +): AsyncResult, RuntimeStartFailed> => { + const namespace = options.namespace ?? "default"; + + return TypedWorker.create({ + contract: options.contract, + connection: options.connection, + namespace, + ...options.workflows, + activities: declareActivitiesHandler({ + contract: options.contract, + activities: { placeOrder: { place: placeActivity(host) } }, + }), + shutdownGraceTime: options.gracePeriod ?? "10 seconds", + shutdownForceTime: options.forceAfter ?? "30 seconds", + }) + .map((worker) => poll(worker, options.contract.taskQueue, namespace)) + .recoverDefect((cause) => + ErrAsync(new RuntimeStartFailed({ runtime: "temporal-worker", cause })), + ); +}; + +const poll = ( + worker: TypedWorker, + taskQueue: string, + namespace: string, +): Serving => { + // `Worker.run()` moves the state to RUNNING synchronously, before its first + // await, so the worker is already polling by the time this returns — which is + // what lets `stopPolling` below trust `getState()`. + // + // The result is HELD, not dropped: `run()` is `AsyncResult`, and + // an empty error channel is not an empty defect channel. Both `drain` and + // `stop` hand it to the kernel, which is what consumes it. + const running = worker.run(); + + // `shutdown()` on a worker that is not RUNNING throws Temporal's + // `IllegalStateError`, and both methods below can reach it — on the signal + // path `stop` always runs after `drain` has already shut the worker down. + const stopPolling = (): void => { + if (worker.raw.getState() === "RUNNING") worker.shutdown(); + }; + + // The kernel's deadline, kept from `drain` so `stop` is released by the same + // abort. Without it the release would only be half done: `finish` calls + // `stop()` after the drain has already timed out, and a `stop` that started + // waiting on `running` all over again would put the worker's own + // `forceAfter` back in charge of when the process exits. + let deadline: AbortSignal | undefined; + + const stopped = (): AsyncResult => + deadline === undefined ? running : releasedBy(deadline, running); + + return { + info: { taskQueue, namespace }, + // `@temporalio/worker` 1.22 has no public forced-shutdown call — + // `Worker.forceShutdown$` is `protected` and `Runtime.shutdown()` is + // process-global — so the escalation available to a runtime is to stop + // waiting: the kernel is handed back its thread at its own deadline, and + // the worker is left to Temporal's `forceAfter` clock and to the entry + // point closing the connection underneath it. + drain: (signal) => { + deadline = signal; + stopPolling(); + return stopped(); + }, + stop: () => { + stopPolling(); + return stopped(); + }, + }; +}; + +/** + * `running`, but no later than the kernel's drain deadline. + * + * `Serving.drain(signal)` is a contract, not a courtesy: the kernel races its + * timeout against the runtime having stopped and aborts `signal` the instant + * that race settles, precisely so a runtime that treats it as its cue to return + * can be released. A worker whose activity never finishes cannot honour it by + * waiting on `run()` — that resolves on Temporal's clock, not the kernel's. + * + * The losing branch's `Result` is dropped, and it is the one drop here: when + * the deadline wins, the kernel has already decided the report and settled + * `exited`, so the worker's eventual outcome has no consumer left — and an + * `AsyncResult` never rejects, so nothing floats. That is the same trade + * `drainApp` documents for its own race. + */ +const releasedBy = ( + signal: AbortSignal, + running: AsyncResult, +): AsyncResult => + fromSafePromise(Promise.race([running, whenAborted(signal)])).flatMap((settled) => settled); + +const whenAborted = (signal: AbortSignal): AsyncResult => + signal.aborted + ? OkAsync() + : fromSafePromise( + new Promise((resolve) => { + signal.addEventListener("abort", () => resolve(), { once: true }); + }), + ); + +/** + * The one activity, and the hinge of this whole example. + * + * It is built as a **factory over the `RuntimeHost`** rather than declared at + * module scope, because a Temporal activity is where the process leaves the + * workflow sandbox and reaches real services — so it is the natural place to + * open a kernel unit, and closing over `host` is how the di context gets there + * without a module-level global. + * + * The `mapErrCases` is the triage point, and the third sibling of + * `order-api`'s into `ORPCError` codes and `order-worker`'s into + * ack/dead-letter. The same `Err` lands somewhere else again: `DuplicateOrder` + * is a `CONFLICT` over HTTP because a caller is waiting to be told, and a + * dead-letter on a queue because none is. Here there *is* a caller — a + * workflow, and behind it a client — so it becomes a typed contract error the + * client branches on by name. What is new is the second thing this mapping + * decides: `contract.ts` declares both of these `nonRetryable`, so naming a + * failure here is also what tells **Temporal** to stop retrying it. An + * unmodelled failure stays unnamed and the retry policy takes over — the + * platform doing for free what the queue worker hand-rolls with an attempt + * budget. + * + * Every case is named. A new domain error is a compile error here, at the one + * place that has to decide what the workflow sees. + */ +const placeActivity = + ( + host: RuntimeHost, + ): ActivityImplementationFor => + (args, { errors }) => + host.run(metaFor(), (ctx, _signal) => + ctx + .get(PlaceOrder) + .execute(args.orderId, args.quantity) + .map((order) => ({ id: order.id, quantity: order.quantity })) + .mapErrCases((matcher) => + matcher + .with(P.tag("InvalidQuantity"), (error) => errors.InvalidQuantity({ id: error.id })) + .with(P.tag("DuplicateOrder"), (error) => errors.OrderAlreadyPlaced({ id: error.id })), + ), + ); + +/** + * `UnitMeta.id` must be unique per unit, and a **workflow id is not one** — + * which is worth stating because it is the obvious choice here and it is wrong + * twice over: an activity is retried under the same execution, and Temporal + * lets a workflow id be reused once an execution has closed. The queue worker + * answers the same problem by making the *delivery* the id rather than the + * message; here the platform already mints exactly that value. A **task token** + * identifies one activity task attempt, so its uniqueness is Temporal's + * guarantee rather than an argument of ours. + * + * The workflow id becomes the `traceId`, which is what `traceId` is for. It is + * the correlation id, minted outside this process by whoever started the + * execution, and it holds steady across every retry, so all three attempts join + * up in the log. An activity not started by a workflow has none — a case this + * contract never produces, but the SDK types it as possible — and falls back to + * the activity id, which is likewise stable across that activity's attempts. + * + * `activityInfo()` is Temporal's own ambient read, available here because this + * runs inside the activity's execution context — before the kernel opens its + * own. + */ +const metaFor = (): UnitMeta => { + const info = activityInfo(); + return { + kind: "activity", + id: info.base64TaskToken, + traceId: info.workflowExecution?.workflowId ?? info.activityId, + }; +}; diff --git a/examples/order-temporal/src/test-fixtures.ts b/examples/order-temporal/src/test-fixtures.ts new file mode 100644 index 0000000..ac5a952 --- /dev/null +++ b/examples/order-temporal/src/test-fixtures.ts @@ -0,0 +1,257 @@ +import { mkdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import { Module, Port, Provider, type Scope, type ServiceOf } from "@btravstack/di"; +import { start, type RunningApp } from "@btravstack/start"; +import { + ApplicationModule, + FindOrder, + Logger, + OrderRepository, + PlaceOrder, +} from "@btravstack/start-example-order-application"; +import { OrderNotFound } from "@btravstack/start-example-order-domain"; +import { + orderContract, + type OrderContract, +} from "@btravstack/start-example-order-temporal-contract"; +import { TypedClient, type ContractClient } from "@temporal-contract/client"; +import { createTimeSkippingTest } from "@temporal-contract/testing/time-skipping"; +import { + bundleFor, + fixturePath, + nextTaskQueueId, + withTaskQueue, +} from "@temporal-contract/testing/workflow-bundle"; +import type { TestWorkflowEnvironment } from "@temporalio/testing"; +import { ErrAsync, fromSafePromise } from "unthrown"; +import { expect } from "vitest"; + +import { OrderTemporalModule } from "./module.js"; +import { temporalWorkerRuntime, type OrderTemporalInfo } from "./temporal-runtime.js"; + +/** + * The time-skipping server binary, cached where **we** decide rather than in the + * OS temp directory. + * + * `@temporalio/testing` downloads a 64 MB test server keyed by the SDK version, + * and its two defaults are both wrong for a repository gate: the system temp + * directory (which CI wipes between jobs and macOS purges on its own schedule) + * and a `ttl` of one day (so a developer who runs the suite on Monday and again + * on Wednesday downloads it twice). A repo-local, gitignored path with a + * year-long ttl makes the download a once-ever event locally and a cacheable + * path in CI. `temporal-contract` forwards these options through unchanged. + * + * This is the one example in the repository that needs network access on a cold + * cache — see the README. + */ +const downloadDir = fileURLToPath( + new URL("../../../.cache/temporal-test-server/", import.meta.url), +); + +// The Rust ephemeral-server downloader writes into this path; creating it here +// keeps the fixture's failure mode "no network" rather than "no directory". +mkdirSync(downloadDir, { recursive: true }); + +type App = RunningApp; + +/** + * `X` is pinned to the three ports the composition roots export rather than + * left generic: `start`'s needs gate is a phantom rest parameter proven at the + * call site, and no proof is available inside a helper generic in the module's + * own exports. The runtime needs only two of them. + */ +type TemporalPorts = PlaceOrder | FindOrder | Logger; + +/** One booted deployment: the kernel's handle, and a client that can reach it. */ +type Deployment = { + readonly app: App; + readonly client: ContractClient; +}; + +/** + * The kernel options a test may override. Only the drain budget so far — a test + * that strands an activity needs the deadline to arrive in milliseconds rather + * than in the default twenty seconds. + */ +type ServeOptions = { readonly drainTimeoutMs: number }; + +type Serve = ( + module: Module, + options?: ServeOptions, +) => Promise>; + +const persistenceOf = (repository: ServiceOf) => + Module("StubPersistence")({ + provides: [Provider(OrderRepository)({ value: repository })], + exports: [OrderRepository], + }); + +/** + * A composition root shaped like the real one but with the repository swapped: + * same `ApplicationModule`, same runtime, same three exported ports, so the + * transport under test is unchanged. + */ +const temporalWith = (repository: ServiceOf) => + Module("StubTemporal")({ + imports: [ApplicationModule, persistenceOf(repository)], + provides: [], + exports: [PlaceOrder, FindOrder, Logger], + }); + +/** + * `start` hands the application context to the runtime alone, so a spec cannot + * reach `Logger` the way `Module.scoped` can. This publishes the very `Logger` + * service instance the use cases write to. + */ +class LoggerTap extends Port("LoggerTap")<{ readonly lines: () => readonly string[] }> {} + +const tappedTemporal = () => { + let read: () => readonly string[] = () => []; + + return { + module: Module("TappedTemporal")({ + imports: [OrderTemporalModule], + provides: [ + Provider(LoggerTap)([Logger], { + sync: (logger) => { + read = logger.lines; + return { lines: logger.lines }; + }, + }), + ], + exports: [PlaceOrder, FindOrder, Logger], + }), + traces: (): readonly string[] => read().map((line) => line.slice(0, line.indexOf("]") + 1)), + }; +}; + +/** + * A composition root whose repository fails in a way nobody modelled: no + * `qualify` triaged the rejection, so it is a defect — and a defect is what + * Temporal's own retry policy is for. `attempts()` reports how many times the + * platform came back. + */ +const unmodelledTemporal = () => { + let attempts = 0; + + return { + module: temporalWith({ + save: () => { + attempts += 1; + return fromSafePromise(Promise.reject(new Error("the database is on fire"))); + }, + find: (id) => ErrAsync(new OrderNotFound({ id })), + }), + attempts: (): number => attempts, + }; +}; + +/** + * A repository whose `save` never settles until `release()` is called, and whose + * `arrived` promise reports the moment the activity reached it. The drain spec + * turns on knowing a unit is genuinely in flight before the drain starts — + * polling a wall clock instead would be the flake. + */ +const gatedTemporal = () => { + let entered!: () => void; + const arrived = new Promise((resolve) => { + entered = resolve; + }); + let release!: () => void; + const held = new Promise((resolve) => { + release = resolve; + }); + + return { + module: temporalWith({ + save: (order) => { + entered(); + return fromSafePromise(held.then(() => order)); + }, + find: (id) => ErrAsync(new OrderNotFound({ id })), + }), + arrived, + release: () => release(), + }; +}; + +export type TemporalFixtures = { + readonly testEnv: TestWorkflowEnvironment; + /** + * Boots an app whose Temporal worker polls a task queue of this test's own, + * and registers its shutdown. The teardown runs even when the test fails, + * which is what a `try`/`finally` used to hand-roll — and it keeps the + * assertion those blocks carried: the app exited `Ok`. + */ + readonly serve: Serve; + readonly tapped: ReturnType; + readonly unmodelled: ReturnType; + readonly gate: ReturnType; +}; + +export const it = createTimeSkippingTest({ + server: { executable: { type: "cached-download", downloadDir, ttl: "365d" } }, +}).extend>({ + serve: async ({ testEnv }, use) => { + // Memoised per spec file by `bundleFor`: webpack over the workflow module + // is the single most expensive thing in this suite, and every test needs + // the same bundle. + const workflowBundle = await bundleFor(fixturePath(import.meta.url, "workflows")); + const shutdowns: (() => Promise)[] = []; + + const serve: Serve = async (module, options) => { + // A queue of this test's own: the environment is shared by every test in + // the worker process, and two workers polling one queue would race for + // each other's tasks. + const contract = withTaskQueue(orderContract, nextTaskQueueId("orders")); + + const app = start(module, { + runtime: temporalWorkerRuntime({ + contract, + connection: testEnv.nativeConnection, + workflows: { workflowBundle }, + }), + signals: false, + probes: false, + preDrainDelayMs: 0, + ...options, + }); + + shutdowns.push(async () => { + app.stop(); + await expect(app.exited).toBeOk(); + }); + + // `.get()`, not `.getOrThrow()`: the error channel is empty, so a client + // that could not be built is a defect and rethrowing its cause is what + // should fail the test. + const client = (await TypedClient.create({ client: testEnv.client }).get()).for(contract); + + return { app, client }; + }; + + await use(serve); + + for (const shutdown of shutdowns) await shutdown(); + }, + + // oxlint-disable-next-line no-empty-pattern -- Vitest fixtures require a destructuring pattern; this one depends on no other fixture + tapped: async ({}, use) => { + await use(tappedTemporal()); + }, + + // oxlint-disable-next-line no-empty-pattern -- see above + unmodelled: async ({}, use) => { + await use(unmodelledTemporal()); + }, + + // oxlint-disable-next-line no-empty-pattern -- see above + gate: async ({}, use) => { + const gate = gatedTemporal(); + await use(gate); + // Released on every exit path, so an activity a test deliberately stranded + // cannot outlive the test that stranded it. + gate.release(); + }, +}); diff --git a/examples/order-temporal/src/vitest.d.ts b/examples/order-temporal/src/vitest.d.ts new file mode 100644 index 0000000..ad36daf --- /dev/null +++ b/examples/order-temporal/src/vitest.d.ts @@ -0,0 +1 @@ +import type {} from "@unthrown/vitest"; diff --git a/examples/order-temporal/src/workflows.ts b/examples/order-temporal/src/workflows.ts new file mode 100644 index 0000000..d709b0b --- /dev/null +++ b/examples/order-temporal/src/workflows.ts @@ -0,0 +1,55 @@ +import { orderContract } from "@btravstack/start-example-order-temporal-contract"; +import { + ACTIVITY_CANCELLED_ERROR_TAG, + ACTIVITY_ERROR_TAG, + declareWorkflow, + propagateActivityFailure, +} from "@temporal-contract/worker/workflow"; +import { P } from "unthrown"; + +/** + * The workflow, in its own module — and it has to be. + * + * Workflow code runs inside a deterministic V8 sandbox that webpack bundles + * separately from the worker's own module graph, so it may not sit in a spec + * file, and it must be free of side effects at module scope: `TypedWorker.create` + * also imports it in the main thread to check that every declared workflow is + * exported. Nothing here reaches the di container or the database — those live + * behind the *activity*, which is where this example's kernel unit is opened. + * + * The body is the second half of the triage `contract.ts` describes. An + * activity-declared contract error is rehydrated **here**, and the client never + * sees it; a workflow-declared one is rehydrated **at the client**. So the two + * domain failures are re-minted against `context.errors` and everything else is + * handed to `propagateActivityFailure`, which re-raises Temporal's original + * failure so the platform classifies the execution exactly as it would have if + * the activity call had thrown. + * + * Every case is named — this repo bans `P._`, and the two activity-machinery + * arms are as much a decision as the two domain ones. + */ +export const placeOrder = declareWorkflow({ + workflowName: "placeOrder", + contract: orderContract, + implementation: (context, args) => + propagateActivityFailure( + context.activities + .place({ orderId: args.orderId, quantity: args.quantity }) + .mapErrCases((matcher) => + matcher + .with({ errorName: "InvalidQuantity" }, (error) => + context.errors.InvalidQuantity({ id: error.data.id }), + ) + .with({ errorName: "OrderAlreadyPlaced" }, (error) => + context.errors.OrderAlreadyPlaced({ id: error.data.id }), + ) + // The activity failed for a reason nobody declared, and Temporal + // has already exhausted the contract's retry policy. Handing the + // wrapper straight back lets `propagateActivityFailure` re-raise + // the failure underneath it, so the execution fails the way an + // untyped Temporal workflow would. Both machinery tags are named, + // grouped into one arm because they share a handler. + .with(P.tag(ACTIVITY_ERROR_TAG), P.tag(ACTIVITY_CANCELLED_ERROR_TAG), (error) => error), + ), + ), +}); diff --git a/examples/order-temporal/tsconfig.json b/examples/order-temporal/tsconfig.json new file mode 100644 index 0000000..3faf372 --- /dev/null +++ b/examples/order-temporal/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@btravstack/tsconfig/base.json", + "compilerOptions": { + "noEmit": true, + "types": ["node"], + // Inherited from the persistence layer this package composes: the generated + // Prisma client imports its own files with explicit `.ts` extensions, and + // those files are part of this program too. `noEmit` makes that legal. + "allowImportingTsExtensions": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "src/**/*.test-d.ts"] +} diff --git a/examples/order-temporal/tsconfig.test-d.json b/examples/order-temporal/tsconfig.test-d.json new file mode 100644 index 0000000..619908b --- /dev/null +++ b/examples/order-temporal/tsconfig.test-d.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "noUnusedLocals": false, "noUnusedParameters": false }, + "include": ["src/**/*.test-d.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/order-temporal/vitest.config.ts b/examples/order-temporal/vitest.config.ts new file mode 100644 index 0000000..ddf09c8 --- /dev/null +++ b/examples/order-temporal/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.spec.ts"], + setupFiles: ["@unthrown/vitest"], + // The generous ceiling is for one thing only: the first run on a machine + // that has never downloaded the 64 MB time-skipping test server. Warm, the + // whole file is a couple of seconds — see the README's timings. + testTimeout: 120_000, + hookTimeout: 120_000, + }, +}); diff --git a/examples/order-worker/README.md b/examples/order-worker/README.md index 574b5be..b992c5c 100644 --- a/examples/order-worker/README.md +++ b/examples/order-worker/README.md @@ -55,16 +55,19 @@ ctx .match({ ok: () => ack, errCases: (matcher) => - matcher - .with(P.tag("InvalidQuantity"), (error) => deadLetter(error._tag)) - .with(P.tag("DuplicateOrder"), (error) => deadLetter(error._tag)), + matcher.with(P.tag("InvalidQuantity"), P.tag("DuplicateOrder"), (error) => + deadLetter(error._tag), + ), defect: (cause) => retry(String(cause)), }); ``` -Every case is named — this repo bans `P._`, and there is no `.otherwise()`. A -new domain error is a compile error here **and** in `order-api/src/router.ts`, -at the two places that have to decide what it means. +Every case is named — this repo bans `P._`, and there is no `.otherwise()`. The +two that share a handler are **grouped** into one arm rather than duplicated, +which is what `no-catch-all-pattern` steers you toward instead of a wildcard; +`error` stays the narrowed union of the two, so `error._tag` still names which +one it was. A new domain error is a compile error here **and** in +`order-api/src/router.ts`, at the two places that have to decide what it means. ## Acking is flushing @@ -143,7 +146,7 @@ is which queue it is on and how many messages it will take at a time. ## Running it ```bash -pnpm --filter @btravstack/start-example-order-worker test # 9 runtime specs + 4 env specs +pnpm --filter @btravstack/start-example-order-worker test # 9 runtime specs + 6 env specs pnpm --filter @btravstack/start-example-order-worker test:types # the needs gate ``` diff --git a/examples/order-worker/src/env.spec.ts b/examples/order-worker/src/env.spec.ts index 9c47f26..22c4f73 100644 --- a/examples/order-worker/src/env.spec.ts +++ b/examples/order-worker/src/env.spec.ts @@ -43,6 +43,34 @@ describe("readEnv", () => { ]); }); + it("rejects a variable that is present but blank, rather than defaulting it", () => { + // GIVEN two variables set to whitespace — the shape `Number()` reads as + // `0`, which `PROBE_PORT`'s `min(0)` cannot catch because an ephemeral bind + // is legal + const source = { PROBE_PORT: " ", CONCURRENCY: "\t\n" }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN both are configuration errors rather than absent variables: the + // default is for a variable nobody set, not one set to nothing + expect(env).toBeErrWith([ + expect.objectContaining({ path: ["PROBE_PORT"] }), + expect.objectContaining({ path: ["CONCURRENCY"] }), + ]); + }); + + it("rejects a concurrency that is a number but not a whole one", () => { + // GIVEN a value `Number()` reads happily and no scheduler could use + const source = { CONCURRENCY: "3.5" }; + + // WHEN it is validated + const env = readEnv(source); + + // THEN it is an issue rather than a silent truncation + expect(env).toBeErrWith([expect.objectContaining({ path: ["CONCURRENCY"] })]); + }); + it("rejects a concurrency no worker should be asked for", () => { // GIVEN a number that parses but that nothing sensible would run const source = { CONCURRENCY: "1000" }; diff --git a/examples/order-worker/src/env.ts b/examples/order-worker/src/env.ts index 468ebce..6299473 100644 --- a/examples/order-worker/src/env.ts +++ b/examples/order-worker/src/env.ts @@ -6,17 +6,23 @@ import { z } from "zod"; * A whole number, read the way an environment variable actually arrives: as a * string. * - * `z.coerce.number()` would be shorter and wrong — it is `Number()` underneath, - * so `CONCURRENCY=abc` becomes `NaN` and `CONCURRENCY=` becomes `0`, a worker - * that consumes nothing at all. A digits-only string is the honest model, and - * anything else is a validation issue rather than a number nobody asked for. + * The non-empty string in front of the coercion is the load-bearing part. + * Coercion is `Number()` underneath, and `Number("")` is `0` — so a bare + * `CONCURRENCY=` would build a worker that consumes nothing at all, and a bare + * `PROBE_PORT=` would bind the ephemeral port `0`, which `min(0)` cannot catch + * because an ephemeral bind has to stay expressible. An empty value is a + * configuration error, not an absent one: `.default(...)` applies only when the + * variable is genuinely missing. + * + * With that guard, the bounds handle the rest — `abc` is `NaN`, `3.5` is not an + * integer, and anything outside `min`/`max` is out of range. */ const wholeNumber = (fallback: number, min: number, max: number) => z .string() - .regex(/^\d+$/u, "must be a whole number of decimal digits") - .transform(Number) - .pipe(z.int().min(min).max(max)) + .trim() + .min(1) + .pipe(z.coerce.number().int().min(min).max(max)) .default(fallback); const environment = z.object({ diff --git a/examples/order-worker/src/queue-runtime.ts b/examples/order-worker/src/queue-runtime.ts index fba1fa9..be7c555 100644 --- a/examples/order-worker/src/queue-runtime.ts +++ b/examples/order-worker/src/queue-runtime.ts @@ -85,8 +85,10 @@ const retry = (reason: string): Disposition => ({ kind: "retry", reason }); * another delivery is a `Defect` — an unmodelled failure is the infrastructure * one, and infrastructure comes back. * - * Every case is named. A new domain error is a compile error here, at the one - * place that has to decide what happens to the message. + * Every case is named — grouped into one arm, not collapsed into a wildcard: + * both park the message, and `error._tag` is still the narrowed union of the two + * so the reason names which one it was. A new domain error is a compile error + * here, at the one place that has to decide what happens to the message. */ const dispositionOf = ( ctx: Context>, @@ -99,9 +101,9 @@ const dispositionOf = ( .match({ ok: () => ack, errCases: (matcher) => - matcher - .with(P.tag("InvalidQuantity"), (error) => deadLetter(error._tag)) - .with(P.tag("DuplicateOrder"), (error) => deadLetter(error._tag)), + matcher.with(P.tag("InvalidQuantity"), P.tag("DuplicateOrder"), (error) => + deadLetter(error._tag), + ), defect: (cause) => retry(String(cause)), }), ); diff --git a/knip.json b/knip.json index 330da10..e9fc074 100644 --- a/knip.json +++ b/knip.json @@ -5,6 +5,7 @@ "ignoreDependencies": ["@btravstack/lefthook", "@btravstack/oxlint"], "workspaces": { "examples/order-api": { "entry": ["src/main.ts"] }, - "examples/order-worker": { "entry": ["src/main.ts"] } + "examples/order-worker": { "entry": ["src/main.ts"] }, + "examples/order-temporal": { "entry": ["src/main.ts"] } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2ae80f..b2fce45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,36 @@ catalogs: '@prisma/client': specifier: 7.9.1 version: 7.9.1 + '@temporal-contract/client': + specifier: 8.0.0-beta.5 + version: 8.0.0-beta.5 + '@temporal-contract/contract': + specifier: 8.0.0-beta.5 + version: 8.0.0-beta.5 + '@temporal-contract/testing': + specifier: 8.0.0-beta.5 + version: 8.0.0-beta.5 + '@temporal-contract/worker': + specifier: 8.0.0-beta.5 + version: 8.0.0-beta.5 + '@temporalio/activity': + specifier: 1.22.0 + version: 1.22.0 + '@temporalio/client': + specifier: 1.22.0 + version: 1.22.0 + '@temporalio/common': + specifier: 1.22.0 + version: 1.22.0 + '@temporalio/testing': + specifier: 1.22.0 + version: 1.22.0 + '@temporalio/worker': + specifier: 1.22.0 + version: 1.22.0 + '@temporalio/workflow': + specifier: 1.22.0 + version: 1.22.0 '@types/node': specifier: 26.1.2 version: 26.1.2 @@ -146,6 +176,9 @@ importers: '@btravstack/start': specifier: workspace:* version: link:../../packages/start + '@btravstack/start-example-order-api-contract': + specifier: workspace:* + version: link:../order-api-contract '@btravstack/start-example-order-application': specifier: workspace:* version: link:../order-application @@ -158,9 +191,6 @@ importers: '@orpc/client': specifier: 'catalog:' version: 2.0.0-beta.23 - '@orpc/contract': - specifier: 'catalog:' - version: 2.0.0-beta.23 '@orpc/server': specifier: 'catalog:' version: 2.0.0-beta.23 @@ -191,7 +221,38 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) + + examples/order-api-contract: + dependencies: + '@orpc/contract': + specifier: 'catalog:' + version: 2.0.0-beta.23 + devDependencies: + '@btravstack/tsconfig': + specifier: 'catalog:' + version: 0.2.0 + '@orpc/client': + specifier: 'catalog:' + version: 2.0.0-beta.23 + '@types/node': + specifier: 'catalog:' + version: 26.1.2 + '@unthrown/orpc': + specifier: 'catalog:' + version: 0.1.2(@orpc/client@2.0.0-beta.23)(@orpc/server@2.0.0-beta.23) + '@unthrown/vitest': + specifier: 'catalog:' + version: 5.2.0(unthrown@5.2.0)(vitest@4.1.10) + typescript: + specifier: 'catalog:' + version: 7.0.2 + unthrown: + specifier: 'catalog:' + version: 5.2.0 + vitest: + specifier: 'catalog:' + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) examples/order-application: dependencies: @@ -222,7 +283,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) examples/order-domain: dependencies: @@ -253,7 +314,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) examples/order-infrastructure: dependencies: @@ -296,7 +357,111 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) + + examples/order-temporal: + dependencies: + '@btravstack/di': + specifier: 'catalog:' + version: 0.1.0(unthrown@5.2.0) + '@btravstack/start': + specifier: workspace:* + version: link:../../packages/start + '@btravstack/start-example-order-application': + specifier: workspace:* + version: link:../order-application + '@btravstack/start-example-order-domain': + specifier: workspace:* + version: link:../order-domain + '@btravstack/start-example-order-infrastructure': + specifier: workspace:* + version: link:../order-infrastructure + '@btravstack/start-example-order-temporal-contract': + specifier: workspace:* + version: link:../order-temporal-contract + '@temporal-contract/client': + specifier: 'catalog:' + version: 8.0.0-beta.5(@temporalio/client@1.22.0)(@temporalio/common@1.22.0)(unthrown@5.2.0) + '@temporal-contract/worker': + specifier: 'catalog:' + version: 8.0.0-beta.5(@temporalio/common@1.22.0)(@temporalio/worker@1.22.0)(@temporalio/workflow@1.22.0)(unthrown@5.2.0) + '@temporalio/activity': + specifier: 'catalog:' + version: 1.22.0 + '@temporalio/client': + specifier: 'catalog:' + version: 1.22.0 + '@temporalio/common': + specifier: 'catalog:' + version: 1.22.0 + '@temporalio/worker': + specifier: 'catalog:' + version: 1.22.0 + '@temporalio/workflow': + specifier: 'catalog:' + version: 1.22.0 + '@unthrown/standard-schema': + specifier: 'catalog:' + version: 5.2.0 + unthrown: + specifier: 'catalog:' + version: 5.2.0 + zod: + specifier: 'catalog:' + version: 4.4.3 + devDependencies: + '@btravstack/tsconfig': + specifier: 'catalog:' + version: 0.2.0 + '@temporal-contract/testing': + specifier: 'catalog:' + version: 8.0.0-beta.5(@temporal-contract/client@8.0.0-beta.5(@temporalio/client@1.22.0)(@temporalio/common@1.22.0)(unthrown@5.2.0))(@temporal-contract/contract@8.0.0-beta.5(unthrown@5.2.0))(@temporal-contract/worker@8.0.0-beta.5(@temporalio/common@1.22.0)(@temporalio/worker@1.22.0)(@temporalio/workflow@1.22.0)(unthrown@5.2.0))(@temporalio/client@1.22.0)(@temporalio/testing@1.22.0)(@temporalio/worker@1.22.0)(unthrown@5.2.0)(vitest@4.1.10) + '@temporalio/testing': + specifier: 'catalog:' + version: 1.22.0 + '@types/node': + specifier: 'catalog:' + version: 26.1.2 + '@unthrown/vitest': + specifier: 'catalog:' + version: 5.2.0(unthrown@5.2.0)(vitest@4.1.10) + typescript: + specifier: 'catalog:' + version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) + + examples/order-temporal-contract: + dependencies: + '@temporal-contract/contract': + specifier: 'catalog:' + version: 8.0.0-beta.5(unthrown@5.2.0) + zod: + specifier: 'catalog:' + version: 4.4.3 + devDependencies: + '@btravstack/tsconfig': + specifier: 'catalog:' + version: 0.2.0 + '@types/node': + specifier: 'catalog:' + version: 26.1.2 + '@unthrown/standard-schema': + specifier: 'catalog:' + version: 5.2.0 + '@unthrown/vitest': + specifier: 'catalog:' + version: 5.2.0(unthrown@5.2.0)(vitest@4.1.10) + typescript: + specifier: 'catalog:' + version: 7.0.2 + unthrown: + specifier: 'catalog:' + version: 5.2.0 + vitest: + specifier: 'catalog:' + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) examples/order-worker: dependencies: @@ -339,7 +504,7 @@ importers: version: 7.0.2 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) packages/start: devDependencies: @@ -369,7 +534,7 @@ importers: version: 5.2.0 vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) packages: @@ -601,6 +766,15 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -610,16 +784,145 @@ packages: '@types/node': optional: true + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/base64@17.67.0': + resolution: {integrity: sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@1.2.1': + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@17.67.0': + resolution: {integrity: sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@17.67.0': + resolution: {integrity: sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-core@4.68.1': + resolution: {integrity: sha512-V5oZ4Gt9WJKyQef0n9cAd0N9qjSkIBm3E4MYsgNIWBk5aINCDPKxMPo1i29rBxqiT4Ixf1epklqV9VJMKIxwlw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-fsa@4.68.1': + resolution: {integrity: sha512-HCG72UioncuO7Gw09XNVG+S85e3cq2hrUC/mexBrsWsa3mI7eePkkqWie3uVYbtsb64OR9YGQs5SqaufDRYBcg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-builtins@4.68.1': + resolution: {integrity: sha512-HK1BTksysokNZxNspqDH0yPaqN9YgR/AYIlYiIaU2Ys4BOk5CdybI7r6BgiZuiiPiV8n4sK/kZdice7Znpy2Kw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-to-fsa@4.68.1': + resolution: {integrity: sha512-lpKmU4X9e/oh8GIuAI7EXaS5QiLNM3KD15CkdhfS6PYmrGvoJqKQcyEfnLgnnaGslh/PFUMYSIZBCf2ejJGw8g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-utils@4.68.1': + resolution: {integrity: sha512-/GxfW1DWm9SCdkfbvqevLO/P5duobQfmKkHXxdMIDbcZMQeAgooAstIfZhkXpATzq9QbCQsnoWFM/dGHdZfndw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node@4.68.1': + resolution: {integrity: sha512-R5D9mWtqdURzcOWj1vdXr3APCwX0xchtFT+kmW7fXLNDifWdDrnh26jSID8pdnUfFBxTyfHtFtTL/NWKzIH7kQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-print@4.68.1': + resolution: {integrity: sha512-oGeZOGPYKK9v1CgeVeEDsLomH1lCnslSpqUN5GmPzrmAVGQlsmsdcXNA2O4lV8Y4xkuSuynx2ITBkUHJVaTbow==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-snapshot@4.68.1': + resolution: {integrity: sha512-XZfP0FDZN32bbc4t2bZN2qRrYHg5AktJnzk22HRoKGK4BprrbNRH2k5ceSNS/kupKYcofCs+O841+xaAbjnxwQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.21.0': + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@17.67.0': + resolution: {integrity: sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@17.67.0': + resolution: {integrity: sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@17.67.0': + resolution: {integrity: sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -1214,6 +1517,33 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -1410,6 +1740,178 @@ packages: '@standardserver/shared@0.6.0': resolution: {integrity: sha512-pPn3XL6bUOHhT85zN20uFeoZjFdmdTabNvhOCXKRiW2zvzbuWDYyBqjcptjHXI4muamEFqOXiSLoqvh8Gjy+Ag==} + '@swc/core-darwin-arm64@1.15.47': + resolution: {integrity: sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.47': + resolution: {integrity: sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.47': + resolution: {integrity: sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.47': + resolution: {integrity: sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.47': + resolution: {integrity: sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.47': + resolution: {integrity: sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.47': + resolution: {integrity: sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.47': + resolution: {integrity: sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.47': + resolution: {integrity: sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.47': + resolution: {integrity: sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.47': + resolution: {integrity: sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.47': + resolution: {integrity: sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.47': + resolution: {integrity: sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.28': + resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==} + + '@temporal-contract/client@8.0.0-beta.5': + resolution: {integrity: sha512-K5dtM6SuTXa/6Upa7H7zMp50X3PcbGSS7s9REPLz39IxOeUPIm1vzxeKscESjduA1x6jgcFGtiXOIH3l3lEAhA==} + engines: {node: '>=22.19.0'} + peerDependencies: + '@temporalio/client': ^1.16.0 + '@temporalio/common': ^1.16.0 + unthrown: ^5.0.0 + + '@temporal-contract/contract@8.0.0-beta.5': + resolution: {integrity: sha512-l70Q/cxswXacEcq89islqFKYdm4NUn4CMegVC0sJFPcjHt4EDqdywRzOSV5ZyX06kn0LWhBcEuLvSpjKb/4PaA==} + engines: {node: '>=22.19.0'} + peerDependencies: + unthrown: ^5.0.0 + peerDependenciesMeta: + unthrown: + optional: true + + '@temporal-contract/testing@8.0.0-beta.5': + resolution: {integrity: sha512-EpjcrCBmU+rQ0j+yvEypHm8MreZnuF7KGrxkL8iDYoDJtg4N9OcyyOL0RCyQ3EXPjGvjfXWMPn4e8RFBXwUcPQ==} + engines: {node: '>=22.19.0'} + peerDependencies: + '@temporal-contract/client': ^8.0.0-beta.5 + '@temporal-contract/contract': ^8.0.0-beta.5 + '@temporal-contract/worker': ^8.0.0-beta.5 + '@temporalio/client': ^1.16.0 + '@temporalio/testing': ^1.16.0 + '@temporalio/worker': ^1.16.0 + testcontainers: ^12 + unthrown: ^5.0.0 + vitest: ^4 + peerDependenciesMeta: + testcontainers: + optional: true + + '@temporal-contract/worker@8.0.0-beta.5': + resolution: {integrity: sha512-apKMRrrprZNZBlVBxRUbsxsb8PYh1iqHPASJ7/vRALQ1xdGyvKC6zIWQImYE994goB72qCRy0K8khrYJfoUoIw==} + engines: {node: '>=22.19.0'} + peerDependencies: + '@temporalio/common': ^1.16.0 + '@temporalio/worker': ^1.16.0 + '@temporalio/workflow': ^1.16.0 + unthrown: ^5.0.0 + + '@temporalio/activity@1.22.0': + resolution: {integrity: sha512-hrPeELIMloFWVrwmDfG1lh3uFNkaYp1ZHpcpWmJniTeSyxqCqLdil4lBOv11FByb6HeKxE6rsvOOUohulyVhRg==} + engines: {node: '>= 20.3.0'} + + '@temporalio/client@1.22.0': + resolution: {integrity: sha512-b02ty0uDly6/ZRKlbavs5GjXVTry/GB4tkq0v1NH4H7x+nTo/OK0u5Ms8Nni8D+aar3xff9lzIpXNwu/Dp3P3w==} + engines: {node: '>= 20.3.0'} + + '@temporalio/common@1.22.0': + resolution: {integrity: sha512-1NpQpo/y6XU1ULbo/bXgBEhl6qQnOeB79NrsSJdjI38/hfXPom2IYGJcIxOFfsQICQ0zePEKzB9Yyo31KNpIxA==} + engines: {node: '>= 20.3.0'} + + '@temporalio/core-bridge@1.22.0': + resolution: {integrity: sha512-mJ5agqtfW+GBPYl+ivYLrBpKJ3oFOw8ogcoizNrxYCzwJeprGFiEgHDb/jzyvECR4HAy5s9zQFJYNyTruXXFMw==} + engines: {node: '>= 20.3.0'} + + '@temporalio/nexus@1.22.0': + resolution: {integrity: sha512-4S1ZZdSNjrO9IUyhWxweIidvNn1X/ihEl+P2fOn/Ky38YRx9G+8wtV6g6lqG8zUrtEv//uqFw+HsRMziE2y4tg==} + engines: {node: '>= 20.3.0'} + + '@temporalio/proto@1.22.0': + resolution: {integrity: sha512-X7NVa0Z6HK3l9irZR48FKwZ9w9YXetCdF2z2KCIRr0W7Kul64Wt9o5hwvSXShAtrZuCEEHH8WO/ffHTXxJieLg==} + engines: {node: '>= 20.3.0'} + + '@temporalio/testing@1.22.0': + resolution: {integrity: sha512-LWIVkJZHLKsuuXLeTZ9VuN6Sjt79BJTl9KKTQ84dOysPj+tMGTET8TEZasBhCuocS0qFGrh00c5e4RzXjfnLLA==} + engines: {node: '>= 20.3.0'} + + '@temporalio/worker@1.22.0': + resolution: {integrity: sha512-axRHhUsovFlqDtXZxUnhRLV7WmC6qOMUCij13lw4glT5yRH62k8wIZqcT3naEfvLSzoDxjt2xM7me3VvTRSaow==} + engines: {node: '>= 20.3.0'} + + '@temporalio/workflow@1.22.0': + resolution: {integrity: sha512-31Ax529+TvfH2RCexOhiCyM3lARJJoVzrnRUS1gX+jl+mka+pRRnEUx4B572mZz0oOlCCxbiztHZF72wwwWZDA==} + engines: {node: '>= 20.3.0'} + '@turbo/darwin-64@2.10.8': resolution: {integrity: sha512-po+7rfJfUnFXjWlcoN2RwhErgzCdRtBc1T26vYPcywHlggmCQiQe1uWaE4j+BibI2uY9/2pDoFzMN0rmSaPFOw==} cpu: [x64] @@ -1488,6 +1990,9 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash@4.17.25': resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} @@ -1726,6 +2231,57 @@ packages: '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@yuku-codegen/binding-android-arm64@0.8.4': resolution: {integrity: sha512-rsYkGl2kOkDRsh1mxriYnk1qBS78vjlBJ3+T2XwtwKwqOliy2n+2Ae0EDxJ/uX1DZLm3KkBZarGO5isEnmHchA==} cpu: [arm64] @@ -1861,6 +2417,23 @@ packages: '@yuku-toolchain/types@0.8.4': resolution: {integrity: sha512-p7JE8flrj7ijZ/qLjHi4UwKqMarMD6zumbKXhrjp2I2iLJOuTYiQyci2U36VlXcUlNyzsY7E/mLnKCHotbzJVw==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -1876,6 +2449,10 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1912,6 +2489,11 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.13: + resolution: {integrity: sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==} + engines: {node: '>=6.0.0'} + hasBin: true + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -1933,6 +2515,14 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1952,6 +2542,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1966,13 +2559,31 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} @@ -2121,12 +2732,18 @@ packages: effect@3.20.0: resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + electron-to-chromium@1.5.403: + resolution: {integrity: sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==} + elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -2138,6 +2755,10 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -2163,14 +2784,38 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -2255,6 +2900,9 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2292,6 +2940,12 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-to-regex.js@1.2.0: + resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + global-directory@5.0.0: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} @@ -2313,6 +2967,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + heap-js@2.7.1: + resolution: {integrity: sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==} + engines: {node: '>=10.0.0'} + hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} @@ -2323,6 +2981,14 @@ packages: resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -2363,6 +3029,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2401,6 +3071,10 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -2572,6 +3246,9 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -2595,6 +3272,12 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + memfs@4.68.1: + resolution: {integrity: sha512-OD+IDRUvIxu3QHL+nFm9gdyugInD27FDJ+sl4B5QgomPHXMlbw+GP918P8VNKu2FkNlVeqBkpzkwROpamVifRw==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2603,6 +3286,10 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} @@ -2610,6 +3297,49 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minimizer-webpack-plugin@5.6.1: + resolution: {integrity: sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -2617,6 +3347,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + ms@3.0.0-canary.1: + resolution: {integrity: sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==} + engines: {node: '>=12.13'} + mysql2@3.15.3: resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} engines: {node: '>= 8.0'} @@ -2633,10 +3367,21 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nexus-rpc@0.0.2: + resolution: {integrity: sha512-IWjIExdVYlmwXuzHdY/Q3lXCv1gbqoAXPazQhy2w4Xgtgha3H0OOujEESVPQcFUFMWm+pAk2gKnb57g8S41JZg==} + engines: {node: '>= 20.0.0'} + node-abi@3.94.0: resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} engines: {node: '>=10'} + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} @@ -2785,6 +3530,14 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + proto3-json-serializer@2.0.2: + resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} + engines: {node: '>=14.0.0'} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -2826,6 +3579,10 @@ packages: remeda@2.33.4: resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2883,6 +3640,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2893,6 +3653,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -2937,6 +3701,23 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-loader@5.0.0: + resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.72.1 + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -2956,6 +3737,10 @@ packages: std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -2991,10 +3776,24 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + swc-loader@0.2.7: + resolution: {integrity: sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==} + peerDependencies: + '@swc/core': ^1.2.147 + webpack: '>=2' + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tar-fs@2.1.5: resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} @@ -3006,6 +3805,17 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + terser@5.49.2: + resolution: {integrity: sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==} + engines: {node: '>=10'} + hasBin: true + + thingies@2.6.1: + resolution: {integrity: sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3029,6 +3839,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3096,6 +3912,9 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + unionfs@4.6.0: + resolution: {integrity: sha512-fJAy3gTHjFi5S3TP5EGdjs/OUMFFvI/ady3T8qVuZfkv8Qi8prV/Q8BuFEgODJslhZTT2z2qdD2lGdee9qjEnA==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -3104,9 +3923,19 @@ packages: resolution: {integrity: sha512-x6evkZlJpE9MyTeLexoJcvNMypw19ceB8UMdxYjou78m4hcIJx3D6NTd/oLBfh62RdqC51LelzhEQo3GpVWvQg==} engines: {node: '>=20'} + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + valibot@1.4.2: resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: @@ -3206,6 +4035,24 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} + engines: {node: '>=10.13.0'} + + webpack@5.109.2: + resolution: {integrity: sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3216,6 +4063,10 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3232,10 +4083,18 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yargs@18.1.0: resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} @@ -3586,28 +4445,180 @@ snapshots: '@emnapi/runtime@1.11.2': dependencies: tslib: 2.8.1 - optional: true + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 + + '@inquirer/external-editor@1.0.3(@types/node@26.1.2)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 26.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/base64@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-core@4.68.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.1(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-fsa@4.68.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.1(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-builtins@4.68.1(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-to-fsa@4.68.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-fsa': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-utils@4.68.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.68.1(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node@4.68.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.68.1(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-print@4.68.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-utils': 4.68.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-snapshot@4.68.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 - '@emnapi/wasi-threads@1.2.2': + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) tslib: 2.8.1 - optional: true - '@inquirer/external-editor@1.0.3(@types/node@26.1.2)': + '@jsonjoy.com/json-pointer@17.67.0(tslib@2.8.1)': dependencies: - chardet: 2.2.0 - iconv-lite: 0.7.3 - optionalDependencies: - '@types/node': 26.1.2 - - '@jridgewell/resolve-uri@3.1.2': {} + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 - '@jridgewell/trace-mapping@0.3.31': + '@jsonjoy.com/util@17.67.0(tslib@2.8.1)': dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 '@manypkg/find-root@1.1.0': dependencies: @@ -4025,6 +5036,26 @@ snapshots: transitivePeerDependencies: - '@types/react-dom' + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 @@ -4146,6 +5177,208 @@ snapshots: '@standardserver/shared@0.6.0': {} + '@swc/core-darwin-arm64@1.15.47': + optional: true + + '@swc/core-darwin-x64@1.15.47': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.47': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.47': + optional: true + + '@swc/core-linux-arm64-musl@1.15.47': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.47': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.47': + optional: true + + '@swc/core-linux-x64-gnu@1.15.47': + optional: true + + '@swc/core-linux-x64-musl@1.15.47': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.47': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.47': + optional: true + + '@swc/core-win32-x64-msvc@1.15.47': + optional: true + + '@swc/core@1.15.47': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.28 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.47 + '@swc/core-darwin-x64': 1.15.47 + '@swc/core-linux-arm-gnueabihf': 1.15.47 + '@swc/core-linux-arm64-gnu': 1.15.47 + '@swc/core-linux-arm64-musl': 1.15.47 + '@swc/core-linux-ppc64-gnu': 1.15.47 + '@swc/core-linux-s390x-gnu': 1.15.47 + '@swc/core-linux-x64-gnu': 1.15.47 + '@swc/core-linux-x64-musl': 1.15.47 + '@swc/core-win32-arm64-msvc': 1.15.47 + '@swc/core-win32-ia32-msvc': 1.15.47 + '@swc/core-win32-x64-msvc': 1.15.47 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.28': + dependencies: + '@swc/counter': 0.1.3 + + '@temporal-contract/client@8.0.0-beta.5(@temporalio/client@1.22.0)(@temporalio/common@1.22.0)(unthrown@5.2.0)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@temporal-contract/contract': 8.0.0-beta.5(unthrown@5.2.0) + '@temporalio/client': 1.22.0 + '@temporalio/common': 1.22.0 + unthrown: 5.2.0 + + '@temporal-contract/contract@8.0.0-beta.5(unthrown@5.2.0)': + dependencies: + '@standard-schema/spec': 1.1.0 + optionalDependencies: + unthrown: 5.2.0 + + '@temporal-contract/testing@8.0.0-beta.5(@temporal-contract/client@8.0.0-beta.5(@temporalio/client@1.22.0)(@temporalio/common@1.22.0)(unthrown@5.2.0))(@temporal-contract/contract@8.0.0-beta.5(unthrown@5.2.0))(@temporal-contract/worker@8.0.0-beta.5(@temporalio/common@1.22.0)(@temporalio/worker@1.22.0)(@temporalio/workflow@1.22.0)(unthrown@5.2.0))(@temporalio/client@1.22.0)(@temporalio/testing@1.22.0)(@temporalio/worker@1.22.0)(unthrown@5.2.0)(vitest@4.1.10)': + dependencies: + '@temporal-contract/client': 8.0.0-beta.5(@temporalio/client@1.22.0)(@temporalio/common@1.22.0)(unthrown@5.2.0) + '@temporal-contract/contract': 8.0.0-beta.5(unthrown@5.2.0) + '@temporal-contract/worker': 8.0.0-beta.5(@temporalio/common@1.22.0)(@temporalio/worker@1.22.0)(@temporalio/workflow@1.22.0)(unthrown@5.2.0) + '@temporalio/client': 1.22.0 + '@temporalio/testing': 1.22.0 + '@temporalio/worker': 1.22.0 + unthrown: 5.2.0 + vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) + + '@temporal-contract/worker@8.0.0-beta.5(@temporalio/common@1.22.0)(@temporalio/worker@1.22.0)(@temporalio/workflow@1.22.0)(unthrown@5.2.0)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@temporal-contract/contract': 8.0.0-beta.5(unthrown@5.2.0) + '@temporalio/common': 1.22.0 + '@temporalio/worker': 1.22.0 + '@temporalio/workflow': 1.22.0 + unthrown: 5.2.0 + + '@temporalio/activity@1.22.0': + dependencies: + '@temporalio/client': 1.22.0 + '@temporalio/common': 1.22.0 + + '@temporalio/client@1.22.0': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@temporalio/common': 1.22.0 + '@temporalio/proto': 1.22.0 + abort-controller: 3.0.0 + long: 5.3.2 + nexus-rpc: 0.0.2 + uuid: 11.1.1 + + '@temporalio/common@1.22.0': + dependencies: + '@temporalio/proto': 1.22.0 + long: 5.3.2 + ms: 3.0.0-canary.1 + nexus-rpc: 0.0.2 + proto3-json-serializer: 2.0.2 + + '@temporalio/core-bridge@1.22.0': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@temporalio/common': 1.22.0 + + '@temporalio/nexus@1.22.0': + dependencies: + '@temporalio/client': 1.22.0 + '@temporalio/common': 1.22.0 + '@temporalio/proto': 1.22.0 + long: 5.3.2 + nexus-rpc: 0.0.2 + + '@temporalio/proto@1.22.0': + dependencies: + long: 5.3.2 + protobufjs: 7.6.5 + + '@temporalio/testing@1.22.0': + dependencies: + '@temporalio/activity': 1.22.0 + '@temporalio/client': 1.22.0 + '@temporalio/common': 1.22.0 + '@temporalio/core-bridge': 1.22.0 + '@temporalio/proto': 1.22.0 + '@temporalio/worker': 1.22.0 + '@temporalio/workflow': 1.22.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/helpers' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + - webpack-cli + + '@temporalio/worker@1.22.0': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@swc/core': 1.15.47 + '@temporalio/activity': 1.22.0 + '@temporalio/client': 1.22.0 + '@temporalio/common': 1.22.0 + '@temporalio/core-bridge': 1.22.0 + '@temporalio/nexus': 1.22.0 + '@temporalio/proto': 1.22.0 + '@temporalio/workflow': 1.22.0 + heap-js: 2.7.1 + memfs: 4.68.1 + nexus-rpc: 0.0.2 + protobufjs: 7.6.5 + rxjs: 7.8.2 + source-map: 0.7.6 + source-map-loader: 5.0.0(webpack@5.109.2(@swc/core@1.15.47)) + supports-color: 8.1.1 + swc-loader: 0.2.7(@swc/core@1.15.47)(webpack@5.109.2(@swc/core@1.15.47)) + unionfs: 4.6.0 + webpack: 5.109.2(@swc/core@1.15.47) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/helpers' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + - webpack-cli + + '@temporalio/workflow@1.22.0': + dependencies: + '@temporalio/common': 1.22.0 + '@temporalio/proto': 1.22.0 + nexus-rpc: 0.0.2 + '@turbo/darwin-64@2.10.8': optional: true @@ -4210,6 +5443,8 @@ snapshots: '@types/geojson@7946.0.16': {} + '@types/json-schema@7.0.15': {} + '@types/lodash@4.17.25': {} '@types/node@12.20.55': {} @@ -4307,7 +5542,7 @@ snapshots: '@unthrown/vitest@5.2.0(unthrown@5.2.0)(vitest@4.1.10)': dependencies: unthrown: 5.2.0 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) '@visx/curve@4.0.1-alpha.0': dependencies: @@ -4394,7 +5629,7 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) '@vitest/expect@4.1.10': dependencies: @@ -4405,13 +5640,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.1.2)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.1(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.1.2)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -4437,6 +5672,86 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.1 + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + '@yuku-codegen/binding-android-arm64@0.8.4': optional: true @@ -4511,6 +5826,21 @@ snapshots: '@yuku-toolchain/types@0.8.4': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn@8.18.0: {} + + ajv-formats@2.1.1: + dependencies: + ajv: 8.20.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -4524,6 +5854,10 @@ snapshots: ansi-regex@6.2.2: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansi-styles@6.2.3: {} ansis@4.3.1: {} @@ -4550,6 +5884,8 @@ snapshots: base64-js@1.5.1: {} + baseline-browser-mapping@2.11.13: {} + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -4575,6 +5911,16 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.13 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.403 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) + + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -4601,6 +5947,8 @@ snapshots: callsites@3.1.0: {} + caniuse-lite@1.0.30001809: {} + chai@6.2.2: {} chardet@2.2.0: {} @@ -4611,14 +5959,30 @@ snapshots: chownr@1.1.4: {} + chrome-trace-event@1.0.4: {} + classnames@2.5.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@9.0.1: dependencies: string-width: 7.2.0 strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@2.20.3: {} + confbox@0.2.4: {} conventional-changelog-angular@9.2.1: @@ -4745,10 +6109,14 @@ snapshots: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 + electron-to-chromium@1.5.403: {} + elkjs@0.11.1: {} emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} + empathic@2.0.0: {} empathic@2.0.1: {} @@ -4757,6 +6125,11 @@ snapshots: dependencies: once: 1.4.0 + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -4776,12 +6149,29 @@ snapshots: escalade@3.2.0: {} + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + esprima@4.0.1: {} + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 + event-target-shim@5.0.1: {} + + events@3.3.0: {} + expand-template@2.0.3: {} expect-type@1.4.0: {} @@ -4864,6 +6254,8 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-monkey@1.1.0: {} + fsevents@2.3.3: optional: true @@ -4893,6 +6285,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regex.js@1.2.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + global-directory@5.0.0: dependencies: ini: 6.0.0 @@ -4914,12 +6310,20 @@ snapshots: has-flag@4.0.0: {} + heap-js@2.7.1: {} + hookable@6.1.1: {} html-escaper@2.0.2: {} human-id@4.2.0: {} + hyperdyperid@1.2.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -4947,6 +6351,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4978,6 +6384,12 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + jest-worker@27.5.1: + dependencies: + '@types/node': 26.1.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + jiti@2.6.1: {} jiti@2.7.0: {} @@ -5117,6 +6529,8 @@ snapshots: dependencies: p-locate: 4.1.0 + lodash.camelcase@4.3.0: {} + lodash.startcase@4.4.0: {} lodash@4.18.1: {} @@ -5139,6 +6553,25 @@ snapshots: dependencies: semver: 7.8.5 + memfs@4.68.1: + dependencies: + '@jsonjoy.com/fs-core': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-to-fsa': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.68.1(tslib@2.8.1) + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -5146,14 +6579,28 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.54.0: {} + mimic-response@3.1.0: {} minimist@1.2.8: {} + minimizer-webpack-plugin@5.6.1(@swc/core@1.15.47)(webpack@5.109.2(@swc/core@1.15.47)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.49.2 + webpack: 5.109.2(@swc/core@1.15.47) + optionalDependencies: + '@swc/core': 1.15.47 + mkdirp-classic@0.5.3: {} mri@1.2.0: {} + ms@3.0.0-canary.1: {} + mysql2@3.15.3: dependencies: aws-ssl-profiles: 1.1.2 @@ -5174,10 +6621,16 @@ snapshots: napi-build-utils@2.0.0: {} + neo-async@2.6.2: {} + + nexus-rpc@0.0.2: {} + node-abi@3.94.0: dependencies: semver: 7.8.5 + node-releases@2.0.53: {} + obug@2.1.4: {} ohash@2.0.11: {} @@ -5385,6 +6838,24 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 + proto3-json-serializer@2.0.2: + dependencies: + protobufjs: 7.6.5 + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 26.1.2 + long: 5.3.2 + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -5429,6 +6900,8 @@ snapshots: remeda@2.33.4: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -5483,6 +6956,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-buffer@5.2.1: {} safe-regex2@5.1.1: @@ -5491,6 +6968,13 @@ snapshots: safer-buffer@2.1.2: {} + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1 + ajv-keywords: 5.1.0(ajv@8.20.0) + semver@7.8.5: {} seq-queue@0.0.5: {} @@ -5521,6 +7005,21 @@ snapshots: source-map-js@1.2.1: {} + source-map-loader@5.0.0(webpack@5.109.2(@swc/core@1.15.47)): + dependencies: + iconv-lite: 0.6.3 + source-map-js: 1.2.1 + webpack: 5.109.2(@swc/core@1.15.47) + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -5536,6 +7035,12 @@ snapshots: std-env@4.2.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -5569,8 +7074,20 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + swc-loader@0.2.7(@swc/core@1.15.47)(webpack@5.109.2(@swc/core@1.15.47)): + dependencies: + '@swc/core': 1.15.47 + '@swc/counter': 0.1.3 + webpack: 5.109.2(@swc/core@1.15.47) + tagged-tag@1.0.0: {} + tapable@2.3.3: {} + tar-fs@2.1.5: dependencies: chownr: 1.1.4 @@ -5588,6 +7105,17 @@ snapshots: term-size@2.2.1: {} + terser@5.49.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.18.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + thingies@2.6.1(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tinybench@2.9.0: {} tinyexec@1.3.0: {} @@ -5605,6 +7133,10 @@ snapshots: dependencies: is-number: 7.0.0 + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} tsdown@0.22.14(oxc-resolver@11.24.2)(typescript@7.0.2): @@ -5632,8 +7164,7 @@ snapshots: - oxc-resolver - vue-tsc - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tunnel-agent@0.6.0: dependencies: @@ -5684,19 +7215,31 @@ snapshots: undici-types@8.3.0: {} + unionfs@4.6.0: + dependencies: + fs-monkey: 1.1.0 + universalify@0.1.2: {} unthrown@5.2.0: {} + update-browserslist-db@1.3.1(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + util-deprecate@1.0.2: {} + uuid@11.1.1: {} + valibot@1.4.2(typescript@7.0.2): optionalDependencies: typescript: 7.0.2 verkit@0.3.2: {} - vite@8.2.1(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0): + vite@8.2.1(@types/node@26.1.2)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -5707,12 +7250,13 @@ snapshots: '@types/node': 26.1.2 fsevents: 2.3.3 jiti: 2.7.0 + terser: 5.49.2 yaml: 2.9.0 - vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(yaml@2.9.0): + vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.1.2)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -5729,7 +7273,7 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.1(@types/node@26.1.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.1.2)(jiti@2.7.0)(terser@5.49.2)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.1.2 @@ -5750,6 +7294,48 @@ snapshots: walk-up-path@4.0.0: {} + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + webpack-sources@3.5.1: {} + + webpack@5.109.2(@swc/core@1.15.47): + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.18.0 + browserslist: 4.28.8 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.1 + eslint-scope: 5.1.1 + events: 3.3.0 + graceful-fs: 4.2.11 + mime-db: 1.54.0 + minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.47)(webpack@5.109.2(@swc/core@1.15.47)) + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + watchpack: 2.5.2 + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + which@2.0.2: dependencies: isexe: 2.0.0 @@ -5759,6 +7345,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -5771,8 +7363,20 @@ snapshots: yaml@2.9.0: {} + yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yargs@18.1.0: dependencies: cliui: 9.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2eca7d6..1d6d4ac 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,6 +25,20 @@ catalog: "@orpc/server": 2.0.0-beta.23 "@prisma/adapter-better-sqlite3": 7.9.1 "@prisma/client": 7.9.1 + # `@temporal-contract/*` v8 is still in beta and its `latest` dist-tag is the + # 7.x line, which peers on `unthrown@^4` while this repo pins 5.2.0. 8.x also + # ships the `test-rig` / `workflow-bundle` subpaths the Temporal example's + # specs are built on. The exact beta is the contract until v8 goes stable. + "@temporal-contract/client": 8.0.0-beta.5 + "@temporal-contract/contract": 8.0.0-beta.5 + "@temporal-contract/testing": 8.0.0-beta.5 + "@temporal-contract/worker": 8.0.0-beta.5 + "@temporalio/activity": 1.22.0 + "@temporalio/client": 1.22.0 + "@temporalio/common": 1.22.0 + "@temporalio/testing": 1.22.0 + "@temporalio/worker": 1.22.0 + "@temporalio/workflow": 1.22.0 "@types/node": 26.1.2 "@unthrown/oxlint": 5.2.0 "@unthrown/orpc": 0.1.2 @@ -47,6 +61,9 @@ catalog: allowBuilds: # Prisma 7 is engine-less (WASM query compiler); nothing to build or download. "@prisma/engines": false + # Pulled in by @temporalio/*; both allowed upstream in temporal-contract. + "@swc/core": true + protobufjs: true # Native SQLite bindings for the persistence example's tests. better-sqlite3: true esbuild: true