@voltro/shared 0.86.0 → 0.87.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.
package/README.md CHANGED
@@ -24,3 +24,5 @@ deliberately short so there is nothing here to drift out of date.
24
24
 
25
25
  Proprietary. © 2026 Voltro UG. Your use is governed by the
26
26
  [Terms of Service](https://voltro.cloud/legal/terms). See the bundled `LICENSE`.
27
+
28
+ `newTraceId()` creates a nonzero 128-bit trace ID as 32 lowercase hexadecimal characters, using the same implementation re-exported by `@voltro/runtime`. It is browser-safe and diagnostic; it is not a security token.
@@ -0,0 +1,72 @@
1
+ import { Cause as e, Clock as t, Duration as n, Effect as r, Ref as i, Schema as a } from "effect";
2
+ //#region src/circuitBreaker.ts
3
+ var o = class extends a.TaggedError()("CircuitOpen", {
4
+ name: a.String,
5
+ retryAfterMs: a.Number
6
+ }) {}, s = {
7
+ tag: "closed",
8
+ failureCount: 0,
9
+ halfOpenSuccesses: 0,
10
+ openedAt: 0
11
+ }, c = { make: (a) => r.gen(function* () {
12
+ let c = a.name ?? "circuit", l = n.toMillis(n.fromInputUnsafe(a.resetTimeout)), u = a.halfOpenSuccessThreshold ?? 1, d = a.isFailure ?? (() => !0), f = yield* i.make(s), p = r.gen(function* () {
13
+ let e = yield* i.get(f);
14
+ if (e.tag === "half-open") {
15
+ let t = e.halfOpenSuccesses + 1;
16
+ yield* i.set(f, t >= u ? s : {
17
+ ...e,
18
+ halfOpenSuccesses: t
19
+ });
20
+ } else e.failureCount !== 0 && (yield* i.set(f, s));
21
+ }), m = r.gen(function* () {
22
+ let e = yield* t.currentTimeMillis, n = yield* i.get(f);
23
+ if (n.tag === "half-open" || n.tag === "open") {
24
+ yield* i.set(f, {
25
+ tag: "open",
26
+ failureCount: a.failureThreshold,
27
+ halfOpenSuccesses: 0,
28
+ openedAt: e
29
+ });
30
+ return;
31
+ }
32
+ let r = n.failureCount + 1;
33
+ yield* i.set(f, r >= a.failureThreshold ? {
34
+ tag: "open",
35
+ failureCount: r,
36
+ halfOpenSuccesses: 0,
37
+ openedAt: e
38
+ } : {
39
+ ...n,
40
+ failureCount: r
41
+ });
42
+ }), h = r.gen(function* () {
43
+ let e = yield* i.get(f);
44
+ if (e.tag !== "open") return null;
45
+ let n = (yield* t.currentTimeMillis) - e.openedAt;
46
+ return n >= l ? (yield* i.set(f, {
47
+ tag: "half-open",
48
+ failureCount: 0,
49
+ halfOpenSuccesses: 0,
50
+ openedAt: e.openedAt
51
+ }), null) : l - n;
52
+ }), g = (t) => r.gen(function* () {
53
+ let n = yield* h;
54
+ return n === null ? yield* t.pipe(r.tap(() => p), r.tapCause((t) => {
55
+ if (e.hasInterruptsOnly(t)) return r.void;
56
+ let n = e.findErrorOption(t), i = n._tag === "Some" ? n.value : void 0;
57
+ return d(i) ? m : r.void;
58
+ })) : yield* r.fail(new o({
59
+ name: c,
60
+ retryAfterMs: n
61
+ }));
62
+ }), _ = ((e) => g(e));
63
+ return Object.defineProperty(_, "state", {
64
+ value: i.get(f).pipe(r.map((e) => e.tag)),
65
+ enumerable: !0
66
+ }), Object.defineProperty(_, "reset", {
67
+ value: i.set(f, s),
68
+ enumerable: !0
69
+ }), _;
70
+ }) };
71
+ //#endregion
72
+ export { c as CircuitBreaker, o as CircuitOpen };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { Cause } from 'effect';
2
+ import { Duration } from 'effect';
3
+ import { Effect } from 'effect';
1
4
  import { Schema } from 'effect';
2
5
  import { VoidIfEmpty } from 'effect/Types';
3
6
  import { YieldableError } from 'effect/Cause';
@@ -19,6 +22,51 @@ export declare const CallId: Schema.brand<Schema.NonEmptyString, "CallId">;
19
22
 
20
23
  export declare type CallId = typeof CallId.Type;
21
24
 
25
+ export declare interface CircuitBreaker {
26
+ /** Run an effect through the breaker. OPEN ⇒ fails fast with CircuitOpen. */
27
+ <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E | CircuitOpen, R>;
28
+ /** Current state — reads the Ref. */
29
+ readonly state: Effect.Effect<CircuitState>;
30
+ /** Force the breaker back to closed (clears counters). */
31
+ readonly reset: Effect.Effect<void>;
32
+ }
33
+
34
+ export declare const CircuitBreaker: {
35
+ /** Construct a breaker. Effect-returning because it allocates a Ref. */
36
+ readonly make: (options: CircuitBreakerOptions) => Effect.Effect<CircuitBreaker>;
37
+ };
38
+
39
+ export declare interface CircuitBreakerOptions {
40
+ /** Consecutive failures before the breaker opens. */
41
+ readonly failureThreshold: number;
42
+ /** Wait before transitioning open → half-open (Duration or string). */
43
+ readonly resetTimeout: Duration.Input;
44
+ /** Successes required in half-open before closing again. Default 1. */
45
+ readonly halfOpenSuccessThreshold?: number;
46
+ /** Predicate: does this error count toward the threshold? Default: all
47
+ * typed failures count. Interruptions NEVER count (they're not a
48
+ * downstream failure — the caller went away). */
49
+ readonly isFailure?: (error: unknown) => boolean;
50
+ /** Optional name for inspect / metrics + dashboard surfacing. */
51
+ readonly name?: string;
52
+ }
53
+
54
+ /**
55
+ * Tagged error the breaker fails with while OPEN. Declarable on a
56
+ * mutation / workflow `error:` schema so it marshals typed to the client.
57
+ */
58
+ export declare class CircuitOpen extends CircuitOpen_base {
59
+ }
60
+
61
+ declare const CircuitOpen_base: Schema.Class<CircuitOpen, Schema.TaggedStruct<"CircuitOpen", {
62
+ /** Breaker name (the `name` option, or `'circuit'` when unnamed). */
63
+ readonly name: Schema.String;
64
+ /** Best-effort ms until the breaker next admits a probe call. */
65
+ readonly retryAfterMs: Schema.Number;
66
+ }>, Cause.YieldableError>;
67
+
68
+ export declare type CircuitState = 'closed' | 'open' | 'half-open';
69
+
22
70
  export declare class Conflict extends Conflict_base<{
23
71
  readonly resource: string;
24
72
  readonly reason: string;
@@ -88,18 +136,37 @@ declare const I18nError_base: new <A extends Record<string, any> = {}>(args: Voi
88
136
  readonly _tag: "@voltro/error/I18nError";
89
137
  } & Readonly<A>;
90
138
 
91
- export declare const JsonValue: Schema.Codec<JsonValue_2>;
139
+ export declare type JsonValue = JsonValueData;
92
140
 
93
- declare type JsonValue_2 = null | boolean | number | string | ReadonlyArray<JsonValue_2> | {
94
- readonly [key: string]: JsonValue_2;
95
- };
141
+ export declare const JsonValue: Schema.Codec<JsonValueData>;
96
142
 
97
- declare const JsonValue: Schema.Codec<JsonValue_2>;
143
+ declare type JsonValueData = null | boolean | number | string | ReadonlyArray<JsonValueData> | {
144
+ readonly [key: string]: JsonValueData;
145
+ };
98
146
 
99
147
  export declare const LocaleTag: Schema.brand<Schema.String, "LocaleTag">;
100
148
 
101
149
  export declare type LocaleTag = typeof LocaleTag.Type;
102
150
 
151
+ /**
152
+ * A fresh trace id for work that has NO incoming span to continue — a webhook
153
+ * delivery, a schedule firing, a devtools invoke, a REST or gRPC entry point.
154
+ *
155
+ * 128 random bits as 32 lowercase hex, the shape `TRACEPARENT_RE` above accepts
156
+ * and the shape a collector, a log pipeline or `voltro traces` can join on.
157
+ *
158
+ * The reason this exists as ONE function: every entry point that minted its own
159
+ * had drifted somewhere different, and three of the shapes were not ids at all.
160
+ * `incoming:<webhookId>`, `reaction:<workflow>` and the literal
161
+ * `'webhooks:trigger'` are CONSTANTS — they were the caller's `source` label
162
+ * copied into the id field, so every delivery a process ever handled shared one
163
+ * trace and the waterfall merged unrelated work. `public-`/`grpc-`/`rest-` plus
164
+ * eight base36 characters carried ~41 bits, which collides long before a busy
165
+ * day is over. The entry-point kind is not lost by fixing this: it lives in the
166
+ * `source` field beside the id, which is where it was already being written.
167
+ */
168
+ export declare const newTraceId: () => string;
169
+
103
170
  export declare class NotFound extends NotFound_base<{
104
171
  readonly resource: string;
105
172
  readonly id?: string;
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { BadRequest as e, Conflict as t, CrossTenantAccessDenied as n, DatabaseUnavailable as r, Forbidden as i, FrameworkError as a, I18nError as o, NotFound as s, RateLimitExceeded as c, ServiceUnavailable as l, TenantContextMissing as u, Unauthorized as d } from "./errors.js";
2
2
  import { ApiKeyId as f, CallId as p, ConnectionId as m, RouteId as h, SubscriptionId as g, TenantId as _, TraceId as v, UserId as y, WorkflowExecutionId as b } from "./ids.js";
3
3
  import { EmailAddress as x, JsonValue as S, LocaleTag as C, TimestampISO as w, TimezoneName as T } from "./schemas.js";
4
- export { f as ApiKeyId, e as BadRequest, p as CallId, t as Conflict, m as ConnectionId, n as CrossTenantAccessDenied, r as DatabaseUnavailable, x as EmailAddress, i as Forbidden, a as FrameworkError, o as I18nError, S as JsonValue, C as LocaleTag, s as NotFound, c as RateLimitExceeded, h as RouteId, l as ServiceUnavailable, g as SubscriptionId, u as TenantContextMissing, _ as TenantId, w as TimestampISO, T as TimezoneName, v as TraceId, d as Unauthorized, y as UserId, b as WorkflowExecutionId };
4
+ import { CircuitBreaker as E, CircuitOpen as D } from "./circuitBreaker.js";
5
+ import { newTraceId as O } from "./traceId.js";
6
+ export { f as ApiKeyId, e as BadRequest, p as CallId, E as CircuitBreaker, D as CircuitOpen, t as Conflict, m as ConnectionId, n as CrossTenantAccessDenied, r as DatabaseUnavailable, x as EmailAddress, i as Forbidden, a as FrameworkError, o as I18nError, S as JsonValue, C as LocaleTag, s as NotFound, c as RateLimitExceeded, h as RouteId, l as ServiceUnavailable, g as SubscriptionId, u as TenantContextMissing, _ as TenantId, w as TimestampISO, T as TimezoneName, v as TraceId, d as Unauthorized, y as UserId, b as WorkflowExecutionId, O as newTraceId };
@@ -0,0 +1,16 @@
1
+ //#region src/traceId.ts
2
+ var e = () => {
3
+ let e = globalThis.crypto, t = /* @__PURE__ */ new Uint8Array(16);
4
+ if (e?.getRandomValues !== void 0) e.getRandomValues(t);
5
+ else {
6
+ let e = `${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
7
+ for (let n = 0; n < t.length; n += 1) {
8
+ let r = e.slice(n * 2 % e.length, n * 2 % e.length + 2);
9
+ t[n] = (parseInt(r, 16) || n + 1) ^ (Math.random() * 256 | 0);
10
+ }
11
+ }
12
+ let n = Array.from(t, (e) => e.toString(16).padStart(2, "0")).join("");
13
+ return n === "0".repeat(32) ? `${"0".repeat(31)}1` : n;
14
+ };
15
+ //#endregion
16
+ export { e as newTraceId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/shared",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "description": "Internal shared utilities used across the @voltro/* packages.",
5
5
  "keywords": [
6
6
  "voltro",