@blokjs/runner 0.2.2 → 0.6.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 (213) hide show
  1. package/dist/Blok.js +32 -3
  2. package/dist/Blok.js.map +1 -1
  3. package/dist/Configuration.d.ts +59 -5
  4. package/dist/Configuration.js +366 -96
  5. package/dist/Configuration.js.map +1 -1
  6. package/dist/ForEachNode.d.ts +59 -0
  7. package/dist/ForEachNode.js +522 -0
  8. package/dist/ForEachNode.js.map +1 -0
  9. package/dist/LoopMaxIterationsError.d.ts +11 -0
  10. package/dist/LoopMaxIterationsError.js +18 -0
  11. package/dist/LoopMaxIterationsError.js.map +1 -0
  12. package/dist/LoopNode.d.ts +36 -0
  13. package/dist/LoopNode.js +182 -0
  14. package/dist/LoopNode.js.map +1 -0
  15. package/dist/PayloadTooLargeError.d.ts +19 -0
  16. package/dist/PayloadTooLargeError.js +29 -0
  17. package/dist/PayloadTooLargeError.js.map +1 -0
  18. package/dist/RunCancelledError.d.ts +17 -0
  19. package/dist/RunCancelledError.js +25 -0
  20. package/dist/RunCancelledError.js.map +1 -0
  21. package/dist/Runner.d.ts +11 -1
  22. package/dist/Runner.js +9 -2
  23. package/dist/Runner.js.map +1 -1
  24. package/dist/RunnerSteps.js +648 -44
  25. package/dist/RunnerSteps.js.map +1 -1
  26. package/dist/RuntimeAdapterNode.d.ts +2 -1
  27. package/dist/RuntimeAdapterNode.js +2 -2
  28. package/dist/RuntimeAdapterNode.js.map +1 -1
  29. package/dist/RuntimeRegistry.d.ts +23 -2
  30. package/dist/RuntimeRegistry.js +31 -2
  31. package/dist/RuntimeRegistry.js.map +1 -1
  32. package/dist/SubworkflowNode.d.ts +181 -0
  33. package/dist/SubworkflowNode.js +479 -0
  34. package/dist/SubworkflowNode.js.map +1 -0
  35. package/dist/SwitchNode.d.ts +37 -0
  36. package/dist/SwitchNode.js +153 -0
  37. package/dist/SwitchNode.js.map +1 -0
  38. package/dist/TriggerBase.d.ts +178 -0
  39. package/dist/TriggerBase.js +1032 -5
  40. package/dist/TriggerBase.js.map +1 -1
  41. package/dist/TryCatchNode.d.ts +32 -0
  42. package/dist/TryCatchNode.js +207 -0
  43. package/dist/TryCatchNode.js.map +1 -0
  44. package/dist/WaitDispatchRequest.d.ts +38 -0
  45. package/dist/WaitDispatchRequest.js +13 -0
  46. package/dist/WaitDispatchRequest.js.map +1 -0
  47. package/dist/WaitNode.d.ts +23 -0
  48. package/dist/WaitNode.js +26 -0
  49. package/dist/WaitNode.js.map +1 -0
  50. package/dist/adapters/grpc/GrpcCodec.js +2 -2
  51. package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +6 -4
  52. package/dist/adapters/grpc/GrpcRuntimeAdapter.js +6 -4
  53. package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -1
  54. package/dist/adapters/grpc/types.d.ts +7 -5
  55. package/dist/adapters/grpc/types.js.map +1 -1
  56. package/dist/adapters/transport.d.ts +12 -41
  57. package/dist/adapters/transport.js +21 -70
  58. package/dist/adapters/transport.js.map +1 -1
  59. package/dist/cache/NodeResultCache.js +7 -0
  60. package/dist/cache/NodeResultCache.js.map +1 -1
  61. package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
  62. package/dist/concurrency/ConcurrencyBackend.js +20 -0
  63. package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
  64. package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
  65. package/dist/concurrency/ConcurrencyLimitError.js +16 -0
  66. package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
  67. package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
  68. package/dist/concurrency/NatsKvConcurrencyBackend.js +310 -0
  69. package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
  70. package/dist/concurrency/QueueExpiredError.d.ts +40 -0
  71. package/dist/concurrency/QueueExpiredError.js +15 -0
  72. package/dist/concurrency/QueueExpiredError.js.map +1 -0
  73. package/dist/concurrency/RedisConcurrencyBackend.d.ts +64 -0
  74. package/dist/concurrency/RedisConcurrencyBackend.js +374 -0
  75. package/dist/concurrency/RedisConcurrencyBackend.js.map +1 -0
  76. package/dist/concurrency/createConcurrencyBackend.d.ts +24 -0
  77. package/dist/concurrency/createConcurrencyBackend.js +38 -0
  78. package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
  79. package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
  80. package/dist/concurrency/readConcurrencyConfig.js +60 -0
  81. package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
  82. package/dist/defineNode.d.ts +8 -0
  83. package/dist/defineNode.js +25 -5
  84. package/dist/defineNode.js.map +1 -1
  85. package/dist/graphql/GraphQLSchemaGenerator.js +1 -1
  86. package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -1
  87. package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
  88. package/dist/idempotency/resolveIdempotencyKey.js +37 -0
  89. package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
  90. package/dist/index.d.ts +30 -6
  91. package/dist/index.js +55 -6
  92. package/dist/index.js.map +1 -1
  93. package/dist/marketplace/RuntimeCatalog.d.ts +6 -0
  94. package/dist/marketplace/RuntimeCatalog.js.map +1 -1
  95. package/dist/marketplace/RuntimeDiscovery.d.ts +2 -2
  96. package/dist/marketplace/RuntimeDiscovery.js +18 -6
  97. package/dist/marketplace/RuntimeDiscovery.js.map +1 -1
  98. package/dist/monitoring/ConcurrencyMetrics.d.ts +82 -0
  99. package/dist/monitoring/ConcurrencyMetrics.js +139 -0
  100. package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
  101. package/dist/monitoring/ForEachWaitMetrics.d.ts +22 -0
  102. package/dist/monitoring/ForEachWaitMetrics.js +36 -0
  103. package/dist/monitoring/ForEachWaitMetrics.js.map +1 -0
  104. package/dist/monitoring/JanitorMetrics.d.ts +27 -0
  105. package/dist/monitoring/JanitorMetrics.js +48 -0
  106. package/dist/monitoring/JanitorMetrics.js.map +1 -0
  107. package/dist/openapi/OpenAPIGenerator.js +7 -2
  108. package/dist/openapi/OpenAPIGenerator.js.map +1 -1
  109. package/dist/runtime/PrimitiveStack.d.ts +64 -0
  110. package/dist/runtime/PrimitiveStack.js +92 -0
  111. package/dist/runtime/PrimitiveStack.js.map +1 -0
  112. package/dist/scheduling/DebounceBackend.d.ts +108 -0
  113. package/dist/scheduling/DebounceBackend.js +23 -0
  114. package/dist/scheduling/DebounceBackend.js.map +1 -0
  115. package/dist/scheduling/DebounceCoordinator.d.ts +141 -0
  116. package/dist/scheduling/DebounceCoordinator.js +362 -0
  117. package/dist/scheduling/DebounceCoordinator.js.map +1 -0
  118. package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
  119. package/dist/scheduling/DeferredDispatchSignal.js +14 -0
  120. package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
  121. package/dist/scheduling/DeferredRunScheduler.d.ts +96 -0
  122. package/dist/scheduling/DeferredRunScheduler.js +256 -0
  123. package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
  124. package/dist/scheduling/NatsKvDebounceBackend.d.ts +53 -0
  125. package/dist/scheduling/NatsKvDebounceBackend.js +334 -0
  126. package/dist/scheduling/NatsKvDebounceBackend.js.map +1 -0
  127. package/dist/scheduling/RedisDebounceBackend.d.ts +49 -0
  128. package/dist/scheduling/RedisDebounceBackend.js +356 -0
  129. package/dist/scheduling/RedisDebounceBackend.js.map +1 -0
  130. package/dist/scheduling/createDebounceBackend.d.ts +25 -0
  131. package/dist/scheduling/createDebounceBackend.js +39 -0
  132. package/dist/scheduling/createDebounceBackend.js.map +1 -0
  133. package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
  134. package/dist/scheduling/readSchedulingConfig.js +52 -0
  135. package/dist/scheduling/readSchedulingConfig.js.map +1 -0
  136. package/dist/security/AuditLogger.js +1 -1
  137. package/dist/security/AuditLogger.js.map +1 -1
  138. package/dist/security/AuthMiddleware.d.ts +19 -20
  139. package/dist/security/AuthMiddleware.js +35 -20
  140. package/dist/security/AuthMiddleware.js.map +1 -1
  141. package/dist/security/OAuthProvider.js +2 -2
  142. package/dist/security/OAuthProvider.js.map +1 -1
  143. package/dist/security/SecretManager.js +14 -13
  144. package/dist/security/SecretManager.js.map +1 -1
  145. package/dist/security/index.d.ts +3 -1
  146. package/dist/security/index.js +3 -1
  147. package/dist/security/index.js.map +1 -1
  148. package/dist/testing/TestHarness.d.ts +27 -12
  149. package/dist/testing/TestHarness.js +19 -3
  150. package/dist/testing/TestHarness.js.map +1 -1
  151. package/dist/testing/WorkflowTestRunner.js +0 -7
  152. package/dist/testing/WorkflowTestRunner.js.map +1 -1
  153. package/dist/timeouts/StepTimeoutError.d.ts +22 -0
  154. package/dist/timeouts/StepTimeoutError.js +31 -0
  155. package/dist/timeouts/StepTimeoutError.js.map +1 -0
  156. package/dist/tracing/InMemoryRunStore.d.ts +41 -1
  157. package/dist/tracing/InMemoryRunStore.js +239 -0
  158. package/dist/tracing/InMemoryRunStore.js.map +1 -1
  159. package/dist/tracing/Janitor.d.ts +70 -0
  160. package/dist/tracing/Janitor.js +150 -0
  161. package/dist/tracing/Janitor.js.map +1 -0
  162. package/dist/tracing/PostgresRunStore.d.ts +57 -1
  163. package/dist/tracing/PostgresRunStore.js +711 -6
  164. package/dist/tracing/PostgresRunStore.js.map +1 -1
  165. package/dist/tracing/RoutingDiagnostics.d.ts +55 -0
  166. package/dist/tracing/RoutingDiagnostics.js +50 -0
  167. package/dist/tracing/RoutingDiagnostics.js.map +1 -0
  168. package/dist/tracing/RunStore.d.ts +181 -1
  169. package/dist/tracing/RunTracker.d.ts +244 -9
  170. package/dist/tracing/RunTracker.js +594 -1
  171. package/dist/tracing/RunTracker.js.map +1 -1
  172. package/dist/tracing/SqliteRunStore.d.ts +79 -2
  173. package/dist/tracing/SqliteRunStore.js +775 -16
  174. package/dist/tracing/SqliteRunStore.js.map +1 -1
  175. package/dist/tracing/TraceRouter.d.ts +20 -2
  176. package/dist/tracing/TraceRouter.js +612 -6
  177. package/dist/tracing/TraceRouter.js.map +1 -1
  178. package/dist/tracing/createStore.js +14 -3
  179. package/dist/tracing/createStore.js.map +1 -1
  180. package/dist/tracing/metadataFilter.d.ts +63 -0
  181. package/dist/tracing/metadataFilter.js +224 -0
  182. package/dist/tracing/metadataFilter.js.map +1 -0
  183. package/dist/tracing/sanitize.d.ts +11 -0
  184. package/dist/tracing/sanitize.js +29 -0
  185. package/dist/tracing/sanitize.js.map +1 -1
  186. package/dist/tracing/types.d.ts +672 -2
  187. package/dist/utils/createChildContext.d.ts +32 -0
  188. package/dist/utils/createChildContext.js +113 -0
  189. package/dist/utils/createChildContext.js.map +1 -0
  190. package/dist/utils/envAllowlist.d.ts +35 -0
  191. package/dist/utils/envAllowlist.js +113 -0
  192. package/dist/utils/envAllowlist.js.map +1 -0
  193. package/dist/version/RuntimeVersionValidator.d.ts +38 -0
  194. package/dist/version/RuntimeVersionValidator.js +121 -0
  195. package/dist/version/RuntimeVersionValidator.js.map +1 -0
  196. package/dist/visualization/WorkflowVisualizer.js +4 -4
  197. package/dist/visualization/WorkflowVisualizer.js.map +1 -1
  198. package/dist/workflow/PersistenceHelper.d.ts +18 -10
  199. package/dist/workflow/PersistenceHelper.js +35 -9
  200. package/dist/workflow/PersistenceHelper.js.map +1 -1
  201. package/dist/workflow/WorkflowNormalizer.d.ts +48 -42
  202. package/dist/workflow/WorkflowNormalizer.js +650 -18
  203. package/dist/workflow/WorkflowNormalizer.js.map +1 -1
  204. package/dist/workflow/WorkflowRegistry.d.ts +186 -0
  205. package/dist/workflow/WorkflowRegistry.js +202 -0
  206. package/dist/workflow/WorkflowRegistry.js.map +1 -0
  207. package/dist/workflow/sampleBody.d.ts +54 -0
  208. package/dist/workflow/sampleBody.js +320 -0
  209. package/dist/workflow/sampleBody.js.map +1 -0
  210. package/package.json +3 -8
  211. package/dist/adapters/HttpRuntimeAdapter.d.ts +0 -79
  212. package/dist/adapters/HttpRuntimeAdapter.js +0 -233
  213. package/dist/adapters/HttpRuntimeAdapter.js.map +0 -1
@@ -0,0 +1,310 @@
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
+ // nats.js v2.x — KV lives at `nc.jetstream().views.kv(name)`. The
92
+ // returned `KV` auto-creates the bucket on first use given the
93
+ // connection has KV bucket-create permission. (Earlier versions
94
+ // exposed `nc.kv(name)` directly; that API was removed.)
95
+ const js = this.nc.jetstream();
96
+ this.kv = await js.views.kv(this.config.bucketName);
97
+ this.connected = true;
98
+ }
99
+ async disconnect() {
100
+ if (!this.connected)
101
+ return;
102
+ try {
103
+ await this.nc?.drain();
104
+ }
105
+ finally {
106
+ this.nc = null;
107
+ this.kv = null;
108
+ this.connected = false;
109
+ }
110
+ }
111
+ bucketKey(workflowName, concurrencyKey) {
112
+ // Use `__` (double underscore) — KV keys cannot contain `.` or
113
+ // `>` per NATS subject grammar; `__` is unambiguous and allows
114
+ // arbitrary workflow / key strings.
115
+ return `${this.encodeSegment(workflowName)}__${this.encodeSegment(concurrencyKey)}`;
116
+ }
117
+ encodeSegment(s) {
118
+ // NATS KV keys must match `[-/_=\.a-zA-Z0-9]+`. Replace anything
119
+ // outside the safe set with hex escape `_HHHH_` to keep the
120
+ // roundtrip lossless.
121
+ return s.replace(/[^-_=.a-zA-Z0-9]/g, (ch) => `_${ch.codePointAt(0)?.toString(16)}_`);
122
+ }
123
+ requireKv() {
124
+ if (!this.kv) {
125
+ throw new Error("NatsKvConcurrencyBackend not connected — call connect() first.");
126
+ }
127
+ return this.kv;
128
+ }
129
+ async acquireSlot(workflowName, concurrencyKey, concurrencyLimit, runId, leaseExpiresAt) {
130
+ const kv = this.requireKv();
131
+ const bucketKey = this.bucketKey(workflowName, concurrencyKey);
132
+ // PR 3 D2 — record OCC retry depth + outcome on every exit path.
133
+ const metricAttrs = { workflow_name: workflowName, concurrency_key: concurrencyKey };
134
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
135
+ const entry = await this.safeGet(kv, bucketKey);
136
+ // PR 2 A6 — fetch failure (broker unreachable / non-NotFound
137
+ // error). Spinning 10× CAS retries on a connection problem just
138
+ // burns latency. Fail-fast so the trigger sees the issue and
139
+ // can fall back / alert. Existing run continues with no slot;
140
+ // the gate is conservative.
141
+ if (entry === "fetch-failed") {
142
+ console.warn(`[blok][concurrency][nats-kv] acquireSlot fetch-failed for ${workflowName}:${concurrencyKey} (attempt ${attempt + 1}); failing closed`);
143
+ ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "fail-closed" }, attempt);
144
+ return { acquired: false, currentInFlight: -1 };
145
+ }
146
+ if (!entry) {
147
+ // Bucket doesn't exist — create with first lease.
148
+ const initial = { leases: [{ runId, expiresAt: leaseExpiresAt }] };
149
+ try {
150
+ await kv.create(bucketKey, JSON.stringify(initial));
151
+ ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
152
+ return { acquired: true, currentInFlight: 1 };
153
+ }
154
+ catch {
155
+ // Race — another process created. Retry.
156
+ continue;
157
+ }
158
+ }
159
+ // Read current state, lazy-purge expired.
160
+ const current = this.parseBucket(entry);
161
+ const now = Date.now();
162
+ const active = current.leases.filter((l) => l.expiresAt > now);
163
+ // Idempotent re-acquire: refresh lease, don't grow count.
164
+ const existingIdx = active.findIndex((l) => l.runId === runId);
165
+ if (existingIdx >= 0) {
166
+ active[existingIdx] = { runId, expiresAt: leaseExpiresAt };
167
+ try {
168
+ await kv.update(bucketKey, JSON.stringify({ leases: active }), entry.revision);
169
+ ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
170
+ return { acquired: true, currentInFlight: active.length };
171
+ }
172
+ catch {
173
+ continue;
174
+ }
175
+ }
176
+ // Limit check.
177
+ if (active.length >= concurrencyLimit) {
178
+ ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "denied" }, attempt);
179
+ return { acquired: false, currentInFlight: active.length };
180
+ }
181
+ // Insert + CAS.
182
+ const updated = { leases: [...active, { runId, expiresAt: leaseExpiresAt }] };
183
+ try {
184
+ await kv.update(bucketKey, JSON.stringify(updated), entry.revision);
185
+ ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
186
+ return { acquired: true, currentInFlight: updated.leases.length };
187
+ }
188
+ catch { }
189
+ }
190
+ // Retry exhausted — fail-closed.
191
+ console.warn(`[blok][concurrency][nats-kv] acquireSlot exhausted ${MAX_CAS_RETRIES} CAS retries for ${workflowName}:${concurrencyKey}; denying slot to runId=${runId}`);
192
+ ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "fail-closed" }, MAX_CAS_RETRIES);
193
+ return { acquired: false, currentInFlight: -1 };
194
+ }
195
+ async releaseSlot(workflowName, concurrencyKey, runId) {
196
+ const kv = this.requireKv();
197
+ const bucketKey = this.bucketKey(workflowName, concurrencyKey);
198
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
199
+ const entry = await this.safeGet(kv, bucketKey);
200
+ // PR 2 A6 — fetch failure on release. Lease will expire via
201
+ // TTL; safe to fail-fast.
202
+ if (entry === "fetch-failed") {
203
+ console.warn(`[blok][concurrency][nats-kv] releaseSlot fetch-failed for ${workflowName}:${concurrencyKey} (attempt ${attempt + 1}); lease for runId=${runId} will expire via TTL`);
204
+ return;
205
+ }
206
+ if (!entry)
207
+ return; // Idempotent — bucket already gone.
208
+ const current = this.parseBucket(entry);
209
+ const next = current.leases.filter((l) => l.runId !== runId);
210
+ // No-op when the runId wasn't holding a slot.
211
+ if (next.length === current.leases.length)
212
+ return;
213
+ if (next.length === 0) {
214
+ try {
215
+ await kv.delete(bucketKey);
216
+ return;
217
+ }
218
+ catch {
219
+ // Another process beat us to delete — fine.
220
+ return;
221
+ }
222
+ }
223
+ try {
224
+ await kv.update(bucketKey, JSON.stringify({ leases: next }), entry.revision);
225
+ return;
226
+ }
227
+ catch { }
228
+ }
229
+ console.warn(`[blok][concurrency][nats-kv] releaseSlot exhausted ${MAX_CAS_RETRIES} CAS retries for ${workflowName}:${concurrencyKey}; lease for runId=${runId} will expire via TTL`);
230
+ }
231
+ async purgeExpired(now) {
232
+ const kv = this.requireKv();
233
+ let purged = 0;
234
+ // **DRAIN THE ITERATOR FIRST.** nats.js v2.x `kv.keys()` returns a
235
+ // `QueuedIterator` backed by a JetStream watch consumer. Calling
236
+ // `kv.get()` mid-iteration interferes with the iterator's internal
237
+ // state — observed in practice that subsequent yields silently
238
+ // drop. Collect every key into an array before doing per-key
239
+ // reads, then operate on the array.
240
+ const allKeys = [];
241
+ for await (const key of await kv.keys()) {
242
+ allKeys.push(key);
243
+ }
244
+ for (const key of allKeys) {
245
+ const entry = await this.safeGet(kv, key);
246
+ // Treat both legitimate misses and fetch failures as "skip
247
+ // this bucket" — purge is a best-effort sweep.
248
+ if (!entry || entry === "fetch-failed")
249
+ continue;
250
+ const current = this.parseBucket(entry);
251
+ const active = current.leases.filter((l) => l.expiresAt > now);
252
+ const expired = current.leases.length - active.length;
253
+ if (expired === 0)
254
+ continue;
255
+ if (active.length === 0) {
256
+ try {
257
+ await kv.delete(key);
258
+ purged += expired;
259
+ }
260
+ catch {
261
+ // best-effort
262
+ }
263
+ continue;
264
+ }
265
+ try {
266
+ await kv.update(key, JSON.stringify({ leases: active }), entry.revision);
267
+ purged += expired;
268
+ }
269
+ catch {
270
+ // CAS conflict — leave for next sweep.
271
+ }
272
+ }
273
+ return purged;
274
+ }
275
+ /**
276
+ * PR 2 A6 — distinguishes legitimate "key not found" from "broker
277
+ * unreachable / non-NotFound error". Returns:
278
+ * - `NatsKvEntry` on a successful fetch.
279
+ * - `null` when the key doesn't exist (NotFound code or null entry).
280
+ * - `"fetch-failed"` for any other error (transient broker outage,
281
+ * auth failure, network blip, etc.) so the OCC loop can fail-fast
282
+ * instead of spinning 10× before fail-closing.
283
+ */
284
+ async safeGet(kv, key) {
285
+ try {
286
+ const e = await kv.get(key);
287
+ return e ?? null;
288
+ }
289
+ catch (err) {
290
+ // NATS surfaces "not found" via a code. Different `nats`
291
+ // package versions use different shapes; cover the common ones.
292
+ const code = err.code;
293
+ if (code === "NotFound" || code === "404")
294
+ return null;
295
+ return "fetch-failed";
296
+ }
297
+ }
298
+ parseBucket(entry) {
299
+ try {
300
+ const parsed = JSON.parse(entry.string());
301
+ if (!parsed || !Array.isArray(parsed.leases))
302
+ return { leases: [] };
303
+ return parsed;
304
+ }
305
+ catch {
306
+ return { leases: [] };
307
+ }
308
+ }
309
+ }
310
+ //# 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,64 @@
1
+ /**
2
+ * Tier C #4 follow-up · Redis-backed concurrency backend.
3
+ *
4
+ * Coordinates per-(workflow, concurrencyKey) lease state across processes
5
+ * via a single Redis key per bucket. Atomicity comes from server-side Lua
6
+ * scripts — `EVAL` runs single-threaded against the keyspace, so the
7
+ * read → filter → check-limit → write sequence is a single round-trip
8
+ * with no OCC retry loop (the headline win over the NATS KV backend's
9
+ * `WATCH`/`MULTI`/`EXEC`-style optimistic concurrency).
10
+ *
11
+ * Storage model: one Redis string key per `(workflowName, concurrencyKey)`
12
+ * bucket. Value is a JSON-encoded `{leases: [{runId, expiresAt}]}`
13
+ * document. Bounded-cardinality assumption identical to NATS KV — typical
14
+ * concurrency keys hold 1-50 active leases.
15
+ *
16
+ * Lease leak: each lease carries an `expiresAt`. Expired leases are
17
+ * lazy-purged inside the Lua script that observes them; an explicit
18
+ * `purgeExpired` SCAN sweep is also exposed for janitor use.
19
+ *
20
+ * Connection: ioredis is loaded via dynamic `import("ioredis")` so the
21
+ * dependency stays optional. Matches the existing pattern used by
22
+ * `triggers/worker`'s `RedisStreamsAdapter` and
23
+ * `triggers/pubsub`'s `RedisStreamsPubSubAdapter`.
24
+ */
25
+ import type { ConcurrencySlotResult } from "../tracing/types";
26
+ import type { ConcurrencyBackend } from "./ConcurrencyBackend";
27
+ export interface RedisConcurrencyConfig {
28
+ /** Full Redis connection URL (e.g. `redis://[user:pass@]host:port[/db]`). Takes precedence over host/port. */
29
+ url?: string;
30
+ host?: string;
31
+ port?: number;
32
+ password?: string;
33
+ username?: string;
34
+ db?: number;
35
+ tls?: boolean;
36
+ /** Namespace prefix for every Redis key the backend touches. */
37
+ keyPrefix: string;
38
+ }
39
+ /**
40
+ * Read configuration from environment variables. Used by
41
+ * {@link createConcurrencyBackend} when the operator opts into Redis.
42
+ */
43
+ export declare function readRedisConfigFromEnv(): RedisConcurrencyConfig;
44
+ export declare class RedisConcurrencyBackend implements ConcurrencyBackend {
45
+ readonly name = "redis";
46
+ private client;
47
+ private readonly config;
48
+ private connected;
49
+ constructor(config?: Partial<RedisConcurrencyConfig>);
50
+ connect(): Promise<void>;
51
+ disconnect(): Promise<void>;
52
+ private bucketKey;
53
+ private encodeSegment;
54
+ private requireClient;
55
+ acquireSlot(workflowName: string, concurrencyKey: string, concurrencyLimit: number, runId: string, leaseExpiresAt: number): Promise<ConcurrencySlotResult>;
56
+ releaseSlot(workflowName: string, concurrencyKey: string, runId: string): Promise<void>;
57
+ purgeExpired(now: number): Promise<number>;
58
+ /**
59
+ * Decode the `{acquired, currentInFlight}` pair from a Lua eval result.
60
+ * ioredis returns Redis arrays as plain JS arrays of (string | number)
61
+ * — the script returns integers, so both elements should be numbers.
62
+ */
63
+ private parsePair;
64
+ }