@smthrs/run-store 0.0.0-stage → 1.0.0-rc.3

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 (157) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +21 -0
  3. package/README.md +195 -2
  4. package/dist/cjs/AttemptStore.d.ts +359 -0
  5. package/dist/cjs/AttemptStore.d.ts.map +1 -0
  6. package/dist/cjs/AttemptStore.js +731 -0
  7. package/dist/cjs/AttemptStore.js.map +7 -0
  8. package/dist/cjs/Heartbeat.d.ts +46 -0
  9. package/dist/cjs/Heartbeat.d.ts.map +1 -0
  10. package/dist/cjs/Heartbeat.js +28 -0
  11. package/dist/cjs/Heartbeat.js.map +7 -0
  12. package/dist/cjs/Migrations.d.ts +42 -0
  13. package/dist/cjs/Migrations.d.ts.map +1 -0
  14. package/dist/cjs/Migrations.js +55 -0
  15. package/dist/cjs/Migrations.js.map +7 -0
  16. package/dist/cjs/Ownership.d.ts +275 -0
  17. package/dist/cjs/Ownership.d.ts.map +1 -0
  18. package/dist/cjs/Ownership.js +129 -0
  19. package/dist/cjs/Ownership.js.map +7 -0
  20. package/dist/cjs/RunStore.d.ts +571 -0
  21. package/dist/cjs/RunStore.d.ts.map +1 -0
  22. package/dist/cjs/RunStore.js +1079 -0
  23. package/dist/cjs/RunStore.js.map +7 -0
  24. package/dist/cjs/RunStoreMetrics.d.ts +151 -0
  25. package/dist/cjs/RunStoreMetrics.d.ts.map +1 -0
  26. package/dist/cjs/RunStoreMetrics.js +91 -0
  27. package/dist/cjs/RunStoreMetrics.js.map +7 -0
  28. package/dist/cjs/index.d.ts +47 -0
  29. package/dist/cjs/index.d.ts.map +1 -0
  30. package/dist/cjs/index.js +43 -0
  31. package/dist/cjs/index.js.map +7 -0
  32. package/dist/cjs/internal/Boundary.d.ts +80 -0
  33. package/dist/cjs/internal/Boundary.d.ts.map +1 -0
  34. package/dist/cjs/internal/Boundary.js +92 -0
  35. package/dist/cjs/internal/Boundary.js.map +7 -0
  36. package/dist/cjs/internal/Companion.d.ts +44 -0
  37. package/dist/cjs/internal/Companion.d.ts.map +1 -0
  38. package/dist/cjs/internal/Companion.js +50 -0
  39. package/dist/cjs/internal/Companion.js.map +7 -0
  40. package/dist/cjs/internal/SpanOutcome.d.ts +37 -0
  41. package/dist/cjs/internal/SpanOutcome.d.ts.map +1 -0
  42. package/dist/cjs/internal/SpanOutcome.js +62 -0
  43. package/dist/cjs/internal/SpanOutcome.js.map +7 -0
  44. package/dist/cjs/migrations/0001_initial.d.ts +15 -0
  45. package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
  46. package/dist/cjs/migrations/0001_initial.js +110 -0
  47. package/dist/cjs/migrations/0001_initial.js.map +7 -0
  48. package/dist/cjs/migrations/0002_lineage.d.ts +27 -0
  49. package/dist/cjs/migrations/0002_lineage.d.ts.map +1 -0
  50. package/dist/cjs/migrations/0002_lineage.js +43 -0
  51. package/dist/cjs/migrations/0002_lineage.js.map +7 -0
  52. package/dist/cjs/migrations/0003_execution_revisions.d.ts +15 -0
  53. package/dist/cjs/migrations/0003_execution_revisions.d.ts.map +1 -0
  54. package/dist/cjs/migrations/0003_execution_revisions.js +77 -0
  55. package/dist/cjs/migrations/0003_execution_revisions.js.map +7 -0
  56. package/dist/cjs/migrations/0004_waiting_request.d.ts +32 -0
  57. package/dist/cjs/migrations/0004_waiting_request.d.ts.map +1 -0
  58. package/dist/cjs/migrations/0004_waiting_request.js +42 -0
  59. package/dist/cjs/migrations/0004_waiting_request.js.map +7 -0
  60. package/dist/cjs/package.json +1 -0
  61. package/dist/cjs/test/TestRunStore.d.ts +17 -0
  62. package/dist/cjs/test/TestRunStore.d.ts.map +1 -0
  63. package/dist/cjs/test/TestRunStore.js +42 -0
  64. package/dist/cjs/test/TestRunStore.js.map +7 -0
  65. package/dist/esm/AttemptStore.d.ts +359 -0
  66. package/dist/esm/AttemptStore.d.ts.map +1 -0
  67. package/dist/esm/AttemptStore.js +835 -0
  68. package/dist/esm/AttemptStore.js.map +1 -0
  69. package/dist/esm/Heartbeat.d.ts +46 -0
  70. package/dist/esm/Heartbeat.d.ts.map +1 -0
  71. package/dist/esm/Heartbeat.js +46 -0
  72. package/dist/esm/Heartbeat.js.map +1 -0
  73. package/dist/esm/Migrations.d.ts +42 -0
  74. package/dist/esm/Migrations.d.ts.map +1 -0
  75. package/dist/esm/Migrations.js +56 -0
  76. package/dist/esm/Migrations.js.map +1 -0
  77. package/dist/esm/Ownership.d.ts +275 -0
  78. package/dist/esm/Ownership.d.ts.map +1 -0
  79. package/dist/esm/Ownership.js +299 -0
  80. package/dist/esm/Ownership.js.map +1 -0
  81. package/dist/esm/RunStore.d.ts +571 -0
  82. package/dist/esm/RunStore.d.ts.map +1 -0
  83. package/dist/esm/RunStore.js +1287 -0
  84. package/dist/esm/RunStore.js.map +1 -0
  85. package/dist/esm/RunStoreMetrics.d.ts +151 -0
  86. package/dist/esm/RunStoreMetrics.d.ts.map +1 -0
  87. package/dist/esm/RunStoreMetrics.js +120 -0
  88. package/dist/esm/RunStoreMetrics.js.map +1 -0
  89. package/dist/esm/index.d.ts +47 -0
  90. package/dist/esm/index.d.ts.map +1 -0
  91. package/dist/esm/index.js +47 -0
  92. package/dist/esm/index.js.map +1 -0
  93. package/dist/esm/internal/Boundary.d.ts +80 -0
  94. package/dist/esm/internal/Boundary.d.ts.map +1 -0
  95. package/dist/esm/internal/Boundary.js +109 -0
  96. package/dist/esm/internal/Boundary.js.map +1 -0
  97. package/dist/esm/internal/Companion.d.ts +44 -0
  98. package/dist/esm/internal/Companion.d.ts.map +1 -0
  99. package/dist/esm/internal/Companion.js +53 -0
  100. package/dist/esm/internal/Companion.js.map +1 -0
  101. package/dist/esm/internal/SpanOutcome.d.ts +37 -0
  102. package/dist/esm/internal/SpanOutcome.d.ts.map +1 -0
  103. package/dist/esm/internal/SpanOutcome.js +48 -0
  104. package/dist/esm/internal/SpanOutcome.js.map +1 -0
  105. package/dist/esm/migrations/0001_initial.d.ts +15 -0
  106. package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
  107. package/dist/esm/migrations/0001_initial.js +88 -0
  108. package/dist/esm/migrations/0001_initial.js.map +1 -0
  109. package/dist/esm/migrations/0002_lineage.d.ts +27 -0
  110. package/dist/esm/migrations/0002_lineage.d.ts.map +1 -0
  111. package/dist/esm/migrations/0002_lineage.js +33 -0
  112. package/dist/esm/migrations/0002_lineage.js.map +1 -0
  113. package/dist/esm/migrations/0003_execution_revisions.d.ts +15 -0
  114. package/dist/esm/migrations/0003_execution_revisions.d.ts.map +1 -0
  115. package/dist/esm/migrations/0003_execution_revisions.js +58 -0
  116. package/dist/esm/migrations/0003_execution_revisions.js.map +1 -0
  117. package/dist/esm/migrations/0004_waiting_request.d.ts +32 -0
  118. package/dist/esm/migrations/0004_waiting_request.d.ts.map +1 -0
  119. package/dist/esm/migrations/0004_waiting_request.js +37 -0
  120. package/dist/esm/migrations/0004_waiting_request.js.map +1 -0
  121. package/dist/esm/test/TestRunStore.d.ts +17 -0
  122. package/dist/esm/test/TestRunStore.d.ts.map +1 -0
  123. package/dist/esm/test/TestRunStore.js +19 -0
  124. package/dist/esm/test/TestRunStore.js.map +1 -0
  125. package/docs/README.md +142 -0
  126. package/docs/api.md +901 -0
  127. package/docs/concepts/durable-values.md +140 -0
  128. package/docs/concepts/execution-revisions.md +53 -0
  129. package/docs/concepts/fencing.md +146 -0
  130. package/docs/concepts/leases.md +154 -0
  131. package/docs/concepts/liveness-evidence.md +142 -0
  132. package/docs/guides/cancel-a-run.md +156 -0
  133. package/docs/guides/claim-and-finish-a-run.md +175 -0
  134. package/docs/guides/compose-the-stores.md +142 -0
  135. package/docs/guides/observe-outcomes.md +182 -0
  136. package/docs/guides/record-step-attempts.md +170 -0
  137. package/docs/guides/recover-a-stalled-run.md +173 -0
  138. package/docs/guides/testing.md +141 -0
  139. package/docs/installation.md +109 -0
  140. package/docs/quickstart.md +201 -0
  141. package/docs/troubleshooting.md +255 -0
  142. package/package.json +183 -3
  143. package/src/AttemptStore.ts +1195 -0
  144. package/src/Heartbeat.ts +47 -0
  145. package/src/Migrations.ts +59 -0
  146. package/src/Ownership.ts +407 -0
  147. package/src/RunStore.ts +2063 -0
  148. package/src/RunStoreMetrics.ts +165 -0
  149. package/src/index.ts +51 -0
  150. package/src/internal/Boundary.ts +130 -0
  151. package/src/internal/Companion.ts +64 -0
  152. package/src/internal/SpanOutcome.ts +71 -0
  153. package/src/migrations/0001_initial.ts +114 -0
  154. package/src/migrations/0002_lineage.ts +38 -0
  155. package/src/migrations/0003_execution_revisions.ts +69 -0
  156. package/src/migrations/0004_waiting_request.ts +39 -0
  157. package/src/test/TestRunStore.ts +22 -0
@@ -0,0 +1,731 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var AttemptStore_exports = {};
30
+ __export(AttemptStore_exports, {
31
+ Attempt: () => Attempt,
32
+ AttemptId: () => AttemptId,
33
+ AttemptPatch: () => AttemptPatch,
34
+ AttemptStore: () => AttemptStore,
35
+ AttemptStoreError: () => AttemptStoreError,
36
+ AttemptStoreErrorCode: () => AttemptStoreErrorCode,
37
+ FinishAttempt: () => FinishAttempt,
38
+ JsonValue: () => JsonValue,
39
+ layer: () => layer,
40
+ layerNoop: () => layerNoop,
41
+ layerWith: () => layerWith,
42
+ make: () => make,
43
+ makeNoop: () => makeNoop,
44
+ makeWith: () => makeWith,
45
+ maximumCheckpointBytes: () => maximumCheckpointBytes,
46
+ maximumJsonDepth: () => maximumJsonDepth,
47
+ maximumJsonNodes: () => maximumJsonNodes
48
+ });
49
+ module.exports = __toCommonJS(AttemptStore_exports);
50
+ var Dialect = __toESM(require("@smthrs/database/Dialect"), 1);
51
+ var import_DurableWriter = require("@smthrs/database/DurableWriter");
52
+ var Context = __toESM(require("effect/Context"), 1);
53
+ var Effect = __toESM(require("effect/Effect"), 1);
54
+ var Layer = __toESM(require("effect/Layer"), 1);
55
+ var Option = __toESM(require("effect/Option"), 1);
56
+ var Schema = __toESM(require("effect/Schema"), 1);
57
+ var SqlClient = __toESM(require("effect/unstable/sql/SqlClient"), 1);
58
+ var SqlError = __toESM(require("effect/unstable/sql/SqlError"), 1);
59
+ var Boundary = __toESM(require("./internal/Boundary.js"), 1);
60
+ var import_SpanOutcome = require("./internal/SpanOutcome.js");
61
+ const NonNegativeSafeInt = Schema.Int.check(
62
+ Schema.isGreaterThanOrEqualTo(0),
63
+ Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER)
64
+ );
65
+ const durableIdentifier = Schema.NonEmptyString.check(
66
+ Schema.isMaxLength(Boundary.maximumIdentifierLength),
67
+ Schema.makeFilter((value) => Boundary.isDurableText(value), { title: "durableIdentifier" })
68
+ );
69
+ const maximumCheckpointBytes = 16 * 1024 * 1024;
70
+ const maximumJsonDepth = 128;
71
+ const maximumJsonNodes = 1e5;
72
+ const jsonLimits = (maxBytes) => ({
73
+ ...maxBytes === void 0 ? {} : { maxBytes, maxStringBytes: maxBytes },
74
+ maxDepth: maximumJsonDepth,
75
+ maxMembers: maximumJsonNodes,
76
+ maxNodes: maximumJsonNodes
77
+ });
78
+ const JsonValue = Schema.declare(
79
+ (value) => Boundary.admitJson(value, jsonLimits()).ok,
80
+ { identifier: "@smthrs/run-store/JsonValue" }
81
+ );
82
+ const AttemptStoreErrorCode = Schema.Literals([
83
+ "invalid_attempt",
84
+ "constraint",
85
+ "decode_failed",
86
+ "persistence_failed",
87
+ "unknown"
88
+ ]);
89
+ class AttemptStoreError extends Schema.TaggedError()(
90
+ "@smthrs/run-store/AttemptStoreError",
91
+ {
92
+ code: AttemptStoreErrorCode,
93
+ method: Schema.String,
94
+ message: Schema.String,
95
+ cause: Schema.optional(Schema.Unknown)
96
+ }
97
+ ) {
98
+ }
99
+ const AttemptId = Schema.Struct({
100
+ runId: durableIdentifier,
101
+ stepKeyDigest: durableIdentifier,
102
+ attempt: NonNegativeSafeInt
103
+ });
104
+ const Attempt = Schema.Struct({
105
+ ...AttemptId.fields,
106
+ state: durableIdentifier,
107
+ startedAtMs: NonNegativeSafeInt,
108
+ finishedAtMs: Schema.optionalKey(NonNegativeSafeInt),
109
+ heartbeatAtMs: Schema.optionalKey(NonNegativeSafeInt),
110
+ checkpoint: Schema.optionalKey(JsonValue),
111
+ error: Schema.optionalKey(JsonValue),
112
+ outcome: Schema.optionalKey(JsonValue),
113
+ meta: JsonValue
114
+ });
115
+ const FinishAttempt = Schema.Struct({
116
+ ...AttemptId.fields,
117
+ state: durableIdentifier,
118
+ finishedAtMs: NonNegativeSafeInt,
119
+ error: Schema.optionalKey(JsonValue),
120
+ outcome: Schema.optionalKey(JsonValue),
121
+ meta: Schema.optionalKey(JsonValue)
122
+ });
123
+ const AttemptPatch = Schema.Struct({
124
+ checkpoint: Schema.optionalKey(JsonValue),
125
+ error: Schema.optionalKey(JsonValue),
126
+ outcome: Schema.optionalKey(JsonValue),
127
+ meta: Schema.optionalKey(JsonValue)
128
+ });
129
+ class AttemptStore extends Context.Service()("@smthrs/run-store/AttemptStore") {
130
+ }
131
+ const AttemptRow = Schema.Struct({
132
+ run_id: durableIdentifier,
133
+ step_key_digest: durableIdentifier,
134
+ attempt: NonNegativeSafeInt,
135
+ state: durableIdentifier,
136
+ started_at_ms: NonNegativeSafeInt,
137
+ finished_at_ms: Schema.NullOr(NonNegativeSafeInt),
138
+ heartbeat_at_ms: Schema.NullOr(NonNegativeSafeInt),
139
+ checkpoint_json: Schema.NullOr(Schema.String),
140
+ error_json: Schema.NullOr(Schema.String),
141
+ outcome_json: Schema.NullOr(Schema.String),
142
+ meta_json: Schema.String
143
+ });
144
+ const attemptStoreError = (method, code, detail, cause) => new AttemptStoreError({
145
+ code,
146
+ method,
147
+ message: `${code}: AttemptStore.${method}: ${detail}`,
148
+ cause
149
+ });
150
+ const error = (method, code, detail) => attemptStoreError(method, code, detail);
151
+ const encode = (method, value, field, maxBytes) => Effect.suspend(() => {
152
+ const admitted = Boundary.admitJson(value, jsonLimits(maxBytes));
153
+ return admitted.ok ? Effect.succeed(JSON.stringify(admitted.value)) : Effect.fail(error(method, "invalid_attempt", `${field} ${admitted.complaint}`));
154
+ });
155
+ const encodeOptionalWith = (method) => (value, field) => value === void 0 ? Effect.succeed(null) : encode(method, value, field);
156
+ const defaultMaxCheckpointBytes = 1024 * 1024;
157
+ const defaultInProgressStates = ["running"];
158
+ const encodeCheckpointWith = (method, maxBytes) => (value) => value === void 0 ? Effect.succeed(null) : encode(method, value, "checkpoint", maxBytes);
159
+ const decode = (method, value, field, maxBytes) => Effect.suspend(() => {
160
+ if (value === null) return Effect.succeed(void 0);
161
+ const admitted = Boundary.admitJsonText(value, jsonLimits(maxBytes));
162
+ return admitted.ok ? Effect.succeed(admitted.json) : Effect.fail(error(method, "decode_failed", `${field} ${admitted.complaint}`));
163
+ });
164
+ const decodeRequired = (method, value, field, maxBytes) => (
165
+ // A non-null SQL column cannot take `decode`'s missing-value branch.
166
+ decode(method, value, field, maxBytes)
167
+ );
168
+ const validateId = (method, id) => Boundary.isDurableText(id.runId) && Boundary.isDurableText(id.stepKeyDigest) && Number.isSafeInteger(id.attempt) && id.attempt >= 0 ? Effect.void : Effect.fail(
169
+ error(method, "invalid_attempt", "attempt identity violates the durable identifier contract")
170
+ );
171
+ const inspectRecord = (input, required, optional, field) => {
172
+ if (typeof input !== "object" || input === null) throw new TypeError(field);
173
+ const prototype = Object.getPrototypeOf(input);
174
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(field);
175
+ const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
176
+ const output = /* @__PURE__ */ Object.create(null);
177
+ for (const name of required) {
178
+ const descriptor = Object.getOwnPropertyDescriptor(input, name);
179
+ if (descriptor === void 0 || !("value" in descriptor) || !descriptor.enumerable) throw new TypeError(field);
180
+ output[name] = descriptor.value;
181
+ }
182
+ for (const name of optional) {
183
+ const descriptor = Object.getOwnPropertyDescriptor(input, name);
184
+ if (descriptor === void 0) continue;
185
+ if (!("value" in descriptor) || !descriptor.enumerable) throw new TypeError(field);
186
+ if (descriptor.value !== void 0) output[name] = descriptor.value;
187
+ }
188
+ for (const key of Reflect.ownKeys(input)) {
189
+ if (typeof key !== "string" || !allowed.has(key)) {
190
+ if (Object.getOwnPropertyDescriptor(input, key)?.enumerable) throw new TypeError(field);
191
+ }
192
+ }
193
+ return output;
194
+ };
195
+ const snapshotOwner = (method, input) => Effect.suspend(() => {
196
+ try {
197
+ const values = inspectRecord(input, ["hostId", "pid", "nonce"], [], "owner");
198
+ const owner = Object.freeze({ hostId: values.hostId, pid: values.pid, nonce: values.nonce });
199
+ if (Boundary.isDurableText(owner.hostId) && Boundary.isDurableText(owner.nonce) && Number.isSafeInteger(owner.pid) && owner.pid >= 0) {
200
+ return Effect.succeed(owner);
201
+ }
202
+ } catch {
203
+ }
204
+ return Effect.fail(error(method, "invalid_attempt", "owner must be an inert, valid owner identity"));
205
+ });
206
+ const snapshotJson = (method, value, field, maxBytes) => {
207
+ const admitted = Boundary.admitJson(value, jsonLimits(maxBytes));
208
+ return admitted.ok ? { ok: true, value: admitted.value } : { ok: false, error: error(method, "invalid_attempt", `${field} ${admitted.complaint}`) };
209
+ };
210
+ const inspectIdRecord = (input, field) => {
211
+ if (typeof input !== "object" || input === null) throw new TypeError(field);
212
+ const prototype = Object.getPrototypeOf(input);
213
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(field);
214
+ for (const key of Reflect.ownKeys(input)) {
215
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
216
+ if (descriptor !== void 0 && descriptor.enumerable && !("value" in descriptor)) throw new TypeError(field);
217
+ }
218
+ const output = /* @__PURE__ */ Object.create(null);
219
+ for (const name of ["runId", "stepKeyDigest", "attempt"]) {
220
+ const descriptor = Object.getOwnPropertyDescriptor(input, name);
221
+ if (descriptor === void 0 || !("value" in descriptor) || !descriptor.enumerable) throw new TypeError(field);
222
+ output[name] = descriptor.value;
223
+ }
224
+ return output;
225
+ };
226
+ const snapshotId = (method, input) => Effect.suspend(() => {
227
+ try {
228
+ const values = inspectIdRecord(input, "attempt id");
229
+ const id = Object.freeze({
230
+ runId: values.runId,
231
+ stepKeyDigest: values.stepKeyDigest,
232
+ attempt: values.attempt
233
+ });
234
+ return Effect.as(validateId(method, id), id);
235
+ } catch {
236
+ return Effect.fail(error(method, "invalid_attempt", "attempt id must be an inert data record"));
237
+ }
238
+ });
239
+ const snapshotAttempt = (method, input) => Effect.suspend(() => {
240
+ try {
241
+ const values = inspectRecord(
242
+ input,
243
+ ["runId", "stepKeyDigest", "attempt", "state", "startedAtMs", "meta"],
244
+ ["finishedAtMs", "heartbeatAtMs", "checkpoint", "error", "outcome"],
245
+ "attempt"
246
+ );
247
+ const checkpoint = values.checkpoint === void 0 ? void 0 : snapshotJson(method, values.checkpoint, "checkpoint", maximumCheckpointBytes);
248
+ const attemptError = values.error === void 0 ? void 0 : snapshotJson(method, values.error, "error");
249
+ const outcome = values.outcome === void 0 ? void 0 : snapshotJson(method, values.outcome, "outcome");
250
+ const meta = snapshotJson(method, values.meta, "meta");
251
+ if (checkpoint !== void 0 && !checkpoint.ok) return Effect.fail(checkpoint.error);
252
+ if (attemptError !== void 0 && !attemptError.ok) return Effect.fail(attemptError.error);
253
+ if (outcome !== void 0 && !outcome.ok) return Effect.fail(outcome.error);
254
+ if (!meta.ok) return Effect.fail(meta.error);
255
+ const candidate = Object.freeze({
256
+ runId: values.runId,
257
+ stepKeyDigest: values.stepKeyDigest,
258
+ attempt: values.attempt,
259
+ state: values.state,
260
+ startedAtMs: values.startedAtMs,
261
+ ...values.finishedAtMs === void 0 ? {} : { finishedAtMs: values.finishedAtMs },
262
+ ...values.heartbeatAtMs === void 0 ? {} : { heartbeatAtMs: values.heartbeatAtMs },
263
+ ...checkpoint === void 0 ? {} : { checkpoint: checkpoint.value },
264
+ ...attemptError === void 0 ? {} : { error: attemptError.value },
265
+ ...outcome === void 0 ? {} : { outcome: outcome.value },
266
+ meta: meta.value
267
+ });
268
+ return Schema.decodeUnknownEffect(Attempt)(candidate).pipe(
269
+ Effect.mapError(() => error(method, "invalid_attempt", "attempt violates the persistence contract"))
270
+ );
271
+ } catch {
272
+ return Effect.fail(error(method, "invalid_attempt", "attempt must be an inert data record"));
273
+ }
274
+ });
275
+ const snapshotFinish = (method, input) => Effect.suspend(() => {
276
+ try {
277
+ const values = inspectRecord(
278
+ input,
279
+ ["runId", "stepKeyDigest", "attempt", "state", "finishedAtMs"],
280
+ ["error", "outcome", "meta"],
281
+ "finished attempt"
282
+ );
283
+ const attemptError = values.error === void 0 ? void 0 : snapshotJson(method, values.error, "error");
284
+ const outcome = values.outcome === void 0 ? void 0 : snapshotJson(method, values.outcome, "outcome");
285
+ const meta = values.meta === void 0 ? void 0 : snapshotJson(method, values.meta, "meta");
286
+ if (attemptError !== void 0 && !attemptError.ok) return Effect.fail(attemptError.error);
287
+ if (outcome !== void 0 && !outcome.ok) return Effect.fail(outcome.error);
288
+ if (meta !== void 0 && !meta.ok) return Effect.fail(meta.error);
289
+ return Schema.decodeUnknownEffect(FinishAttempt)(Object.freeze({
290
+ runId: values.runId,
291
+ stepKeyDigest: values.stepKeyDigest,
292
+ attempt: values.attempt,
293
+ state: values.state,
294
+ finishedAtMs: values.finishedAtMs,
295
+ ...attemptError === void 0 ? {} : { error: attemptError.value },
296
+ ...outcome === void 0 ? {} : { outcome: outcome.value },
297
+ ...meta === void 0 ? {} : { meta: meta.value }
298
+ })).pipe(Effect.mapError(() => error(method, "invalid_attempt", "finished attempt violates the contract")));
299
+ } catch {
300
+ return Effect.fail(error(method, "invalid_attempt", "finished attempt must be an inert data record"));
301
+ }
302
+ });
303
+ const snapshotPatch = (method, input) => Effect.suspend(() => {
304
+ try {
305
+ const values = inspectRecord(input, [], ["checkpoint", "error", "outcome", "meta"], "attempt patch");
306
+ const checkpoint = values.checkpoint === void 0 ? void 0 : snapshotJson(method, values.checkpoint, "checkpoint", maximumCheckpointBytes);
307
+ const attemptError = values.error === void 0 ? void 0 : snapshotJson(method, values.error, "error");
308
+ const outcome = values.outcome === void 0 ? void 0 : snapshotJson(method, values.outcome, "outcome");
309
+ const meta = values.meta === void 0 ? void 0 : snapshotJson(method, values.meta, "meta");
310
+ if (checkpoint !== void 0 && !checkpoint.ok) return Effect.fail(checkpoint.error);
311
+ if (attemptError !== void 0 && !attemptError.ok) return Effect.fail(attemptError.error);
312
+ if (outcome !== void 0 && !outcome.ok) return Effect.fail(outcome.error);
313
+ if (meta !== void 0 && !meta.ok) return Effect.fail(meta.error);
314
+ return Effect.succeed(Object.freeze({
315
+ ...checkpoint === void 0 ? {} : { checkpoint: checkpoint.value },
316
+ ...attemptError === void 0 ? {} : { error: attemptError.value },
317
+ ...outcome === void 0 ? {} : { outcome: outcome.value },
318
+ ...meta === void 0 ? {} : { meta: meta.value }
319
+ }));
320
+ } catch {
321
+ return Effect.fail(error(method, "invalid_attempt", "attempt patch must be an inert data record"));
322
+ }
323
+ });
324
+ const snapshotOptions = (method, input) => Effect.suspend(() => {
325
+ try {
326
+ const values = inspectRecord(
327
+ input,
328
+ [],
329
+ ["inProgressStates", "maxCheckpointBytes", "putMode"],
330
+ "attempt store options"
331
+ );
332
+ const rawStates = values.inProgressStates ?? defaultInProgressStates;
333
+ const admitted = Boundary.admitJson(rawStates, {
334
+ ...jsonLimits(64 * 1024),
335
+ maxDepth: 2,
336
+ maxMembers: 256,
337
+ maxNodes: 257
338
+ });
339
+ if (!admitted.ok || !Array.isArray(admitted.value)) {
340
+ return Effect.fail(error(method, "invalid_attempt", "inProgressStates must be an inert string array"));
341
+ }
342
+ const states = admitted.value;
343
+ if (states.length === 0 || states.some((state) => !Boundary.isDurableText(state)) || new Set(states).size !== states.length) {
344
+ return Effect.fail(
345
+ error(method, "invalid_attempt", "inProgressStates must contain unique durable state names")
346
+ );
347
+ }
348
+ const maxCheckpointBytes = values.maxCheckpointBytes ?? defaultMaxCheckpointBytes;
349
+ if (typeof maxCheckpointBytes !== "number" || !Number.isSafeInteger(maxCheckpointBytes) || maxCheckpointBytes <= 0 || maxCheckpointBytes > maximumCheckpointBytes) {
350
+ return Effect.fail(
351
+ error(method, "invalid_attempt", `maxCheckpointBytes must be between 1 and ${maximumCheckpointBytes}`)
352
+ );
353
+ }
354
+ const putMode = values.putMode ?? "insert";
355
+ if (putMode !== "insert" && putMode !== "upsert") {
356
+ return Effect.fail(error(method, "invalid_attempt", "putMode must be insert or upsert"));
357
+ }
358
+ return Effect.succeed(Object.freeze({
359
+ inProgressStates: Object.freeze([...states]),
360
+ maxCheckpointBytes,
361
+ putMode
362
+ }));
363
+ } catch {
364
+ return Effect.fail(error(method, "invalid_attempt", "attempt store options must be an inert data record"));
365
+ }
366
+ });
367
+ const ownsRunningRun = (row, owner) => row.status === "running" && row.owner_host_id === owner.hostId && row.owner_pid === owner.pid && row.owner_nonce === owner.nonce;
368
+ const sameAttempt = (stored, expected) => stored.state === expected.state && stored.startedAtMs === expected.startedAtMs && stored.finishedAtMs === expected.finishedAtMs && stored.heartbeatAtMs === expected.heartbeatAtMs && Boundary.sameJson(stored.checkpoint, expected.checkpoint) && Boundary.sameJson(stored.error, expected.error) && Boundary.sameJson(stored.outcome, expected.outcome) && Boundary.sameJson(stored.meta, expected.meta);
369
+ const mapPersistenceError = (method) => (cause) => {
370
+ if (Schema.is(AttemptStoreError)(cause)) {
371
+ return cause;
372
+ }
373
+ const sqlError = SqlError.isSqlError(cause);
374
+ const databaseError = Schema.is(import_DurableWriter.DatabaseError)(cause);
375
+ const constraint = sqlError ? cause.reason instanceof SqlError.ConstraintError || cause.reason instanceof SqlError.UniqueViolation : databaseError && cause.code === "constraint";
376
+ const code = constraint ? "constraint" : "persistence_failed";
377
+ return attemptStoreError(
378
+ method,
379
+ code,
380
+ "attempt persistence failed",
381
+ {
382
+ category: code,
383
+ reason: sqlError ? cause.reason._tag : databaseError ? cause.code : "unknown",
384
+ // The outer writer must still recognize a nested savepoint's conflict.
385
+ ...sqlError || databaseError ? { cause: new import_DurableWriter.DatabaseError({ code: sqlError ? (0, import_DurableWriter.fromSqlError)(cause).code : cause.code }) } : {}
386
+ }
387
+ );
388
+ };
389
+ const decodeRow = (method, input) => Schema.decodeUnknownEffect(AttemptRow)(input).pipe(
390
+ Effect.mapError(() => error(method, "decode_failed", "could not decode flows_attempts row")),
391
+ Effect.flatMap(
392
+ (row) => Effect.all({
393
+ checkpoint: decode(method, row.checkpoint_json, "checkpoint_json", maximumCheckpointBytes),
394
+ error: decode(method, row.error_json, "error_json"),
395
+ outcome: decode(method, row.outcome_json, "outcome_json"),
396
+ meta: decodeRequired(method, row.meta_json, "meta_json")
397
+ }).pipe(
398
+ Effect.map(
399
+ ({ checkpoint, error: attemptError, outcome, meta }) => Object.freeze({
400
+ runId: row.run_id,
401
+ stepKeyDigest: row.step_key_digest,
402
+ attempt: row.attempt,
403
+ state: row.state,
404
+ startedAtMs: row.started_at_ms,
405
+ ...row.finished_at_ms === null ? {} : { finishedAtMs: row.finished_at_ms },
406
+ ...row.heartbeat_at_ms === null ? {} : { heartbeatAtMs: row.heartbeat_at_ms },
407
+ ...checkpoint === void 0 ? {} : { checkpoint },
408
+ ...attemptError === void 0 ? {} : { error: attemptError },
409
+ ...outcome === void 0 ? {} : { outcome },
410
+ meta
411
+ })
412
+ )
413
+ )
414
+ )
415
+ );
416
+ const makeWith = (options = {}) => Effect.gen(function* () {
417
+ const sql = yield* Effect.service(SqlClient.SqlClient);
418
+ const writer = yield* import_DurableWriter.DurableWriter;
419
+ const configured = yield* snapshotOptions("makeWith", options);
420
+ const inProgressStates = configured.inProgressStates;
421
+ const maxCheckpointBytes = configured.maxCheckpointBytes;
422
+ const upsert = configured.putMode === "upsert";
423
+ const putEncodeOptional = encodeOptionalWith("put");
424
+ const putEncodeCheckpoint = encodeCheckpointWith("put", maxCheckpointBytes);
425
+ const heartbeatEncodeCheckpoint = encodeCheckpointWith("heartbeat", maxCheckpointBytes);
426
+ const finishEncodeOptional = encodeOptionalWith("finish");
427
+ const patchEncodeOptional = encodeOptionalWith("patch");
428
+ const patchEncodeCheckpoint = encodeCheckpointWith("patch", maxCheckpointBytes);
429
+ const inProgress = sql.in("state", [...inProgressStates]);
430
+ const put = Effect.fn("AttemptStore.put")(
431
+ (input, ownerInput) => Effect.gen(function* () {
432
+ const owner = yield* snapshotOwner("put", ownerInput);
433
+ const attempt = yield* snapshotAttempt("put", input);
434
+ yield* Effect.annotateCurrentSpan({
435
+ runId: attempt.runId,
436
+ stepKeyDigest: attempt.stepKeyDigest,
437
+ attempt: attempt.attempt,
438
+ ownerHostId: owner.hostId
439
+ });
440
+ const checkpoint = yield* putEncodeCheckpoint(attempt.checkpoint);
441
+ const attemptError = yield* putEncodeOptional(attempt.error, "error");
442
+ const outcome = yield* putEncodeOptional(attempt.outcome, "outcome");
443
+ const meta = yield* encode("put", attempt.meta, "meta");
444
+ return yield* writer.write(
445
+ Effect.gen(function* () {
446
+ const inserted = yield* sql`
447
+ INSERT INTO flows_attempts (
448
+ run_id, step_key_digest, attempt, state, started_at_ms, finished_at_ms,
449
+ heartbeat_at_ms, checkpoint_json, error_json, outcome_json, meta_json
450
+ )
451
+ SELECT
452
+ ${attempt.runId}, ${attempt.stepKeyDigest}, ${attempt.attempt}, ${attempt.state},
453
+ ${attempt.startedAtMs}, ${attempt.finishedAtMs ?? null}, ${attempt.heartbeatAtMs ?? null},
454
+ ${checkpoint}, ${attemptError}, ${outcome}, ${meta}
455
+ WHERE EXISTS (
456
+ SELECT 1 FROM flows_runs
457
+ WHERE run_id = ${attempt.runId}
458
+ AND status = 'running'
459
+ AND owner_host_id = ${owner.hostId}
460
+ AND owner_pid = ${owner.pid}
461
+ AND owner_nonce = ${owner.nonce}
462
+ )
463
+ ON CONFLICT (run_id, step_key_digest, attempt) DO NOTHING
464
+ RETURNING attempt
465
+ `;
466
+ if (inserted.length > 0) {
467
+ return { _tag: "Inserted" };
468
+ }
469
+ if (upsert) {
470
+ const replaced = yield* sql`
471
+ UPDATE flows_attempts
472
+ SET
473
+ state = ${attempt.state},
474
+ started_at_ms = ${attempt.startedAtMs},
475
+ finished_at_ms = ${attempt.finishedAtMs ?? null},
476
+ heartbeat_at_ms = ${attempt.heartbeatAtMs ?? null},
477
+ checkpoint_json = ${checkpoint},
478
+ error_json = ${attemptError},
479
+ outcome_json = ${outcome},
480
+ meta_json = ${meta}
481
+ WHERE run_id = ${attempt.runId}
482
+ AND step_key_digest = ${attempt.stepKeyDigest}
483
+ AND attempt = ${attempt.attempt}
484
+ AND ${inProgress}
485
+ AND EXISTS (
486
+ SELECT 1 FROM flows_runs
487
+ WHERE run_id = ${attempt.runId}
488
+ AND status = 'running'
489
+ AND owner_host_id = ${owner.hostId}
490
+ AND owner_pid = ${owner.pid}
491
+ AND owner_nonce = ${owner.nonce}
492
+ )
493
+ RETURNING attempt
494
+ `;
495
+ if (replaced.length > 0) {
496
+ return { _tag: "Upserted" };
497
+ }
498
+ }
499
+ const runRows = yield* sql`
500
+ SELECT status, owner_host_id, owner_pid, owner_nonce
501
+ FROM flows_runs WHERE run_id = ${attempt.runId}
502
+ `;
503
+ if (runRows.length === 0) {
504
+ return { _tag: "RunNotFound" };
505
+ }
506
+ if (!ownsRunningRun(runRows[0], owner)) {
507
+ return { _tag: "FenceLost" };
508
+ }
509
+ const rows = yield* sql`
510
+ SELECT run_id, step_key_digest, attempt, state, started_at_ms, finished_at_ms,
511
+ heartbeat_at_ms, checkpoint_json, error_json, outcome_json, meta_json
512
+ FROM flows_attempts
513
+ WHERE run_id = ${attempt.runId}
514
+ AND step_key_digest = ${attempt.stepKeyDigest}
515
+ AND attempt = ${attempt.attempt}
516
+ `;
517
+ if (rows.length === 0) {
518
+ return yield* Effect.fail(error("put", "unknown", "attempt disappeared during put"));
519
+ }
520
+ const stored = yield* decodeRow("put", rows[0]);
521
+ return sameAttempt(stored, attempt) ? { _tag: "ExistingSame" } : { _tag: "Conflict" };
522
+ })
523
+ ).pipe(Effect.mapError(mapPersistenceError("put")));
524
+ }).pipe((0, import_SpanOutcome.observeOutcome)())
525
+ );
526
+ const get = Effect.fn("AttemptStore.get")(
527
+ (input) => Effect.gen(function* () {
528
+ const id = yield* snapshotId("get", input);
529
+ yield* Effect.annotateCurrentSpan({
530
+ runId: id.runId,
531
+ stepKeyDigest: id.stepKeyDigest,
532
+ attempt: id.attempt
533
+ });
534
+ const rows = yield* sql`
535
+ SELECT run_id, step_key_digest, attempt, state, started_at_ms, finished_at_ms,
536
+ heartbeat_at_ms, checkpoint_json, error_json, outcome_json, meta_json
537
+ FROM flows_attempts
538
+ WHERE run_id = ${id.runId} AND step_key_digest = ${id.stepKeyDigest} AND attempt = ${id.attempt}
539
+ `.pipe(Effect.mapError(mapPersistenceError("get")));
540
+ return rows.length === 0 ? Option.none() : yield* Effect.map(decodeRow("get", rows[0]), Option.some);
541
+ }).pipe(import_SpanOutcome.observeExit)
542
+ );
543
+ const heartbeat = Effect.fn("AttemptStore.heartbeat")(
544
+ (runId, stepKeyDigest, attempt, ownerInput, nowMs, checkpointValue) => Effect.gen(function* () {
545
+ const owner = yield* snapshotOwner("heartbeat", ownerInput);
546
+ yield* validateId("heartbeat", { runId, stepKeyDigest, attempt });
547
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
548
+ return yield* Effect.fail(
549
+ error("heartbeat", "invalid_attempt", "nowMs must be a non-negative safe integer")
550
+ );
551
+ }
552
+ yield* Effect.annotateCurrentSpan({ runId, stepKeyDigest, attempt, ownerHostId: owner.hostId });
553
+ const checkpoint = yield* heartbeatEncodeCheckpoint(checkpointValue);
554
+ return yield* writer.write(
555
+ Effect.gen(function* () {
556
+ const updated = yield* sql`
557
+ UPDATE flows_attempts
558
+ SET
559
+ heartbeat_at_ms = ${Dialect.greatest(sql)}(COALESCE(heartbeat_at_ms, ${nowMs}), ${nowMs}),
560
+ checkpoint_json = COALESCE(${checkpoint}, checkpoint_json)
561
+ WHERE run_id = ${runId}
562
+ AND step_key_digest = ${stepKeyDigest}
563
+ AND attempt = ${attempt}
564
+ AND ${inProgress}
565
+ AND EXISTS (
566
+ SELECT 1 FROM flows_runs
567
+ WHERE run_id = ${runId}
568
+ AND status = 'running'
569
+ AND owner_host_id = ${owner.hostId}
570
+ AND owner_pid = ${owner.pid}
571
+ AND owner_nonce = ${owner.nonce}
572
+ )
573
+ RETURNING attempt
574
+ `;
575
+ if (updated.length > 0) {
576
+ return { _tag: "Updated" };
577
+ }
578
+ const found = yield* sql`
579
+ SELECT state FROM flows_attempts
580
+ WHERE run_id = ${runId} AND step_key_digest = ${stepKeyDigest} AND attempt = ${attempt}
581
+ `;
582
+ if (found.length === 0) {
583
+ return { _tag: "NotFound" };
584
+ }
585
+ const runRows = yield* sql`
586
+ SELECT status, owner_host_id, owner_pid, owner_nonce
587
+ FROM flows_runs WHERE run_id = ${runId}
588
+ `;
589
+ if (runRows.length === 0 || !ownsRunningRun(runRows[0], owner)) {
590
+ return { _tag: "FenceLost" };
591
+ }
592
+ return { _tag: "StateChanged" };
593
+ })
594
+ ).pipe(Effect.mapError(mapPersistenceError("heartbeat")));
595
+ }).pipe((0, import_SpanOutcome.observeOutcome)())
596
+ );
597
+ const finish = Effect.fn("AttemptStore.finish")(
598
+ (input, ownerInput) => Effect.gen(function* () {
599
+ const owner = yield* snapshotOwner("finish", ownerInput);
600
+ const attempt = yield* snapshotFinish("finish", input);
601
+ yield* Effect.annotateCurrentSpan({
602
+ runId: attempt.runId,
603
+ stepKeyDigest: attempt.stepKeyDigest,
604
+ attempt: attempt.attempt,
605
+ ownerHostId: owner.hostId
606
+ });
607
+ if (inProgressStates.includes(attempt.state) || !Boundary.isDurableText(attempt.state)) {
608
+ return yield* Effect.fail(
609
+ error("finish", "invalid_attempt", "finish requires a terminal state and valid timestamp")
610
+ );
611
+ }
612
+ const attemptError = yield* finishEncodeOptional(attempt.error, "error");
613
+ const outcome = yield* finishEncodeOptional(attempt.outcome, "outcome");
614
+ const meta = yield* finishEncodeOptional(attempt.meta, "meta");
615
+ return yield* writer.write(
616
+ Effect.gen(function* () {
617
+ const updated = yield* sql`
618
+ UPDATE flows_attempts
619
+ SET
620
+ state = ${attempt.state},
621
+ finished_at_ms = ${attempt.finishedAtMs},
622
+ error_json = COALESCE(${attemptError}, error_json),
623
+ outcome_json = COALESCE(${outcome}, outcome_json),
624
+ meta_json = COALESCE(${meta}, meta_json)
625
+ WHERE run_id = ${attempt.runId}
626
+ AND step_key_digest = ${attempt.stepKeyDigest}
627
+ AND attempt = ${attempt.attempt}
628
+ AND ${inProgress}
629
+ AND EXISTS (
630
+ SELECT 1 FROM flows_runs
631
+ WHERE run_id = ${attempt.runId}
632
+ AND status = 'running'
633
+ AND owner_host_id = ${owner.hostId}
634
+ AND owner_pid = ${owner.pid}
635
+ AND owner_nonce = ${owner.nonce}
636
+ )
637
+ RETURNING attempt
638
+ `;
639
+ if (updated.length > 0) {
640
+ return { _tag: "Finished" };
641
+ }
642
+ const found = yield* sql`
643
+ SELECT state FROM flows_attempts
644
+ WHERE run_id = ${attempt.runId}
645
+ AND step_key_digest = ${attempt.stepKeyDigest}
646
+ AND attempt = ${attempt.attempt}
647
+ `;
648
+ if (found.length === 0) {
649
+ return { _tag: "NotFound" };
650
+ }
651
+ const runRows = yield* sql`
652
+ SELECT status, owner_host_id, owner_pid, owner_nonce
653
+ FROM flows_runs WHERE run_id = ${attempt.runId}
654
+ `;
655
+ if (runRows.length === 0 || !ownsRunningRun(runRows[0], owner)) {
656
+ return { _tag: "FenceLost" };
657
+ }
658
+ return { _tag: "StateChanged" };
659
+ })
660
+ ).pipe(Effect.mapError(mapPersistenceError("finish")));
661
+ }).pipe((0, import_SpanOutcome.observeOutcome)())
662
+ );
663
+ const patch = Effect.fn("AttemptStore.patch")(
664
+ (idInput, patchInput, ownerInput) => Effect.gen(function* () {
665
+ const owner = yield* snapshotOwner("patch", ownerInput);
666
+ const id = yield* snapshotId("patch", idInput);
667
+ const fields = yield* snapshotPatch("patch", patchInput);
668
+ yield* Effect.annotateCurrentSpan({
669
+ runId: id.runId,
670
+ stepKeyDigest: id.stepKeyDigest,
671
+ attempt: id.attempt,
672
+ ownerHostId: owner.hostId
673
+ });
674
+ const checkpoint = yield* patchEncodeCheckpoint(fields.checkpoint);
675
+ const attemptError = yield* patchEncodeOptional(fields.error, "error");
676
+ const outcome = yield* patchEncodeOptional(fields.outcome, "outcome");
677
+ const meta = yield* patchEncodeOptional(fields.meta, "meta");
678
+ return yield* writer.write(
679
+ Effect.gen(function* () {
680
+ const updated = yield* sql`
681
+ UPDATE flows_attempts
682
+ SET
683
+ checkpoint_json = COALESCE(${checkpoint}, checkpoint_json),
684
+ error_json = COALESCE(${attemptError}, error_json),
685
+ outcome_json = COALESCE(${outcome}, outcome_json),
686
+ meta_json = COALESCE(${meta}, meta_json)
687
+ WHERE run_id = ${id.runId}
688
+ AND step_key_digest = ${id.stepKeyDigest}
689
+ AND attempt = ${id.attempt}
690
+ AND EXISTS (
691
+ SELECT 1 FROM flows_runs
692
+ WHERE run_id = ${id.runId}
693
+ AND status = 'running'
694
+ AND owner_host_id = ${owner.hostId}
695
+ AND owner_pid = ${owner.pid}
696
+ AND owner_nonce = ${owner.nonce}
697
+ )
698
+ RETURNING attempt
699
+ `;
700
+ if (updated.length > 0) {
701
+ return { _tag: "Patched" };
702
+ }
703
+ const found = yield* sql`
704
+ SELECT state FROM flows_attempts
705
+ WHERE run_id = ${id.runId} AND step_key_digest = ${id.stepKeyDigest} AND attempt = ${id.attempt}
706
+ `;
707
+ return found.length === 0 ? { _tag: "NotFound" } : { _tag: "FenceLost" };
708
+ })
709
+ ).pipe(Effect.mapError(mapPersistenceError("patch")));
710
+ }).pipe((0, import_SpanOutcome.observeOutcome)())
711
+ );
712
+ return { put, get, heartbeat, finish, patch };
713
+ });
714
+ const make = Effect.orDie(makeWith());
715
+ const makeNoop = (overrides = {}) => {
716
+ const unavailable = (method) => Effect.fail(attemptStoreError(method, "unknown", "the store is unavailable in this environment"));
717
+ return {
718
+ put: Effect.fn("AttemptStore.put")(() => unavailable("put")),
719
+ get: Effect.fn("AttemptStore.get")(() => unavailable("get")),
720
+ heartbeat: Effect.fn("AttemptStore.heartbeat")(() => unavailable("heartbeat")),
721
+ finish: Effect.fn("AttemptStore.finish")(() => unavailable("finish")),
722
+ patch: Effect.fn("AttemptStore.patch")(() => unavailable("patch")),
723
+ ...overrides
724
+ };
725
+ };
726
+ const layerNoop = (overrides = {}) => Layer.succeed(AttemptStore)(makeNoop(overrides));
727
+ const layer = Layer.effect(AttemptStore)(
728
+ make
729
+ );
730
+ const layerWith = (options) => Layer.effect(AttemptStore)(makeWith(options));
731
+ //# sourceMappingURL=AttemptStore.js.map