@blokjs/runner 0.2.1 → 0.4.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/dist/Blok.js +11 -11
- package/dist/Blok.js.map +1 -1
- package/dist/Configuration.d.ts +39 -2
- package/dist/Configuration.js +337 -28
- package/dist/Configuration.js.map +1 -1
- package/dist/ConfigurationResolver.d.ts +9 -0
- package/dist/ConfigurationResolver.js +17 -1
- package/dist/ConfigurationResolver.js.map +1 -1
- package/dist/PayloadTooLargeError.d.ts +19 -0
- package/dist/PayloadTooLargeError.js +29 -0
- package/dist/PayloadTooLargeError.js.map +1 -0
- package/dist/RunCancelledError.d.ts +17 -0
- package/dist/RunCancelledError.js +25 -0
- package/dist/RunCancelledError.js.map +1 -0
- package/dist/RunnerSteps.js +363 -23
- package/dist/RunnerSteps.js.map +1 -1
- package/dist/RuntimeAdapterNode.d.ts +32 -2
- package/dist/RuntimeAdapterNode.js +122 -27
- package/dist/RuntimeAdapterNode.js.map +1 -1
- package/dist/SubworkflowNode.d.ts +75 -0
- package/dist/SubworkflowNode.js +221 -0
- package/dist/SubworkflowNode.js.map +1 -0
- package/dist/TriggerBase.d.ts +128 -0
- package/dist/TriggerBase.js +808 -6
- package/dist/TriggerBase.js.map +1 -1
- package/dist/WaitDispatchRequest.d.ts +38 -0
- package/dist/WaitDispatchRequest.js +13 -0
- package/dist/WaitDispatchRequest.js.map +1 -0
- package/dist/WaitNode.d.ts +23 -0
- package/dist/WaitNode.js +26 -0
- package/dist/WaitNode.js.map +1 -0
- package/dist/adapters/BunRuntimeAdapter.d.ts +1 -0
- package/dist/adapters/BunRuntimeAdapter.js +1 -0
- package/dist/adapters/BunRuntimeAdapter.js.map +1 -1
- package/dist/adapters/DockerRuntimeAdapter.d.ts +2 -1
- package/dist/adapters/DockerRuntimeAdapter.js +10 -1
- package/dist/adapters/DockerRuntimeAdapter.js.map +1 -1
- package/dist/adapters/HttpRuntimeAdapter.d.ts +26 -5
- package/dist/adapters/HttpRuntimeAdapter.js +97 -16
- package/dist/adapters/HttpRuntimeAdapter.js.map +1 -1
- package/dist/adapters/NodeJsRuntimeAdapter.d.ts +1 -0
- package/dist/adapters/NodeJsRuntimeAdapter.js +1 -0
- package/dist/adapters/NodeJsRuntimeAdapter.js.map +1 -1
- package/dist/adapters/RuntimeAdapter.d.ts +17 -0
- package/dist/adapters/WasmRuntimeAdapter.d.ts +1 -0
- package/dist/adapters/WasmRuntimeAdapter.js +1 -0
- package/dist/adapters/WasmRuntimeAdapter.js.map +1 -1
- package/dist/adapters/grpc/GrpcChannelOptions.d.ts +31 -0
- package/dist/adapters/grpc/GrpcChannelOptions.js +68 -0
- package/dist/adapters/grpc/GrpcChannelOptions.js.map +1 -0
- package/dist/adapters/grpc/GrpcClientPool.d.ts +43 -0
- package/dist/adapters/grpc/GrpcClientPool.js +89 -0
- package/dist/adapters/grpc/GrpcClientPool.js.map +1 -0
- package/dist/adapters/grpc/GrpcCodec.d.ts +226 -0
- package/dist/adapters/grpc/GrpcCodec.js +275 -0
- package/dist/adapters/grpc/GrpcCodec.js.map +1 -0
- package/dist/adapters/grpc/GrpcErrors.d.ts +59 -0
- package/dist/adapters/grpc/GrpcErrors.js +190 -0
- package/dist/adapters/grpc/GrpcErrors.js.map +1 -0
- package/dist/adapters/grpc/GrpcHealthChecker.d.ts +69 -0
- package/dist/adapters/grpc/GrpcHealthChecker.js +96 -0
- package/dist/adapters/grpc/GrpcHealthChecker.js.map +1 -0
- package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +98 -0
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js +478 -0
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -0
- package/dist/adapters/grpc/index.d.ts +13 -0
- package/dist/adapters/grpc/index.js +14 -0
- package/dist/adapters/grpc/index.js.map +1 -0
- package/dist/adapters/grpc/proto/blok/runtime/v1/runtime.proto +302 -0
- package/dist/adapters/grpc/types.d.ts +97 -0
- package/dist/adapters/grpc/types.js +41 -0
- package/dist/adapters/grpc/types.js.map +1 -0
- package/dist/adapters/transport.d.ts +108 -0
- package/dist/adapters/transport.js +196 -0
- package/dist/adapters/transport.js.map +1 -0
- package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
- package/dist/concurrency/ConcurrencyBackend.js +20 -0
- package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
- package/dist/concurrency/ConcurrencyLimitError.js +16 -0
- package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js +297 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/QueueExpiredError.d.ts +40 -0
- package/dist/concurrency/QueueExpiredError.js +15 -0
- package/dist/concurrency/QueueExpiredError.js.map +1 -0
- package/dist/concurrency/createConcurrencyBackend.d.ts +23 -0
- package/dist/concurrency/createConcurrencyBackend.js +34 -0
- package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
- package/dist/concurrency/readConcurrencyConfig.js +60 -0
- package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
- package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
- package/dist/idempotency/resolveIdempotencyKey.js +37 -0
- package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
- package/dist/index.d.ts +35 -3
- package/dist/index.js +61 -2
- package/dist/index.js.map +1 -1
- package/dist/monitoring/ConcurrencyMetrics.d.ts +56 -0
- package/dist/monitoring/ConcurrencyMetrics.js +107 -0
- package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
- package/dist/monitoring/JanitorMetrics.d.ts +27 -0
- package/dist/monitoring/JanitorMetrics.js +48 -0
- package/dist/monitoring/JanitorMetrics.js.map +1 -0
- package/dist/scheduling/DebounceCoordinator.d.ts +88 -0
- package/dist/scheduling/DebounceCoordinator.js +141 -0
- package/dist/scheduling/DebounceCoordinator.js.map +1 -0
- package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
- package/dist/scheduling/DeferredDispatchSignal.js +14 -0
- package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
- package/dist/scheduling/DeferredRunScheduler.d.ts +68 -0
- package/dist/scheduling/DeferredRunScheduler.js +154 -0
- package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
- package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
- package/dist/scheduling/readSchedulingConfig.js +52 -0
- package/dist/scheduling/readSchedulingConfig.js.map +1 -0
- package/dist/testing/WorkflowTestRunner.js +12 -0
- package/dist/testing/WorkflowTestRunner.js.map +1 -1
- package/dist/timeouts/StepTimeoutError.d.ts +22 -0
- package/dist/timeouts/StepTimeoutError.js +31 -0
- package/dist/timeouts/StepTimeoutError.js.map +1 -0
- package/dist/tracing/InMemoryRunStore.d.ts +28 -1
- package/dist/tracing/InMemoryRunStore.js +150 -0
- package/dist/tracing/InMemoryRunStore.js.map +1 -1
- package/dist/tracing/Janitor.d.ts +70 -0
- package/dist/tracing/Janitor.js +150 -0
- package/dist/tracing/Janitor.js.map +1 -0
- package/dist/tracing/PostgresRunStore.d.ts +30 -0
- package/dist/tracing/PostgresRunStore.js +435 -3
- package/dist/tracing/PostgresRunStore.js.map +1 -1
- package/dist/tracing/RunStore.d.ts +100 -1
- package/dist/tracing/RunTracker.d.ts +261 -11
- package/dist/tracing/RunTracker.js +691 -11
- package/dist/tracing/RunTracker.js.map +1 -1
- package/dist/tracing/SqliteRunStore.d.ts +23 -1
- package/dist/tracing/SqliteRunStore.js +421 -6
- package/dist/tracing/SqliteRunStore.js.map +1 -1
- package/dist/tracing/TraceRouter.d.ts +20 -2
- package/dist/tracing/TraceRouter.js +494 -9
- package/dist/tracing/TraceRouter.js.map +1 -1
- package/dist/tracing/sanitize.d.ts +11 -0
- package/dist/tracing/sanitize.js +29 -0
- package/dist/tracing/sanitize.js.map +1 -1
- package/dist/tracing/types.d.ts +429 -11
- package/dist/types/GlobalOptions.d.ts +9 -2
- package/dist/utils/createChildContext.d.ts +32 -0
- package/dist/utils/createChildContext.js +113 -0
- package/dist/utils/createChildContext.js.map +1 -0
- package/dist/workflow/PersistenceHelper.d.ts +46 -0
- package/dist/workflow/PersistenceHelper.js +57 -0
- package/dist/workflow/PersistenceHelper.js.map +1 -0
- package/dist/workflow/WorkflowNormalizer.d.ts +79 -0
- package/dist/workflow/WorkflowNormalizer.js +486 -0
- package/dist/workflow/WorkflowNormalizer.js.map +1 -0
- package/dist/workflow/WorkflowRegistry.d.ts +64 -0
- package/dist/workflow/WorkflowRegistry.js +81 -0
- package/dist/workflow/WorkflowRegistry.js.map +1 -0
- package/package.json +10 -7
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 #6 follow-up · NATS KV-backed concurrency backend.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates per-(workflow, concurrencyKey) lease state across processes
|
|
5
|
+
* via a single NATS JetStream KV value per bucket using revision-based
|
|
6
|
+
* compare-and-swap (OCC).
|
|
7
|
+
*
|
|
8
|
+
* Storage model: one KV key per `(workflowName, concurrencyKey)` pair.
|
|
9
|
+
* Value is a JSON `{leases: [{runId, expiresAt}]}` document. Bounded
|
|
10
|
+
* cardinality assumption — typical concurrency keys hold 1-50 active
|
|
11
|
+
* leases (per-tenant rate limits). For higher cardinality, a per-lease
|
|
12
|
+
* key model would scale better; revisit when needed.
|
|
13
|
+
*
|
|
14
|
+
* Atomicity: NATS KV's only guarantee is `kv.create(key, value)` (fails
|
|
15
|
+
* on conflict) and `kv.update(key, value, expectedRevision)` (fails on
|
|
16
|
+
* concurrent modification). The acquire loop reads → filters → checks
|
|
17
|
+
* limit → CAS update. On CAS failure, retry up to 10 times then
|
|
18
|
+
* fail-closed (deny the slot).
|
|
19
|
+
*
|
|
20
|
+
* Lease leak: each lease carries an `expiresAt`. Expired leases are
|
|
21
|
+
* lazy-purged inside the same `acquireSlot` call that observes them;
|
|
22
|
+
* an explicit `purgeExpired` sweep is also exposed for janitor use.
|
|
23
|
+
*/
|
|
24
|
+
import { ConcurrencyMetrics } from "../monitoring/ConcurrencyMetrics";
|
|
25
|
+
const DEFAULT_BUCKET_NAME = "blok-concurrency";
|
|
26
|
+
const MAX_CAS_RETRIES = 10;
|
|
27
|
+
/**
|
|
28
|
+
* Read configuration from environment variables. Used by
|
|
29
|
+
* {@link createConcurrencyBackend} when the user opts into NATS KV.
|
|
30
|
+
*/
|
|
31
|
+
export function readNatsKvConfigFromEnv() {
|
|
32
|
+
const serversRaw = process.env.BLOK_CONCURRENCY_NATS_SERVERS ?? "nats://localhost:4222";
|
|
33
|
+
const servers = serversRaw
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((s) => s.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
return {
|
|
38
|
+
servers,
|
|
39
|
+
token: process.env.BLOK_CONCURRENCY_NATS_TOKEN,
|
|
40
|
+
user: process.env.BLOK_CONCURRENCY_NATS_USER,
|
|
41
|
+
pass: process.env.BLOK_CONCURRENCY_NATS_PASS,
|
|
42
|
+
bucketName: process.env.BLOK_CONCURRENCY_NATS_KV_BUCKET ?? DEFAULT_BUCKET_NAME,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export class NatsKvConcurrencyBackend {
|
|
46
|
+
name = "nats-kv";
|
|
47
|
+
nc = null;
|
|
48
|
+
kv = null;
|
|
49
|
+
config;
|
|
50
|
+
connected = false;
|
|
51
|
+
constructor(config) {
|
|
52
|
+
const env = readNatsKvConfigFromEnv();
|
|
53
|
+
this.config = {
|
|
54
|
+
servers: config?.servers ?? env.servers,
|
|
55
|
+
token: config?.token ?? env.token,
|
|
56
|
+
user: config?.user ?? env.user,
|
|
57
|
+
pass: config?.pass ?? env.pass,
|
|
58
|
+
bucketName: config?.bucketName ?? env.bucketName,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async connect() {
|
|
62
|
+
if (this.connected)
|
|
63
|
+
return;
|
|
64
|
+
// Security review FW-5 — refuse to start in production with the
|
|
65
|
+
// default bucket name. Two deployments sharing a NATS server with
|
|
66
|
+
// the default would contend on the same `(workflow, key)` buckets,
|
|
67
|
+
// silently corrupting concurrency state across tenants. The fix
|
|
68
|
+
// is operator-mandatory: set BLOK_CONCURRENCY_NATS_KV_BUCKET
|
|
69
|
+
// per-deployment.
|
|
70
|
+
const blokEnv = process.env.BLOK_ENV;
|
|
71
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
72
|
+
const isProd = blokEnv === "production" || nodeEnv === "production";
|
|
73
|
+
if (isProd && this.config.bucketName === DEFAULT_BUCKET_NAME) {
|
|
74
|
+
throw new Error(`[blok] NATS KV concurrency backend refuses to start in production with the default bucket name ('${DEFAULT_BUCKET_NAME}'). Set BLOK_CONCURRENCY_NATS_KV_BUCKET to a deployment-unique value (e.g. 'blok-concurrency-acme-prod') to prevent cross-deployment collision on a shared NATS server.`);
|
|
75
|
+
}
|
|
76
|
+
let natsModule;
|
|
77
|
+
try {
|
|
78
|
+
natsModule = (await import("nats"));
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
throw new Error(`NatsKvConcurrencyBackend requires the 'nats' package. Install it: \`bun add nats\` or \`npm install nats\`. Underlying error: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
const connectOpts = { servers: this.config.servers };
|
|
84
|
+
if (this.config.token)
|
|
85
|
+
connectOpts.token = this.config.token;
|
|
86
|
+
if (this.config.user)
|
|
87
|
+
connectOpts.user = this.config.user;
|
|
88
|
+
if (this.config.pass)
|
|
89
|
+
connectOpts.pass = this.config.pass;
|
|
90
|
+
this.nc = (await natsModule.connect(connectOpts));
|
|
91
|
+
// `kv()` auto-creates the bucket on first use (NATS JetStream KV semantics).
|
|
92
|
+
this.kv = await this.nc.kv(this.config.bucketName);
|
|
93
|
+
this.connected = true;
|
|
94
|
+
}
|
|
95
|
+
async disconnect() {
|
|
96
|
+
if (!this.connected)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
await this.nc?.drain();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this.nc = null;
|
|
103
|
+
this.kv = null;
|
|
104
|
+
this.connected = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
bucketKey(workflowName, concurrencyKey) {
|
|
108
|
+
// Use `__` (double underscore) — KV keys cannot contain `.` or
|
|
109
|
+
// `>` per NATS subject grammar; `__` is unambiguous and allows
|
|
110
|
+
// arbitrary workflow / key strings.
|
|
111
|
+
return `${this.encodeSegment(workflowName)}__${this.encodeSegment(concurrencyKey)}`;
|
|
112
|
+
}
|
|
113
|
+
encodeSegment(s) {
|
|
114
|
+
// NATS KV keys must match `[-/_=\.a-zA-Z0-9]+`. Replace anything
|
|
115
|
+
// outside the safe set with hex escape `_HHHH_` to keep the
|
|
116
|
+
// roundtrip lossless.
|
|
117
|
+
return s.replace(/[^-_=.a-zA-Z0-9]/g, (ch) => `_${ch.codePointAt(0)?.toString(16)}_`);
|
|
118
|
+
}
|
|
119
|
+
requireKv() {
|
|
120
|
+
if (!this.kv) {
|
|
121
|
+
throw new Error("NatsKvConcurrencyBackend not connected — call connect() first.");
|
|
122
|
+
}
|
|
123
|
+
return this.kv;
|
|
124
|
+
}
|
|
125
|
+
async acquireSlot(workflowName, concurrencyKey, concurrencyLimit, runId, leaseExpiresAt) {
|
|
126
|
+
const kv = this.requireKv();
|
|
127
|
+
const bucketKey = this.bucketKey(workflowName, concurrencyKey);
|
|
128
|
+
// PR 3 D2 — record OCC retry depth + outcome on every exit path.
|
|
129
|
+
const metricAttrs = { workflow_name: workflowName, concurrency_key: concurrencyKey };
|
|
130
|
+
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
131
|
+
const entry = await this.safeGet(kv, bucketKey);
|
|
132
|
+
// PR 2 A6 — fetch failure (broker unreachable / non-NotFound
|
|
133
|
+
// error). Spinning 10× CAS retries on a connection problem just
|
|
134
|
+
// burns latency. Fail-fast so the trigger sees the issue and
|
|
135
|
+
// can fall back / alert. Existing run continues with no slot;
|
|
136
|
+
// the gate is conservative.
|
|
137
|
+
if (entry === "fetch-failed") {
|
|
138
|
+
console.warn(`[blok][concurrency][nats-kv] acquireSlot fetch-failed for ${workflowName}:${concurrencyKey} (attempt ${attempt + 1}); failing closed`);
|
|
139
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "fail-closed" }, attempt);
|
|
140
|
+
return { acquired: false, currentInFlight: -1 };
|
|
141
|
+
}
|
|
142
|
+
if (!entry) {
|
|
143
|
+
// Bucket doesn't exist — create with first lease.
|
|
144
|
+
const initial = { leases: [{ runId, expiresAt: leaseExpiresAt }] };
|
|
145
|
+
try {
|
|
146
|
+
await kv.create(bucketKey, JSON.stringify(initial));
|
|
147
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
|
|
148
|
+
return { acquired: true, currentInFlight: 1 };
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Race — another process created. Retry.
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Read current state, lazy-purge expired.
|
|
156
|
+
const current = this.parseBucket(entry);
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const active = current.leases.filter((l) => l.expiresAt > now);
|
|
159
|
+
// Idempotent re-acquire: refresh lease, don't grow count.
|
|
160
|
+
const existingIdx = active.findIndex((l) => l.runId === runId);
|
|
161
|
+
if (existingIdx >= 0) {
|
|
162
|
+
active[existingIdx] = { runId, expiresAt: leaseExpiresAt };
|
|
163
|
+
try {
|
|
164
|
+
await kv.update(bucketKey, JSON.stringify({ leases: active }), entry.revision);
|
|
165
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
|
|
166
|
+
return { acquired: true, currentInFlight: active.length };
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Limit check.
|
|
173
|
+
if (active.length >= concurrencyLimit) {
|
|
174
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "denied" }, attempt);
|
|
175
|
+
return { acquired: false, currentInFlight: active.length };
|
|
176
|
+
}
|
|
177
|
+
// Insert + CAS.
|
|
178
|
+
const updated = { leases: [...active, { runId, expiresAt: leaseExpiresAt }] };
|
|
179
|
+
try {
|
|
180
|
+
await kv.update(bucketKey, JSON.stringify(updated), entry.revision);
|
|
181
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
|
|
182
|
+
return { acquired: true, currentInFlight: updated.leases.length };
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
}
|
|
186
|
+
// Retry exhausted — fail-closed.
|
|
187
|
+
console.warn(`[blok][concurrency][nats-kv] acquireSlot exhausted ${MAX_CAS_RETRIES} CAS retries for ${workflowName}:${concurrencyKey}; denying slot to runId=${runId}`);
|
|
188
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "fail-closed" }, MAX_CAS_RETRIES);
|
|
189
|
+
return { acquired: false, currentInFlight: -1 };
|
|
190
|
+
}
|
|
191
|
+
async releaseSlot(workflowName, concurrencyKey, runId) {
|
|
192
|
+
const kv = this.requireKv();
|
|
193
|
+
const bucketKey = this.bucketKey(workflowName, concurrencyKey);
|
|
194
|
+
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
195
|
+
const entry = await this.safeGet(kv, bucketKey);
|
|
196
|
+
// PR 2 A6 — fetch failure on release. Lease will expire via
|
|
197
|
+
// TTL; safe to fail-fast.
|
|
198
|
+
if (entry === "fetch-failed") {
|
|
199
|
+
console.warn(`[blok][concurrency][nats-kv] releaseSlot fetch-failed for ${workflowName}:${concurrencyKey} (attempt ${attempt + 1}); lease for runId=${runId} will expire via TTL`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!entry)
|
|
203
|
+
return; // Idempotent — bucket already gone.
|
|
204
|
+
const current = this.parseBucket(entry);
|
|
205
|
+
const next = current.leases.filter((l) => l.runId !== runId);
|
|
206
|
+
// No-op when the runId wasn't holding a slot.
|
|
207
|
+
if (next.length === current.leases.length)
|
|
208
|
+
return;
|
|
209
|
+
if (next.length === 0) {
|
|
210
|
+
try {
|
|
211
|
+
await kv.delete(bucketKey);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Another process beat us to delete — fine.
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await kv.update(bucketKey, JSON.stringify({ leases: next }), entry.revision);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
}
|
|
225
|
+
console.warn(`[blok][concurrency][nats-kv] releaseSlot exhausted ${MAX_CAS_RETRIES} CAS retries for ${workflowName}:${concurrencyKey}; lease for runId=${runId} will expire via TTL`);
|
|
226
|
+
}
|
|
227
|
+
async purgeExpired(now) {
|
|
228
|
+
const kv = this.requireKv();
|
|
229
|
+
let purged = 0;
|
|
230
|
+
// Iterate all bucket keys.
|
|
231
|
+
for await (const key of kv.keys()) {
|
|
232
|
+
const entry = await this.safeGet(kv, key);
|
|
233
|
+
// Treat both legitimate misses and fetch failures as "skip
|
|
234
|
+
// this bucket" — purge is a best-effort sweep.
|
|
235
|
+
if (!entry || entry === "fetch-failed")
|
|
236
|
+
continue;
|
|
237
|
+
const current = this.parseBucket(entry);
|
|
238
|
+
const active = current.leases.filter((l) => l.expiresAt > now);
|
|
239
|
+
const expired = current.leases.length - active.length;
|
|
240
|
+
if (expired === 0)
|
|
241
|
+
continue;
|
|
242
|
+
if (active.length === 0) {
|
|
243
|
+
try {
|
|
244
|
+
await kv.delete(key);
|
|
245
|
+
purged += expired;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// best-effort
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
await kv.update(key, JSON.stringify({ leases: active }), entry.revision);
|
|
254
|
+
purged += expired;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// CAS conflict — leave for next sweep.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return purged;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* PR 2 A6 — distinguishes legitimate "key not found" from "broker
|
|
264
|
+
* unreachable / non-NotFound error". Returns:
|
|
265
|
+
* - `NatsKvEntry` on a successful fetch.
|
|
266
|
+
* - `null` when the key doesn't exist (NotFound code or null entry).
|
|
267
|
+
* - `"fetch-failed"` for any other error (transient broker outage,
|
|
268
|
+
* auth failure, network blip, etc.) so the OCC loop can fail-fast
|
|
269
|
+
* instead of spinning 10× before fail-closing.
|
|
270
|
+
*/
|
|
271
|
+
async safeGet(kv, key) {
|
|
272
|
+
try {
|
|
273
|
+
const e = await kv.get(key);
|
|
274
|
+
return e ?? null;
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
// NATS surfaces "not found" via a code. Different `nats`
|
|
278
|
+
// package versions use different shapes; cover the common ones.
|
|
279
|
+
const code = err.code;
|
|
280
|
+
if (code === "NotFound" || code === "404")
|
|
281
|
+
return null;
|
|
282
|
+
return "fetch-failed";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
parseBucket(entry) {
|
|
286
|
+
try {
|
|
287
|
+
const parsed = JSON.parse(entry.string());
|
|
288
|
+
if (!parsed || !Array.isArray(parsed.leases))
|
|
289
|
+
return { leases: [] };
|
|
290
|
+
return parsed;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return { leases: [] };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=NatsKvConcurrencyBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NatsKvConcurrencyBackend.js","sourceRoot":"","sources":["../../src/concurrency/NatsKvConcurrencyBackend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR 1-5 polish · thrown by `TriggerBase.run()` when a queued run's
|
|
3
|
+
* `concurrencyQueueTimeoutMs` (PR 5 B2) has elapsed before the gate granted
|
|
4
|
+
* a slot.
|
|
5
|
+
*
|
|
6
|
+
* Distinct from `ConcurrencyLimitError`: a TTL-expired queued run will NEVER
|
|
7
|
+
* succeed (the timer won't re-fire). Conflating it with a transient denial
|
|
8
|
+
* misleads HTTP clients into retrying — the queued run is permanently dead,
|
|
9
|
+
* so the only correct response is `410 Gone`.
|
|
10
|
+
*
|
|
11
|
+
* Triggers catch this and translate:
|
|
12
|
+
* - HTTP trigger → `410 Gone` with structured body (no `Retry-After` — it
|
|
13
|
+
* would contradict the 410 contract).
|
|
14
|
+
* - Worker trigger → ACK without retry (the in-process scheduler owns the
|
|
15
|
+
* eventual dispatch and won't reschedule an expired run).
|
|
16
|
+
*
|
|
17
|
+
* The run record itself is already flipped to `expired` by
|
|
18
|
+
* `tracker.markRunExpired` before this error is thrown, so observability
|
|
19
|
+
* surfaces (Studio status badge, `RUN_EXPIRED` event) are independent of
|
|
20
|
+
* the transport-level response.
|
|
21
|
+
*/
|
|
22
|
+
export interface QueueExpiredInfo {
|
|
23
|
+
/** Workflow name whose queue TTL elapsed. */
|
|
24
|
+
workflowName: string;
|
|
25
|
+
/** Resolved key value (after evaluating the `concurrencyKey` expression). */
|
|
26
|
+
concurrencyKey: string;
|
|
27
|
+
/**
|
|
28
|
+
* The deadline that was breached (ms since epoch). The run was queued
|
|
29
|
+
* with `expiresAt = scheduledAt + concurrencyQueueTimeoutMs` and the
|
|
30
|
+
* dispatcher observed `now > expiresAt`.
|
|
31
|
+
*/
|
|
32
|
+
queueExpiredAt: number;
|
|
33
|
+
/** Run id allocated by the tracer; flipped to `expired` before throw. */
|
|
34
|
+
runId: string;
|
|
35
|
+
}
|
|
36
|
+
export declare class QueueExpiredError extends Error {
|
|
37
|
+
readonly info: QueueExpiredInfo;
|
|
38
|
+
constructor(info: QueueExpiredInfo);
|
|
39
|
+
}
|
|
40
|
+
export declare function isQueueExpiredError(err: unknown): err is QueueExpiredError;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class QueueExpiredError extends Error {
|
|
2
|
+
info;
|
|
3
|
+
constructor(info) {
|
|
4
|
+
super(`Queued run expired for workflow '${info.workflowName}' (key='${info.concurrencyKey}', ` +
|
|
5
|
+
`queueExpiredAt=${info.queueExpiredAt}). Run will not be retried.`);
|
|
6
|
+
this.name = "QueueExpiredError";
|
|
7
|
+
this.info = info;
|
|
8
|
+
// Restore prototype chain when extending Error in transpiled code.
|
|
9
|
+
Object.setPrototypeOf(this, QueueExpiredError.prototype);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isQueueExpiredError(err) {
|
|
13
|
+
return err instanceof QueueExpiredError;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=QueueExpiredError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueueExpiredError.js","sourceRoot":"","sources":["../../src/concurrency/QueueExpiredError.ts"],"names":[],"mappings":"AAoCA,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC3B,IAAI,CAAmB;IAEvC,YAAY,IAAsB;QACjC,KAAK,CACJ,oCAAoC,IAAI,CAAC,YAAY,WAAW,IAAI,CAAC,cAAc,KAAK;YACvF,kBAAkB,IAAI,CAAC,cAAc,6BAA6B,CACnE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,mEAAmE;QACnE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC;CACD;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC/C,OAAO,GAAG,YAAY,iBAAiB,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 #6 follow-up · concurrency backend factory.
|
|
3
|
+
*
|
|
4
|
+
* Reads `BLOK_CONCURRENCY_BACKEND` and returns the matching backend
|
|
5
|
+
* instance, or `null` when the user wants the default in-process behavior.
|
|
6
|
+
*
|
|
7
|
+
* Trigger packages call this in `listen()` and pass the result to
|
|
8
|
+
* `RunTracker.getInstance().setConcurrencyBackend(backend)`.
|
|
9
|
+
*/
|
|
10
|
+
import type { ConcurrencyBackend } from "./ConcurrencyBackend";
|
|
11
|
+
/**
|
|
12
|
+
* Returns a configured `ConcurrencyBackend` based on
|
|
13
|
+
* `BLOK_CONCURRENCY_BACKEND`, or `null` for the default in-process backend.
|
|
14
|
+
*
|
|
15
|
+
* Recognized values:
|
|
16
|
+
* - unset / `""` / `"memory"` — null (use default in-process via RunStore)
|
|
17
|
+
* - `"nats-kv"` — NATS KV backend (requires `nats` package + reachable NATS server)
|
|
18
|
+
*
|
|
19
|
+
* Unknown values throw at startup with a clear error message — silently
|
|
20
|
+
* falling back would be dangerous (operator thinks they configured cross-
|
|
21
|
+
* process coordination but they didn't).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createConcurrencyBackend(): ConcurrencyBackend | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 #6 follow-up · concurrency backend factory.
|
|
3
|
+
*
|
|
4
|
+
* Reads `BLOK_CONCURRENCY_BACKEND` and returns the matching backend
|
|
5
|
+
* instance, or `null` when the user wants the default in-process behavior.
|
|
6
|
+
*
|
|
7
|
+
* Trigger packages call this in `listen()` and pass the result to
|
|
8
|
+
* `RunTracker.getInstance().setConcurrencyBackend(backend)`.
|
|
9
|
+
*/
|
|
10
|
+
import { NatsKvConcurrencyBackend } from "./NatsKvConcurrencyBackend";
|
|
11
|
+
/**
|
|
12
|
+
* Returns a configured `ConcurrencyBackend` based on
|
|
13
|
+
* `BLOK_CONCURRENCY_BACKEND`, or `null` for the default in-process backend.
|
|
14
|
+
*
|
|
15
|
+
* Recognized values:
|
|
16
|
+
* - unset / `""` / `"memory"` — null (use default in-process via RunStore)
|
|
17
|
+
* - `"nats-kv"` — NATS KV backend (requires `nats` package + reachable NATS server)
|
|
18
|
+
*
|
|
19
|
+
* Unknown values throw at startup with a clear error message — silently
|
|
20
|
+
* falling back would be dangerous (operator thinks they configured cross-
|
|
21
|
+
* process coordination but they didn't).
|
|
22
|
+
*/
|
|
23
|
+
export function createConcurrencyBackend() {
|
|
24
|
+
const kind = (process.env.BLOK_CONCURRENCY_BACKEND ?? "").trim().toLowerCase();
|
|
25
|
+
if (!kind || kind === "memory" || kind === "in-process")
|
|
26
|
+
return null;
|
|
27
|
+
switch (kind) {
|
|
28
|
+
case "nats-kv":
|
|
29
|
+
return new NatsKvConcurrencyBackend();
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unknown BLOK_CONCURRENCY_BACKEND='${kind}'. Expected one of: 'memory' (default), 'nats-kv'.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=createConcurrencyBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createConcurrencyBackend.js","sourceRoot":"","sources":["../../src/concurrency/createConcurrencyBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEtE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB;IACvC,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC/E,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IAErE,QAAQ,IAAI,EAAE,CAAC;QACd,KAAK,SAAS;YACb,OAAO,IAAI,wBAAwB,EAAE,CAAC;QACvC;YACC,MAAM,IAAI,KAAK,CAAC,qCAAqC,IAAI,oDAAoD,CAAC,CAAC;IACjH,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 #6 — extract concurrency-key config from a workflow's trigger
|
|
3
|
+
* block, regardless of which trigger type owns it (HTTP, Worker, …).
|
|
4
|
+
*
|
|
5
|
+
* Returns null when the trigger has no concurrency gate. The caller (a
|
|
6
|
+
* `TriggerBase.run()` invocation) treats null as "skip the gate, run the
|
|
7
|
+
* workflow normally" — zero-overhead default.
|
|
8
|
+
*
|
|
9
|
+
* Triggers that gain support for concurrency keys later only need to
|
|
10
|
+
* appear in {@link CONCURRENCY_TRIGGER_KEYS}.
|
|
11
|
+
*/
|
|
12
|
+
/** Parsed, normalized concurrency config ready for the gate. */
|
|
13
|
+
export interface NormalizedConcurrencyConfig {
|
|
14
|
+
/** The literal or `js/...` expression to resolve at run time. */
|
|
15
|
+
keyExpression: string;
|
|
16
|
+
/** Maximum concurrent runs per resolved key. */
|
|
17
|
+
limit: number;
|
|
18
|
+
/** Lease duration for the slot in milliseconds. */
|
|
19
|
+
leaseMs: number;
|
|
20
|
+
/**
|
|
21
|
+
* Behavior when the gate denies acquisition.
|
|
22
|
+
* - `"throw"` (default): emit `ConcurrencyLimitError` → HTTP 429 / Worker NACK.
|
|
23
|
+
* - `"queue"`: defer the run via `DeferredRunScheduler`, throw
|
|
24
|
+
* `DeferredDispatchSignal` with status `"queued"`.
|
|
25
|
+
*
|
|
26
|
+
* Tier 2 #6 follow-up. Reuses the Tier 2 #5+#7 deferred-dispatch plumbing.
|
|
27
|
+
*/
|
|
28
|
+
onLimit: "throw" | "queue";
|
|
29
|
+
/**
|
|
30
|
+
* PR 5 B2 — TTL on queued runs in milliseconds. When set AND
|
|
31
|
+
* `onLimit === "queue"`, queued runs that age past this timeout flip
|
|
32
|
+
* to `expired` instead of re-queueing. Undefined = retry indefinitely
|
|
33
|
+
* (lease-bounded).
|
|
34
|
+
*/
|
|
35
|
+
queueTimeoutMs?: number;
|
|
36
|
+
/**
|
|
37
|
+
* PR 5 B3 — capped exponential backoff config for `onLimit:queue`
|
|
38
|
+
* re-defer. Replaces the fixed 1s. Defaults applied at gate time:
|
|
39
|
+
* min=1000, max=30000, factor=2.
|
|
40
|
+
*/
|
|
41
|
+
queueRetry?: {
|
|
42
|
+
minBackoffMs?: number;
|
|
43
|
+
maxBackoffMs?: number;
|
|
44
|
+
factor?: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read a workflow's trigger config and return the normalized concurrency
|
|
49
|
+
* gate configuration, or null when the workflow has no gate.
|
|
50
|
+
*
|
|
51
|
+
* Defaults are applied here:
|
|
52
|
+
* - `concurrencyLimit` → `1` (Trigger.dev "named mutex per key" parity).
|
|
53
|
+
* - `concurrencyLeaseMs` → 1 hour, override via `BLOK_CONCURRENCY_LEASE_MS`
|
|
54
|
+
* process-wide.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readConcurrencyConfig(trigger: Record<string, unknown> | undefined | null): NormalizedConcurrencyConfig | null;
|
|
57
|
+
export declare const CONCURRENCY_DEFAULTS: {
|
|
58
|
+
readonly limit: 1;
|
|
59
|
+
readonly leaseMs: number;
|
|
60
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 #6 — extract concurrency-key config from a workflow's trigger
|
|
3
|
+
* block, regardless of which trigger type owns it (HTTP, Worker, …).
|
|
4
|
+
*
|
|
5
|
+
* Returns null when the trigger has no concurrency gate. The caller (a
|
|
6
|
+
* `TriggerBase.run()` invocation) treats null as "skip the gate, run the
|
|
7
|
+
* workflow normally" — zero-overhead default.
|
|
8
|
+
*
|
|
9
|
+
* Triggers that gain support for concurrency keys later only need to
|
|
10
|
+
* appear in {@link CONCURRENCY_TRIGGER_KEYS}.
|
|
11
|
+
*/
|
|
12
|
+
/** Trigger types whose schema declares the `ConcurrencyOptsFields` mixin. */
|
|
13
|
+
const CONCURRENCY_TRIGGER_KEYS = ["http", "worker"];
|
|
14
|
+
const DEFAULT_LIMIT = 1;
|
|
15
|
+
const DEFAULT_LEASE_MS = 60 * 60 * 1000;
|
|
16
|
+
/**
|
|
17
|
+
* Read a workflow's trigger config and return the normalized concurrency
|
|
18
|
+
* gate configuration, or null when the workflow has no gate.
|
|
19
|
+
*
|
|
20
|
+
* Defaults are applied here:
|
|
21
|
+
* - `concurrencyLimit` → `1` (Trigger.dev "named mutex per key" parity).
|
|
22
|
+
* - `concurrencyLeaseMs` → 1 hour, override via `BLOK_CONCURRENCY_LEASE_MS`
|
|
23
|
+
* process-wide.
|
|
24
|
+
*/
|
|
25
|
+
export function readConcurrencyConfig(trigger) {
|
|
26
|
+
if (!trigger)
|
|
27
|
+
return null;
|
|
28
|
+
for (const key of CONCURRENCY_TRIGGER_KEYS) {
|
|
29
|
+
const cfg = trigger[key];
|
|
30
|
+
if (!cfg)
|
|
31
|
+
continue;
|
|
32
|
+
const keyExpression = typeof cfg.concurrencyKey === "string" ? cfg.concurrencyKey.trim() : "";
|
|
33
|
+
if (!keyExpression)
|
|
34
|
+
continue;
|
|
35
|
+
const limit = Number.isInteger(cfg.concurrencyLimit) ? cfg.concurrencyLimit : DEFAULT_LIMIT;
|
|
36
|
+
const envLeaseRaw = process.env.BLOK_CONCURRENCY_LEASE_MS;
|
|
37
|
+
const envLease = envLeaseRaw && /^\d+$/.test(envLeaseRaw) ? Number(envLeaseRaw) : null;
|
|
38
|
+
const perTriggerLease = Number.isInteger(cfg.concurrencyLeaseMs) ? cfg.concurrencyLeaseMs : null;
|
|
39
|
+
// Per-trigger value wins over env override; env wins over the hard default.
|
|
40
|
+
const leaseMs = perTriggerLease ?? envLease ?? DEFAULT_LEASE_MS;
|
|
41
|
+
// onLimit: only "throw" (default) and "queue" are valid; anything else
|
|
42
|
+
// falls back to "throw" (defensive — schema already rejects bad values).
|
|
43
|
+
const onLimit = cfg.onLimit === "queue" ? "queue" : "throw";
|
|
44
|
+
// PR 5 B2 — queue TTL. Only meaningful when onLimit === "queue".
|
|
45
|
+
const queueTimeoutMs = onLimit === "queue" && Number.isInteger(cfg.concurrencyQueueTimeoutMs)
|
|
46
|
+
? cfg.concurrencyQueueTimeoutMs
|
|
47
|
+
: undefined;
|
|
48
|
+
// PR 5 B3 — capped exponential backoff. Only meaningful when onLimit === "queue".
|
|
49
|
+
const queueRetry = onLimit === "queue" && cfg.concurrencyQueueRetry && typeof cfg.concurrencyQueueRetry === "object"
|
|
50
|
+
? cfg.concurrencyQueueRetry
|
|
51
|
+
: undefined;
|
|
52
|
+
return { keyExpression, limit, leaseMs, onLimit, queueTimeoutMs, queueRetry };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export const CONCURRENCY_DEFAULTS = {
|
|
57
|
+
limit: DEFAULT_LIMIT,
|
|
58
|
+
leaseMs: DEFAULT_LEASE_MS,
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=readConcurrencyConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readConcurrencyConfig.js","sourceRoot":"","sources":["../../src/concurrency/readConcurrencyConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,6EAA6E;AAC7E,MAAM,wBAAwB,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAU,CAAC;AAE7D,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,gBAAgB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAsCxC;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CACpC,OAAmD;IAEnD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,KAAK,MAAM,GAAG,IAAI,wBAAwB,EAAE,CAAC;QAC5C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CASX,CAAC;QACb,IAAI,CAAC,GAAG;YAAE,SAAS;QAEnB,MAAM,aAAa,GAAG,OAAO,GAAG,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9F,IAAI,CAAC,aAAa;YAAE,SAAS;QAE7B,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,gBAA2B,CAAC,CAAC,CAAC,aAAa,CAAC;QAExG,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;QAC1D,MAAM,QAAQ,GAAG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvF,MAAM,eAAe,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,kBAA6B,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7G,4EAA4E;QAC5E,MAAM,OAAO,GAAG,eAAe,IAAI,QAAQ,IAAI,gBAAgB,CAAC;QAEhE,uEAAuE;QACvE,yEAAyE;QACzE,MAAM,OAAO,GAAsB,GAAG,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;QAE/E,iEAAiE;QACjE,MAAM,cAAc,GACnB,OAAO,KAAK,OAAO,IAAI,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,yBAAyB,CAAC;YACrE,CAAC,CAAE,GAAG,CAAC,yBAAoC;YAC3C,CAAC,CAAC,SAAS,CAAC;QAEd,kFAAkF;QAClF,MAAM,UAAU,GACf,OAAO,KAAK,OAAO,IAAI,GAAG,CAAC,qBAAqB,IAAI,OAAO,GAAG,CAAC,qBAAqB,KAAK,QAAQ;YAChG,CAAC,CAAE,GAAG,CAAC,qBAAmE;YAC1E,CAAC,CAAC,SAAS,CAAC;QAEd,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC;IAC/E,CAAC;IAED,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG;IACnC,KAAK,EAAE,aAAa;IACpB,OAAO,EAAE,gBAAgB;CAChB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Context } from "@blokjs/shared";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a step's `idempotencyKey` value against the live context.
|
|
4
|
+
*
|
|
5
|
+
* Authors may write a literal string (`"static-key"`) OR a `$ proxy`
|
|
6
|
+
* expression that compiled to a `js/...` string at workflow-definition
|
|
7
|
+
* time (`"js/ctx.req.body.requestId"` produced by `$.req.body.requestId`).
|
|
8
|
+
* This helper handles both.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` when:
|
|
11
|
+
* - the key is undefined / empty / not a string
|
|
12
|
+
* - the `js/` expression evaluates to null/undefined
|
|
13
|
+
* - the `js/` expression throws (treat as cache miss; the step still runs)
|
|
14
|
+
*
|
|
15
|
+
* The helper never throws — a failed key resolution falls back to "no
|
|
16
|
+
* caching for this step on this run", which is the safest interpretation.
|
|
17
|
+
*
|
|
18
|
+
* @internal Used by `RunnerSteps` before consulting the idempotency cache.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveIdempotencyKey(rawKey: string | undefined, ctx: Context): string | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const JS_PREFIX = "js/";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a step's `idempotencyKey` value against the live context.
|
|
4
|
+
*
|
|
5
|
+
* Authors may write a literal string (`"static-key"`) OR a `$ proxy`
|
|
6
|
+
* expression that compiled to a `js/...` string at workflow-definition
|
|
7
|
+
* time (`"js/ctx.req.body.requestId"` produced by `$.req.body.requestId`).
|
|
8
|
+
* This helper handles both.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` when:
|
|
11
|
+
* - the key is undefined / empty / not a string
|
|
12
|
+
* - the `js/` expression evaluates to null/undefined
|
|
13
|
+
* - the `js/` expression throws (treat as cache miss; the step still runs)
|
|
14
|
+
*
|
|
15
|
+
* The helper never throws — a failed key resolution falls back to "no
|
|
16
|
+
* caching for this step on this run", which is the safest interpretation.
|
|
17
|
+
*
|
|
18
|
+
* @internal Used by `RunnerSteps` before consulting the idempotency cache.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveIdempotencyKey(rawKey, ctx) {
|
|
21
|
+
if (typeof rawKey !== "string" || rawKey.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
if (!rawKey.startsWith(JS_PREFIX))
|
|
24
|
+
return rawKey;
|
|
25
|
+
const expr = rawKey.slice(JS_PREFIX.length);
|
|
26
|
+
try {
|
|
27
|
+
const fn = new Function("ctx", `"use strict"; return (${expr});`);
|
|
28
|
+
const value = fn(ctx);
|
|
29
|
+
if (value === null || value === undefined)
|
|
30
|
+
return null;
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=resolveIdempotencyKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveIdempotencyKey.js","sourceRoot":"","sources":["../../src/idempotency/resolveIdempotencyKey.ts"],"names":[],"mappings":"AAEA,MAAM,SAAS,GAAG,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAA0B,EAAE,GAAY;IAC7E,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,MAAM,CAAC;IAEjD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC;QACJ,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,KAAK,EAAE,yBAAyB,IAAI,IAAI,CAAC,CAAC;QAClE,MAAM,KAAK,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACvD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC"}
|