@gkoos/caracal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +311 -0
  4. package/dist/chunk-5CXDW7W6.js +202 -0
  5. package/dist/chunk-5CXDW7W6.js.map +1 -0
  6. package/dist/circuit-breaker-BSkcV0W_.d.ts +296 -0
  7. package/dist/fetch.d.ts +58 -0
  8. package/dist/fetch.js +117 -0
  9. package/dist/fetch.js.map +1 -0
  10. package/dist/index.d.ts +19 -0
  11. package/dist/index.js +1065 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/postgres.d.ts +28 -0
  14. package/dist/postgres.js +56 -0
  15. package/dist/postgres.js.map +1 -0
  16. package/dist/redis.d.ts +59 -0
  17. package/dist/redis.js +549 -0
  18. package/dist/redis.js.map +1 -0
  19. package/dist/retry-BFP_k3Hg.d.ts +26 -0
  20. package/dist/testing/index.d.ts +45 -0
  21. package/dist/testing/index.js +101 -0
  22. package/dist/testing/index.js.map +1 -0
  23. package/dist/types-Tf9T76C7.d.ts +187 -0
  24. package/package.json +127 -0
  25. package/src/adapters/fetch/adapter.ts +122 -0
  26. package/src/adapters/fetch/index.ts +15 -0
  27. package/src/adapters/fetch/retry-after.ts +111 -0
  28. package/src/adapters/postgres/adapter.ts +102 -0
  29. package/src/adapters/postgres/index.ts +7 -0
  30. package/src/coordination/redis/bulkhead.ts +61 -0
  31. package/src/coordination/redis/circuit-breaker.ts +270 -0
  32. package/src/coordination/redis/client.ts +78 -0
  33. package/src/coordination/redis/eval-script.ts +71 -0
  34. package/src/coordination/redis/keys.ts +32 -0
  35. package/src/coordination/redis/leases.ts +44 -0
  36. package/src/coordination/redis/scripts.ts +314 -0
  37. package/src/core/bulkhead.ts +336 -0
  38. package/src/core/circuit-breaker.ts +1066 -0
  39. package/src/core/index.ts +36 -0
  40. package/src/core/operation.ts +174 -0
  41. package/src/core/retry.ts +204 -0
  42. package/src/core/runtime.ts +123 -0
  43. package/src/core/scope-state-cache.ts +50 -0
  44. package/src/core/timeout.ts +73 -0
  45. package/src/core/types.ts +230 -0
  46. package/src/fetch.ts +17 -0
  47. package/src/index.ts +49 -0
  48. package/src/postgres.ts +9 -0
  49. package/src/redis.ts +8 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-09
4
+
5
+ Initial public release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Caracal contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,311 @@
1
+ <p align="center">
2
+ <img src="docs/caracal.svg" alt="Caracal" width="140">
3
+ </p>
4
+
5
+ # Caracal
6
+
7
+ **Scoped distributed resilience for asynchronous operations.**
8
+
9
+ Caracal is a library for wrapping asynchronous operations (like HTTP requests or database queries) with resilience policies. It provides a composable model for applying timeouts, retries, circuit breakers, and bulkheads to any operation that can be expressed as an adapter. The library is designed to work in distributed environments, allowing multiple instances of an application to **share and coordinate** their resilience policies through a shared Redis backend.
10
+
11
+ ### The problem
12
+
13
+ There are many libraries for applying resilience policies to operations, but most of them are designed for a single process. In a distributed system, each replica of an application may have its own local concurrency limits and circuit breakers, which can lead to uncoordinated failures and resource exhaustion.
14
+
15
+ If 40 replicas each enforce a local concurrency limit of 20, the downstream can still receive 800 concurrent requests. If each replica maintains its own circuit breaker, you get 40 independent failure windows and 40 independent recovery probes. Caracal uses Redis to coordinate those constraints across whichever scope actually matches your failure domain: region, shard, tenant, credential, workload class, or any grouping that makes sense for your application.
16
+
17
+ ## Quick start
18
+
19
+ ```sh
20
+ npm install @gkoos/caracal
21
+ # Redis coordination (optional peer dependency for distributed policies):
22
+ npm install ioredis
23
+ # PostgreSQL adapter (optional peer dependency for database operations):
24
+ npm install pg
25
+ ```
26
+
27
+ ```ts
28
+ import { bulkhead, circuitBreaker, operation, retry, timeout } from "@gkoos/caracal"
29
+ import { createCoordinationClient, redisCoordinator, redisCircuitBreakerCoordinator } from "@gkoos/caracal/redis"
30
+ import { fetchAdapter } from "@gkoos/caracal/fetch"
31
+ import { Pool } from "pg"
32
+ import { postgresAdapter } from "@gkoos/caracal/postgres"
33
+
34
+ // Redis coordination - connect once, share across all policies
35
+ const redis = createCoordinationClient(process.env.REDIS_URL!)
36
+ await redis.connect()
37
+
38
+ // Circuit breaker shared across all replicas, tracked per region.
39
+ // Use circuitBreaker.local({ name, minimumThroughput, failureThreshold, openMs }) if you only need in-process tracking.
40
+ const breaker = circuitBreaker.distributed({
41
+ name: "partner-api",
42
+ coordinator: redisCircuitBreakerCoordinator(redis, { namespace: "svc:prod" }),
43
+ scope: (ctx) => `region:${String(ctx.metadata.region)}`,
44
+ minimumThroughput: 20,
45
+ failureThreshold: 0.5,
46
+ openMs: 30_000,
47
+ halfOpenProbes: 3,
48
+ onCoordinatorError: "fail-open",
49
+ })
50
+
51
+ // Concurrency limit enforced across all replicas, per region.
52
+ // Use bulkhead.local({ name, limit, queue }) if you only need a per-process limit.
53
+ const capacity = bulkhead.distributed({
54
+ name: "partner-api",
55
+ coordinator: redisCoordinator(redis, { namespace: "svc:prod" }),
56
+ scope: (ctx) => `region:${String(ctx.metadata.region)}`,
57
+ limit: 20,
58
+ leaseMs: 30_000,
59
+ })
60
+
61
+ // Fetch operation: breaker outermost, bulkhead innermost around the adapter.
62
+ // timeout bounds the whole retry sequence; retry drives individual attempts.
63
+ const fetchOp = operation({
64
+ name: "partner-api",
65
+ adapter: fetchAdapter(),
66
+ policies: [
67
+ breaker,
68
+ timeout({ ms: 10_000 }),
69
+ retry({ maxAttempts: 3, delay: (n) => 100 * 2 ** (n - 1) }),
70
+ capacity,
71
+ ],
72
+ events: { emit: (e) => console.log(e.type, e) },
73
+ })
74
+
75
+ const response = await fetchOp.execute(
76
+ { url: "https://api.partner.com/orders/42" },
77
+ { metadata: { region: "eu-west-2" } },
78
+ )
79
+
80
+ // PostgreSQL operation reusing the same policy instances.
81
+ // A distributed policy is identified by (namespace, policy name, operation
82
+ // name, scope), so `orders-db` gets its own breaker window and capacity
83
+ // budget. Reuse the same operation name to share them deliberately.
84
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL })
85
+ const dbOp = operation({
86
+ name: "orders-db",
87
+ adapter: postgresAdapter(pool),
88
+ policies: [breaker, retry({ maxAttempts: 2 }), timeout({ ms: 2_000 }), capacity],
89
+ })
90
+
91
+ const row = await dbOp.execute(
92
+ { sql: "select * from orders where id = $1", values: ["42"], replay: "safe" },
93
+ { metadata: { region: "eu-west-2" } },
94
+ )
95
+
96
+ // At shutdown
97
+ await pool.end()
98
+ redis.disconnect()
99
+ ```
100
+
101
+ Distributed policies coordinate by `(namespace, policy name, operation name, scope)`. Two operations with different names get independent budgets and breaker windows even when they reuse the same policy instances. To share a budget or breaker deliberately, give the operations the same name - see [bulkheads](docs/bulkhead.md) and the [Redis key scheme](docs/redis.md).
102
+
103
+ ## How it works
104
+
105
+ ### Operations and adapters
106
+
107
+ An `operation` wraps an `adapter`: an object that declares its cancellation and replay capabilities before executing underlying work:
108
+
109
+ ```ts
110
+ import { operation, type Adapter } from "@gkoos/caracal"
111
+
112
+ const adapter: Adapter<{ id: string }, Order> = {
113
+ capabilities: () => ({ abort: "supported", replay: "safe" }),
114
+ execute: async ({ id }, context) => fetchOrder(id, { signal: context.signal }),
115
+ }
116
+
117
+ const orders = operation({ name: "orders", adapter })
118
+ const order = await orders.execute({ id: "42" }, { metadata: { region: "eu-west-2" } })
119
+ ```
120
+
121
+ `abort` tells Caracal whether it can signal cancellation to the underlying work. `replay` tells retry whether a repeated attempt is safe. Both are declared explicitly by the adapter, Caracal never infers them.
122
+
123
+ ### Policies
124
+
125
+ Policies are applied in array order, outermost first. The recommended ordering for a fully-configured operation is:
126
+
127
+ ```ts
128
+ // Schematic - order only; each entry is a constructed policy instance.
129
+ policies: [breaker, timeout, retry, capacity]
130
+ ```
131
+
132
+ Each policy is described below.
133
+
134
+ #### Timeout
135
+
136
+ Bounds how long the caller waits. Local only, there is no distributed timeout.
137
+
138
+ ```ts
139
+ import { timeout } from "@gkoos/caracal"
140
+
141
+ timeout({ ms: 5_000 })
142
+ ```
143
+
144
+ If the adapter declares `abort: "supported"`, Caracal cancels the underlying work when the deadline expires. If not, the caller still receives `TimeoutError` on time, but the underlying work may continue until it naturally settles.
145
+
146
+ #### Retry
147
+
148
+ Retries adapter-classified failures. Local only, there is no distributed retry.
149
+
150
+ ```ts
151
+ import { retry } from "@gkoos/caracal"
152
+
153
+ retry({
154
+ maxAttempts: 3,
155
+ delay: (attempt) => 100 * 2 ** (attempt - 1) * (0.5 + Math.random() * 0.5),
156
+ })
157
+ ```
158
+
159
+ `maxAttempts` includes the initial attempt. An attempt is only retried if the adapter classifies the outcome as `retryable` and declares `replay: "safe"`. The delay function receives the attempt number (1 = first retry) and a context carrying the settled outcome, so a protocol-specific helper can pace retries from server feedback. The fetch adapter ships an opt-in `Retry-After` implementation: `retry({ maxAttempts: 3, delay: retryAfterDelay })`. See [fetch](docs/fetch.md#retry-after).
160
+
161
+ #### Bulkhead
162
+
163
+ Limits concurrent underlying adapter calls. Available as local (per-process) or distributed (shared across replicas via Redis).
164
+
165
+ ```ts
166
+ import { bulkhead } from "@gkoos/caracal"
167
+ import { createCoordinationClient, redisCoordinator } from "@gkoos/caracal/redis"
168
+
169
+ // Local - each process enforces its own limit independently
170
+ const localCapacity = bulkhead.local({
171
+ name: "partner-api",
172
+ limit: 10,
173
+ queue: { limit: 50, timeoutMs: 2_000 }, // optional; default is immediate rejection
174
+ })
175
+
176
+ // Distributed - limit is shared across all replicas in the same scope
177
+ const sharedCapacity = bulkhead.distributed({
178
+ name: "partner-api",
179
+ coordinator: redisCoordinator(redis, { namespace: "svc:prod" }),
180
+ scope: (ctx) => `region:${String(ctx.metadata.region)}`,
181
+ limit: 20,
182
+ leaseMs: 30_000,
183
+ })
184
+ ```
185
+
186
+ Permits are held for the duration of the underlying adapter call only, not for the caller's wait or the retry loop. **A distributed bulkhead rejects immediately when full, there is no distributed queue**. On coordinator loss, admission always fails closed with `CoordinatorUnavailableError`.
187
+
188
+ #### Circuit breaker
189
+
190
+ Opens when the failure rate in a sliding window exceeds a threshold, blocking further attempts until a probe succeeds. Available as local (per-process) or distributed (shared across replicas via Redis).
191
+
192
+ ```ts
193
+ import { circuitBreaker } from "@gkoos/caracal"
194
+ import { redisCircuitBreakerCoordinator } from "@gkoos/caracal/redis"
195
+
196
+ // Local - each process tracks its own failure window independently
197
+ const localBreaker = circuitBreaker.local({
198
+ name: "partner-api",
199
+ minimumThroughput: 10, // minimum observations before the breaker may open
200
+ failureThreshold: 0.5, // open when ≥ 50% of the window are failures
201
+ openMs: 10_000, // stay open for 10s before allowing a probe
202
+ })
203
+
204
+ // Distributed - failure window and probe budget are shared across all replicas in the same scope
205
+ const sharedBreaker = circuitBreaker.distributed({
206
+ name: "partner-api",
207
+ coordinator: redisCircuitBreakerCoordinator(redis, { namespace: "svc:prod" }),
208
+ scope: (ctx) => `region:${String(ctx.metadata.region)}`,
209
+ minimumThroughput: 20,
210
+ failureThreshold: 0.5,
211
+ openMs: 30_000,
212
+ halfOpenProbes: 3, // globally bounded concurrent recovery probes per scope
213
+ onCoordinatorError: "fail-open", // or "fail-closed"
214
+ })
215
+ ```
216
+
217
+ Place the local circuit breaker outside retry. The breaker then observes one outcome per logical execution - a transient failure that a retry recovers from never counts against the breaker. **A distributed breaker never silently falls back to local state on coordinator loss.**
218
+
219
+ The `scope` function maps each execution context to a coordination key. Everything sharing that key shares the same failure window and state:
220
+
221
+ ```ts
222
+ scope: (ctx) => `region:${String(ctx.metadata.region)}` // one breaker per region
223
+ scope: (ctx) => `tenant:${String(ctx.metadata.tenantId)}` // one breaker per tenant
224
+ scope: () => "global" // one breaker for all replicas
225
+ ```
226
+
227
+ Use stable, non-secret values, scope keys are observable in the Redis keyspace.
228
+
229
+ ### Events and observability
230
+
231
+ Every operation accepts an `events` sink - or an array of sinks - that receives structured events from every policy decision. Sinks are output-only and isolated: an exception in a sink cannot affect execution.
232
+
233
+ ```ts
234
+ const op = operation({
235
+ name: "partner-api",
236
+ adapter: fetchAdapter(),
237
+ policies: [sharedBreaker, timeout({ ms: 5_000 }), retry({ maxAttempts: 3 }), sharedCapacity],
238
+ events: { emit: (event) => metrics.record(event) },
239
+ })
240
+ ```
241
+
242
+ The full set can be found in the [Events and observability](docs/events-and-observability.md) section.
243
+
244
+ ## Why not just use X, bro?
245
+
246
+ There are many libraries for applying resilience policies to operations, but most of them are designed for a single process. Caracal'
247
+
248
+ | Library / category | What it covers | Distributed aspect | Caracal difference |
249
+ |---|---|---|---|
250
+ | Polly-TS | Retry, breaker, timeout, bulkhead, rate limiting, cache, hedge, fallback; HTTP/framework integrations | Redis-backed distributed circuit breaker | Polly-TS adds Redis to one policy in an otherwise local-first, HTTP-centric library. In Caracal the policies coordinate across a user-defined scope - per region, per tenant, or any grouping - and the same model works for HTTP, PostgreSQL, or any async operation. |
251
+ | Breakwater | TS resilience pipeline: breaker, retry, timeout, fallback, bulkhead, rate limiter, cache, telemetry | Redis distributed circuit breaker; shared rate quota | Same pattern: Redis is an add-on to a local pipeline. Caracal makes scoped coordination the foundation, not an afterthought. |
252
+ | resilience4ts | Broad functional TS fault-tolerance patterns; distributed-first positioning | Distributed locks/cache and planned distributed context; project is small/early | Caracal's scope is narrower and more precise: explicit abort/replay traits, per-scope breaker and bulkhead with documented failure guarantees, and property/fuzz/multi-process integration testing. |
253
+ | Cockatiel / Resilience4j / Polly (.NET) | Mature, well-tested composable resilience patterns | Per-process state only | These are excellent libraries for single-process resilience. Caracal exists for the case where the constraint - capacity, health - belongs to a shared downstream, not to one replica. |
254
+ | Envoy / Envoy Gateway | Network-level circuit breaking, retries, concurrency limits; global rate limiting as a separate service | Circuit-breaker counters are not synchronised across Envoy processes | Envoy operates at the network layer without application semantics. Caracal works at the call site: it understands abort capability, replay safety, and lets you scope coordination to any application-defined group without a sidecar. |
255
+ | Redis semaphore/rate-limit libraries | Individual distributed primitives (semaphores, token buckets) | Shared Redis state | Assembling raw primitives means wiring up admission, leasing, renewal, failure handling, and observability yourself, for each policy. Caracal provides a tested, composed model with consistent semantics across policies. |
256
+
257
+ ## Development
258
+
259
+ Requires Node.js 20+.
260
+
261
+ ```sh
262
+ npm install
263
+ npm run check # format, lint, typecheck, build, unit tests
264
+ ```
265
+
266
+ ### Tests
267
+
268
+ ```sh
269
+ npm test # unit suite (test/unit) - fast, no external dependencies
270
+ npm run test:property # property suite - fast-check, CARACAL_TEST_SEED for replay
271
+ npm run test:fuzz # fuzz suite - seeded event-history generator
272
+ npm run test:integration # integration suite - requires services and CARACAL_* URLs
273
+ npm run test:integration:cluster # cluster suite - local three-master Valkey cluster (not run in CI)
274
+ npm run test:all # check, then the property, fuzz, and integration suites
275
+ ```
276
+
277
+ Integration tests use real Valkey and PostgreSQL via Docker Compose. Each suite skips itself unless its URL is set, so starting the containers alone is not enough:
278
+
279
+ ```sh
280
+ npm run redis:up
281
+ CARACAL_REDIS_URL=redis://127.0.0.1:6379 npm run test:integration
282
+ npm run redis:down
283
+ ```
284
+
285
+ See [Testing](docs/testing.md) and [Local development](docs/development.md) for the full test environment setup, seeded replay, and benchmarks.
286
+
287
+ ## Documentation
288
+
289
+ ### Core
290
+
291
+ - [Core API](docs/core-api.md) - Operation, adapter, policy composition, events
292
+ - [Redis foundation](docs/redis.md) - Standalone and cluster clients, key scheme, security, tuning
293
+
294
+ ### Policies
295
+
296
+ - [Timeout and retry](docs/timeout-and-retry.md) - Cancellation semantics, backoff, replay safety
297
+ - [Bulkheads](docs/bulkhead.md) - Local and distributed, lease guarantees, failure modes
298
+ - [Circuit breaker](docs/circuit-breaker.md) - State machine, sliding window, distributed coordination
299
+ - [Events and observability](docs/events-and-observability.md) - Full event reference, metrics, alerting
300
+
301
+ ### Adapters
302
+
303
+ - [Fetch adapter](docs/fetch.md) - Cancellation, replay safety, response classification, streaming
304
+ - [PostgreSQL adapter](docs/postgres.md) - Cancellation, replay safety, SQLSTATE classification
305
+
306
+ ### Contributing
307
+
308
+ - [Writing your own adapter](docs/adapter-contracts.md) - The `Adapter` interface, capabilities, and the contract test harness
309
+ - [Testing](docs/testing.md) - Test suites, property/fuzz testing, multi-process harness
310
+ - [Architecture](docs/architecture.md) - Package structure, subpath exports, tree-shaking rules
311
+ - [Local development](docs/development.md) - Setup, scripts, integration environments, benchmarks
@@ -0,0 +1,202 @@
1
+ import { randomUUID } from 'crypto';
2
+
3
+ // src/core/operation.ts
4
+
5
+ // src/core/runtime.ts
6
+ var eventSinks = /* @__PURE__ */ Symbol("caracal.eventSinks");
7
+ var admissionSignals = /* @__PURE__ */ new WeakMap();
8
+ function admissionSignal(context) {
9
+ const admission = admissionSignals.get(context);
10
+ return admission && context.signal ? AbortSignal.any([admission, context.signal]) : admission ?? context.signal;
11
+ }
12
+ function withAdmissionSignal(context, signal) {
13
+ const derived = attachRuntime(
14
+ { ...context },
15
+ runtimeContext(context)[eventSinks] ?? []
16
+ );
17
+ const previous = admissionSignal(context);
18
+ admissionSignals.set(
19
+ derived,
20
+ previous ? AbortSignal.any([previous, signal]) : signal
21
+ );
22
+ return derived;
23
+ }
24
+ function inheritAdmission(source, target) {
25
+ const signal = admissionSignals.get(source);
26
+ if (signal) admissionSignals.set(target, signal);
27
+ return target;
28
+ }
29
+ function runtimeContext(context) {
30
+ return context;
31
+ }
32
+ function attachRuntime(values, sinks) {
33
+ const context = values;
34
+ Object.defineProperty(context, eventSinks, { value: sinks });
35
+ return Object.freeze(context);
36
+ }
37
+ function createExecutionContext(values, sinks) {
38
+ return attachRuntime({ attempt: 1, ...values }, sinks);
39
+ }
40
+ function nextAttempt(context) {
41
+ return inheritAdmission(
42
+ context,
43
+ attachRuntime(
44
+ { ...context, attempt: context.attempt + 1 },
45
+ runtimeContext(context)[eventSinks] ?? []
46
+ )
47
+ );
48
+ }
49
+ function withSignal(context, signal) {
50
+ return inheritAdmission(
51
+ context,
52
+ attachRuntime(
53
+ { ...context, signal },
54
+ runtimeContext(context)[eventSinks] ?? []
55
+ )
56
+ );
57
+ }
58
+ function emitRuntimeEvent(context, event) {
59
+ const sinks = runtimeContext(context)[eventSinks] ?? [];
60
+ const fullEvent = { ...event, at: Date.now(), context };
61
+ for (const sink of sinks) {
62
+ try {
63
+ sink.emit(fullEvent);
64
+ } catch {
65
+ }
66
+ }
67
+ }
68
+ function createClassifier(classify) {
69
+ return (outcome) => {
70
+ if (classify === void 0) {
71
+ return outcome.status === "success" ? "success" : "failure";
72
+ }
73
+ return classify(outcome);
74
+ };
75
+ }
76
+
77
+ // src/core/operation.ts
78
+ var summarizeSuccess = () => ({
79
+ status: "success",
80
+ value: void 0
81
+ });
82
+ var summarizeFailure = (error) => ({
83
+ status: "failure",
84
+ error
85
+ });
86
+ function immutableCapabilities(capabilities) {
87
+ return Object.freeze({ ...capabilities });
88
+ }
89
+ function immutableMetadata(metadata) {
90
+ return Object.freeze({ ...metadata ?? {} });
91
+ }
92
+ function normalizeSinks(events) {
93
+ if (events === void 0) {
94
+ return [];
95
+ }
96
+ return "emit" in events ? [events] : events;
97
+ }
98
+ function emit(sinks, event) {
99
+ for (const sink of sinks) {
100
+ try {
101
+ sink.emit(event);
102
+ } catch {
103
+ }
104
+ }
105
+ }
106
+ function validateName(name, kind) {
107
+ if (name.trim().length === 0) {
108
+ throw new Error(`${kind} name must not be empty`);
109
+ }
110
+ }
111
+ function createPipeline(policies, adapter) {
112
+ return policies.reduceRight(
113
+ (next, policy) => async (context) => policy.execute(context, next),
114
+ adapter
115
+ );
116
+ }
117
+ function invokeAdapter(adapter, args, sinks) {
118
+ return async (context) => {
119
+ admissionSignal(context)?.throwIfAborted();
120
+ emit(sinks, { type: "attempt.started", at: Date.now(), context });
121
+ try {
122
+ const value = await adapter.execute(args, context);
123
+ const outcome = { status: "success", value };
124
+ const classification = context.classify(outcome);
125
+ emit(sinks, {
126
+ type: "attempt.settled",
127
+ at: Date.now(),
128
+ context,
129
+ outcome: summarizeSuccess(),
130
+ classification
131
+ });
132
+ return value;
133
+ } catch (error) {
134
+ const outcome = { status: "failure", error };
135
+ const classification = context.classify(outcome);
136
+ emit(sinks, {
137
+ type: "attempt.settled",
138
+ at: Date.now(),
139
+ context,
140
+ outcome: summarizeFailure(error),
141
+ classification
142
+ });
143
+ throw error;
144
+ }
145
+ };
146
+ }
147
+ function operation(options) {
148
+ validateName(options.name, "operation");
149
+ for (const policy of options.policies ?? []) {
150
+ validateName(policy.name, "policy");
151
+ }
152
+ const policies = Object.freeze([...options.policies ?? []]);
153
+ const sinks = Object.freeze(normalizeSinks(options.events));
154
+ return Object.freeze({
155
+ name: options.name,
156
+ async execute(args, executeOptions = {}) {
157
+ const capabilities = options.adapter.capabilities(args);
158
+ const context = createExecutionContext(
159
+ {
160
+ operationName: options.name,
161
+ executionId: executeOptions.executionId ?? randomUUID(),
162
+ signal: executeOptions.signal,
163
+ metadata: immutableMetadata(executeOptions.metadata),
164
+ capabilities: immutableCapabilities(capabilities),
165
+ classify: createClassifier(options.adapter.classify)
166
+ },
167
+ sinks
168
+ );
169
+ const adapter = createPipeline(
170
+ policies.filter((policy) => policy.phase === "attempt"),
171
+ invokeAdapter(options.adapter, args, sinks)
172
+ );
173
+ const pipeline = createPipeline(
174
+ policies.filter((policy) => policy.phase !== "attempt"),
175
+ adapter
176
+ );
177
+ emit(sinks, { type: "execution.started", at: Date.now(), context });
178
+ try {
179
+ const value = await pipeline(context);
180
+ emit(sinks, {
181
+ type: "execution.settled",
182
+ at: Date.now(),
183
+ context,
184
+ outcome: summarizeSuccess()
185
+ });
186
+ return value;
187
+ } catch (error) {
188
+ emit(sinks, {
189
+ type: "execution.settled",
190
+ at: Date.now(),
191
+ context,
192
+ outcome: summarizeFailure(error)
193
+ });
194
+ throw error;
195
+ }
196
+ }
197
+ });
198
+ }
199
+
200
+ export { admissionSignal, emitRuntimeEvent, nextAttempt, operation, withAdmissionSignal, withSignal };
201
+ //# sourceMappingURL=chunk-5CXDW7W6.js.map
202
+ //# sourceMappingURL=chunk-5CXDW7W6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/runtime.ts","../src/core/operation.ts"],"names":[],"mappings":"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","file":"chunk-5CXDW7W6.js","sourcesContent":["import type {\n Classification,\n EventSink,\n ExecutionContext,\n OperationEvent,\n Outcome,\n OutcomeClassifier,\n} from \"./types.js\"\n\nconst eventSinks = Symbol(\"caracal.eventSinks\")\nconst admissionSignals = new WeakMap<ExecutionContext, AbortSignal>()\nexport function admissionSignal(\n context: ExecutionContext,\n): AbortSignal | undefined {\n const admission = admissionSignals.get(context)\n return admission && context.signal\n ? AbortSignal.any([admission, context.signal])\n : (admission ?? context.signal)\n}\nexport function withAdmissionSignal(\n context: ExecutionContext,\n signal: AbortSignal,\n): ExecutionContext {\n const derived = attachRuntime(\n { ...context },\n runtimeContext(context)[eventSinks] ?? [],\n )\n const previous = admissionSignal(context)\n admissionSignals.set(\n derived,\n previous ? AbortSignal.any([previous, signal]) : signal,\n )\n return derived\n}\nfunction inheritAdmission(\n source: ExecutionContext,\n target: ExecutionContext,\n): ExecutionContext {\n const signal = admissionSignals.get(source)\n if (signal) admissionSignals.set(target, signal)\n return target\n}\n\ntype EventWithoutRuntimeFields = OperationEvent extends infer Event\n ? Event extends OperationEvent\n ? Omit<Event, \"at\" | \"context\">\n : never\n : never\n\ntype RuntimeExecutionContext = ExecutionContext & {\n readonly [eventSinks]: readonly EventSink[]\n}\n\nfunction runtimeContext(context: ExecutionContext): RuntimeExecutionContext {\n return context as RuntimeExecutionContext\n}\n\nfunction attachRuntime(\n values: ExecutionContext,\n sinks: readonly EventSink[],\n): ExecutionContext {\n const context = values as RuntimeExecutionContext\n Object.defineProperty(context, eventSinks, { value: sinks })\n return Object.freeze(context)\n}\n\nexport function createExecutionContext(\n values: Omit<ExecutionContext, \"attempt\">,\n sinks: readonly EventSink[],\n): ExecutionContext {\n return attachRuntime({ attempt: 1, ...values }, sinks)\n}\n\nexport function nextAttempt(context: ExecutionContext): ExecutionContext {\n return inheritAdmission(\n context,\n attachRuntime(\n { ...context, attempt: context.attempt + 1 },\n runtimeContext(context)[eventSinks] ?? [],\n ),\n )\n}\n\nexport function withSignal(\n context: ExecutionContext,\n signal: AbortSignal | undefined,\n): ExecutionContext {\n return inheritAdmission(\n context,\n attachRuntime(\n { ...context, signal },\n runtimeContext(context)[eventSinks] ?? [],\n ),\n )\n}\n\nexport function emitRuntimeEvent(\n context: ExecutionContext,\n event: EventWithoutRuntimeFields,\n): void {\n const sinks = runtimeContext(context)[eventSinks] ?? []\n const fullEvent = { ...event, at: Date.now(), context } as OperationEvent\n\n for (const sink of sinks) {\n try {\n sink.emit(fullEvent)\n } catch {\n // Observability must not modify resilience execution.\n }\n }\n}\n\nexport function createClassifier<Result>(\n classify: ((outcome: Outcome<Result>) => Classification) | undefined,\n): OutcomeClassifier {\n return (outcome) => {\n if (classify === undefined) {\n return outcome.status === \"success\" ? \"success\" : \"failure\"\n }\n\n return classify(outcome as Outcome<Result>)\n }\n}\n","import { randomUUID } from \"node:crypto\"\n\nimport {\n admissionSignal,\n createClassifier,\n createExecutionContext,\n} from \"./runtime.js\"\nimport type {\n Adapter,\n EventSink,\n EventSinks,\n ExecutionMetadata,\n Next,\n Operation,\n OperationCapabilities,\n OperationEvent,\n OperationExecuteOptions,\n OperationOptions,\n Outcome,\n Policy,\n} from \"./types.js\"\n\nconst summarizeSuccess = (): Outcome<undefined> => ({\n status: \"success\",\n value: undefined,\n})\nconst summarizeFailure = (error: unknown): Outcome<undefined> => ({\n status: \"failure\",\n error,\n})\n\nfunction immutableCapabilities(\n capabilities: OperationCapabilities,\n): OperationCapabilities {\n return Object.freeze({ ...capabilities })\n}\n\nfunction immutableMetadata(\n metadata: Readonly<Record<string, unknown>> | undefined,\n): ExecutionMetadata {\n return Object.freeze({ ...(metadata ?? {}) })\n}\n\nfunction normalizeSinks(events: EventSinks | undefined): readonly EventSink[] {\n if (events === undefined) {\n return []\n }\n\n return \"emit\" in events ? [events] : events\n}\n\nfunction emit(sinks: readonly EventSink[], event: OperationEvent): void {\n for (const sink of sinks) {\n try {\n sink.emit(event)\n } catch {\n // Observability must not modify resilience execution.\n }\n }\n}\n\nfunction validateName(name: string, kind: \"operation\" | \"policy\"): void {\n if (name.trim().length === 0) {\n throw new Error(`${kind} name must not be empty`)\n }\n}\n\nfunction createPipeline<Result>(\n policies: readonly Policy[],\n adapter: Next<Result>,\n): Next<Result> {\n return policies.reduceRight<Next<Result>>(\n (next, policy) => async (context) => policy.execute(context, next),\n adapter,\n )\n}\n\nfunction invokeAdapter<Args, Result>(\n adapter: Adapter<Args, Result>,\n args: Args,\n sinks: readonly EventSink[],\n): Next<Result> {\n return async (context) => {\n admissionSignal(context)?.throwIfAborted()\n emit(sinks, { type: \"attempt.started\", at: Date.now(), context })\n\n try {\n const value = await adapter.execute(args, context)\n const outcome: Outcome<Result> = { status: \"success\", value }\n const classification = context.classify(outcome)\n emit(sinks, {\n type: \"attempt.settled\",\n at: Date.now(),\n context,\n outcome: summarizeSuccess(),\n classification,\n })\n return value\n } catch (error) {\n const outcome: Outcome<Result> = { status: \"failure\", error }\n const classification = context.classify(outcome)\n emit(sinks, {\n type: \"attempt.settled\",\n at: Date.now(),\n context,\n outcome: summarizeFailure(error),\n classification,\n })\n throw error\n }\n }\n}\n\n/** Creates a named, protocol-agnostic operation. */\nexport function operation<Args, Result>(\n options: OperationOptions<Args, Result>,\n): Operation<Args, Result> {\n validateName(options.name, \"operation\")\n for (const policy of options.policies ?? []) {\n validateName(policy.name, \"policy\")\n }\n\n const policies = Object.freeze([...(options.policies ?? [])])\n const sinks = Object.freeze(normalizeSinks(options.events))\n\n return Object.freeze({\n name: options.name,\n async execute(\n args: Args,\n executeOptions: OperationExecuteOptions = {},\n ): Promise<Result> {\n const capabilities = options.adapter.capabilities(args)\n const context = createExecutionContext(\n {\n operationName: options.name,\n executionId: executeOptions.executionId ?? randomUUID(),\n signal: executeOptions.signal,\n metadata: immutableMetadata(executeOptions.metadata),\n capabilities: immutableCapabilities(capabilities),\n classify: createClassifier(options.adapter.classify),\n },\n sinks,\n )\n const adapter = createPipeline(\n policies.filter((policy) => policy.phase === \"attempt\"),\n invokeAdapter(options.adapter, args, sinks),\n )\n const pipeline = createPipeline(\n policies.filter((policy) => policy.phase !== \"attempt\"),\n adapter,\n )\n\n emit(sinks, { type: \"execution.started\", at: Date.now(), context })\n try {\n const value = await pipeline(context)\n emit(sinks, {\n type: \"execution.settled\",\n at: Date.now(),\n context,\n outcome: summarizeSuccess(),\n })\n return value\n } catch (error) {\n emit(sinks, {\n type: \"execution.settled\",\n at: Date.now(),\n context,\n outcome: summarizeFailure(error),\n })\n throw error\n }\n },\n })\n}\n"]}