@smthrs/journal 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 (257) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -2
  4. package/dist/cjs/CallFact.d.ts +129 -0
  5. package/dist/cjs/CallFact.d.ts.map +1 -0
  6. package/dist/cjs/CallFact.js +106 -0
  7. package/dist/cjs/CallFact.js.map +7 -0
  8. package/dist/cjs/Consensus.d.ts +440 -0
  9. package/dist/cjs/Consensus.d.ts.map +1 -0
  10. package/dist/cjs/Consensus.js +251 -0
  11. package/dist/cjs/Consensus.js.map +7 -0
  12. package/dist/cjs/EngineEvent.d.ts +890 -0
  13. package/dist/cjs/EngineEvent.d.ts.map +1 -0
  14. package/dist/cjs/EngineEvent.js +383 -0
  15. package/dist/cjs/EngineEvent.js.map +7 -0
  16. package/dist/cjs/ExecutionFact.d.ts +216 -0
  17. package/dist/cjs/ExecutionFact.d.ts.map +1 -0
  18. package/dist/cjs/ExecutionFact.js +303 -0
  19. package/dist/cjs/ExecutionFact.js.map +7 -0
  20. package/dist/cjs/Journal.d.ts +617 -0
  21. package/dist/cjs/Journal.d.ts.map +1 -0
  22. package/dist/cjs/Journal.js +190 -0
  23. package/dist/cjs/Journal.js.map +7 -0
  24. package/dist/cjs/JournalEvent.d.ts +421 -0
  25. package/dist/cjs/JournalEvent.d.ts.map +1 -0
  26. package/dist/cjs/JournalEvent.js +281 -0
  27. package/dist/cjs/JournalEvent.js.map +7 -0
  28. package/dist/cjs/JournalGeneration.d.ts +34 -0
  29. package/dist/cjs/JournalGeneration.d.ts.map +1 -0
  30. package/dist/cjs/JournalGeneration.js +72 -0
  31. package/dist/cjs/JournalGeneration.js.map +7 -0
  32. package/dist/cjs/JournalMetrics.d.ts +72 -0
  33. package/dist/cjs/JournalMetrics.d.ts.map +1 -0
  34. package/dist/cjs/JournalMetrics.js +55 -0
  35. package/dist/cjs/JournalMetrics.js.map +7 -0
  36. package/dist/cjs/Migrations.d.ts +42 -0
  37. package/dist/cjs/Migrations.d.ts.map +1 -0
  38. package/dist/cjs/Migrations.js +58 -0
  39. package/dist/cjs/Migrations.js.map +7 -0
  40. package/dist/cjs/OwnerId.d.ts +40 -0
  41. package/dist/cjs/OwnerId.d.ts.map +1 -0
  42. package/dist/cjs/OwnerId.js +40 -0
  43. package/dist/cjs/OwnerId.js.map +7 -0
  44. package/dist/cjs/Projection.d.ts +29 -0
  45. package/dist/cjs/Projection.d.ts.map +1 -0
  46. package/dist/cjs/Projection.js +25 -0
  47. package/dist/cjs/Projection.js.map +7 -0
  48. package/dist/cjs/RedactedLogger.d.ts +121 -0
  49. package/dist/cjs/RedactedLogger.d.ts.map +1 -0
  50. package/dist/cjs/RedactedLogger.js +235 -0
  51. package/dist/cjs/RedactedLogger.js.map +7 -0
  52. package/dist/cjs/Redaction.d.ts +350 -0
  53. package/dist/cjs/Redaction.d.ts.map +1 -0
  54. package/dist/cjs/Redaction.js +928 -0
  55. package/dist/cjs/Redaction.js.map +7 -0
  56. package/dist/cjs/SqlConsensus.d.ts +44 -0
  57. package/dist/cjs/SqlConsensus.d.ts.map +1 -0
  58. package/dist/cjs/SqlConsensus.js +243 -0
  59. package/dist/cjs/SqlConsensus.js.map +7 -0
  60. package/dist/cjs/SqlJournal.d.ts +173 -0
  61. package/dist/cjs/SqlJournal.d.ts.map +1 -0
  62. package/dist/cjs/SqlJournal.js +1568 -0
  63. package/dist/cjs/SqlJournal.js.map +7 -0
  64. package/dist/cjs/StepFact.d.ts +66 -0
  65. package/dist/cjs/StepFact.d.ts.map +1 -0
  66. package/dist/cjs/StepFact.js +68 -0
  67. package/dist/cjs/StepFact.js.map +7 -0
  68. package/dist/cjs/index.d.ts +95 -0
  69. package/dist/cjs/index.d.ts.map +1 -0
  70. package/dist/cjs/index.js +63 -0
  71. package/dist/cjs/index.js.map +7 -0
  72. package/dist/cjs/internal/JournalInternalFault.d.ts +37 -0
  73. package/dist/cjs/internal/JournalInternalFault.d.ts.map +1 -0
  74. package/dist/cjs/internal/JournalInternalFault.js +43 -0
  75. package/dist/cjs/internal/JournalInternalFault.js.map +7 -0
  76. package/dist/cjs/migrations/0001_initial.d.ts +15 -0
  77. package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
  78. package/dist/cjs/migrations/0001_initial.js +54 -0
  79. package/dist/cjs/migrations/0001_initial.js.map +7 -0
  80. package/dist/cjs/migrations/0002_checkpoints.d.ts +27 -0
  81. package/dist/cjs/migrations/0002_checkpoints.d.ts.map +1 -0
  82. package/dist/cjs/migrations/0002_checkpoints.js +48 -0
  83. package/dist/cjs/migrations/0002_checkpoints.js.map +7 -0
  84. package/dist/cjs/migrations/0003_startup_index.d.ts +15 -0
  85. package/dist/cjs/migrations/0003_startup_index.d.ts.map +1 -0
  86. package/dist/cjs/migrations/0003_startup_index.js +40 -0
  87. package/dist/cjs/migrations/0003_startup_index.js.map +7 -0
  88. package/dist/cjs/migrations/0004_dedup.d.ts +15 -0
  89. package/dist/cjs/migrations/0004_dedup.d.ts.map +1 -0
  90. package/dist/cjs/migrations/0004_dedup.js +57 -0
  91. package/dist/cjs/migrations/0004_dedup.js.map +7 -0
  92. package/dist/cjs/migrations/0005_run_event_type.d.ts +15 -0
  93. package/dist/cjs/migrations/0005_run_event_type.d.ts.map +1 -0
  94. package/dist/cjs/migrations/0005_run_event_type.js +40 -0
  95. package/dist/cjs/migrations/0005_run_event_type.js.map +7 -0
  96. package/dist/cjs/migrations/0006_consensus.d.ts +22 -0
  97. package/dist/cjs/migrations/0006_consensus.d.ts.map +1 -0
  98. package/dist/cjs/migrations/0006_consensus.js +84 -0
  99. package/dist/cjs/migrations/0006_consensus.js.map +7 -0
  100. package/dist/cjs/package.json +1 -0
  101. package/dist/cjs/test/Notifying.d.ts +65 -0
  102. package/dist/cjs/test/Notifying.d.ts.map +1 -0
  103. package/dist/cjs/test/Notifying.js +62 -0
  104. package/dist/cjs/test/Notifying.js.map +7 -0
  105. package/dist/cjs/test/TestJournal.d.ts +43 -0
  106. package/dist/cjs/test/TestJournal.d.ts.map +1 -0
  107. package/dist/cjs/test/TestJournal.js +43 -0
  108. package/dist/cjs/test/TestJournal.js.map +7 -0
  109. package/dist/esm/CallFact.d.ts +129 -0
  110. package/dist/esm/CallFact.d.ts.map +1 -0
  111. package/dist/esm/CallFact.js +96 -0
  112. package/dist/esm/CallFact.js.map +1 -0
  113. package/dist/esm/Consensus.d.ts +440 -0
  114. package/dist/esm/Consensus.d.ts.map +1 -0
  115. package/dist/esm/Consensus.js +454 -0
  116. package/dist/esm/Consensus.js.map +1 -0
  117. package/dist/esm/EngineEvent.d.ts +890 -0
  118. package/dist/esm/EngineEvent.d.ts.map +1 -0
  119. package/dist/esm/EngineEvent.js +614 -0
  120. package/dist/esm/EngineEvent.js.map +1 -0
  121. package/dist/esm/ExecutionFact.d.ts +216 -0
  122. package/dist/esm/ExecutionFact.d.ts.map +1 -0
  123. package/dist/esm/ExecutionFact.js +374 -0
  124. package/dist/esm/ExecutionFact.js.map +1 -0
  125. package/dist/esm/Journal.d.ts +617 -0
  126. package/dist/esm/Journal.d.ts.map +1 -0
  127. package/dist/esm/Journal.js +323 -0
  128. package/dist/esm/Journal.js.map +1 -0
  129. package/dist/esm/JournalEvent.d.ts +421 -0
  130. package/dist/esm/JournalEvent.d.ts.map +1 -0
  131. package/dist/esm/JournalEvent.js +517 -0
  132. package/dist/esm/JournalEvent.js.map +1 -0
  133. package/dist/esm/JournalGeneration.d.ts +34 -0
  134. package/dist/esm/JournalGeneration.d.ts.map +1 -0
  135. package/dist/esm/JournalGeneration.js +65 -0
  136. package/dist/esm/JournalGeneration.js.map +1 -0
  137. package/dist/esm/JournalMetrics.d.ts +72 -0
  138. package/dist/esm/JournalMetrics.d.ts.map +1 -0
  139. package/dist/esm/JournalMetrics.js +79 -0
  140. package/dist/esm/JournalMetrics.js.map +1 -0
  141. package/dist/esm/Migrations.d.ts +42 -0
  142. package/dist/esm/Migrations.d.ts.map +1 -0
  143. package/dist/esm/Migrations.js +59 -0
  144. package/dist/esm/Migrations.js.map +1 -0
  145. package/dist/esm/OwnerId.d.ts +40 -0
  146. package/dist/esm/OwnerId.d.ts.map +1 -0
  147. package/dist/esm/OwnerId.js +33 -0
  148. package/dist/esm/OwnerId.js.map +1 -0
  149. package/dist/esm/Projection.d.ts +29 -0
  150. package/dist/esm/Projection.d.ts.map +1 -0
  151. package/dist/esm/Projection.js +13 -0
  152. package/dist/esm/Projection.js.map +1 -0
  153. package/dist/esm/RedactedLogger.d.ts +121 -0
  154. package/dist/esm/RedactedLogger.d.ts.map +1 -0
  155. package/dist/esm/RedactedLogger.js +445 -0
  156. package/dist/esm/RedactedLogger.js.map +1 -0
  157. package/dist/esm/Redaction.d.ts +350 -0
  158. package/dist/esm/Redaction.d.ts.map +1 -0
  159. package/dist/esm/Redaction.js +1584 -0
  160. package/dist/esm/Redaction.js.map +1 -0
  161. package/dist/esm/SqlConsensus.d.ts +44 -0
  162. package/dist/esm/SqlConsensus.d.ts.map +1 -0
  163. package/dist/esm/SqlConsensus.js +252 -0
  164. package/dist/esm/SqlConsensus.js.map +1 -0
  165. package/dist/esm/SqlJournal.d.ts +173 -0
  166. package/dist/esm/SqlJournal.d.ts.map +1 -0
  167. package/dist/esm/SqlJournal.js +1858 -0
  168. package/dist/esm/SqlJournal.js.map +1 -0
  169. package/dist/esm/StepFact.d.ts +66 -0
  170. package/dist/esm/StepFact.d.ts.map +1 -0
  171. package/dist/esm/StepFact.js +50 -0
  172. package/dist/esm/StepFact.js.map +1 -0
  173. package/dist/esm/index.d.ts +95 -0
  174. package/dist/esm/index.d.ts.map +1 -0
  175. package/dist/esm/index.js +95 -0
  176. package/dist/esm/index.js.map +1 -0
  177. package/dist/esm/internal/JournalInternalFault.d.ts +37 -0
  178. package/dist/esm/internal/JournalInternalFault.d.ts.map +1 -0
  179. package/dist/esm/internal/JournalInternalFault.js +28 -0
  180. package/dist/esm/internal/JournalInternalFault.js.map +1 -0
  181. package/dist/esm/migrations/0001_initial.d.ts +15 -0
  182. package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
  183. package/dist/esm/migrations/0001_initial.js +32 -0
  184. package/dist/esm/migrations/0001_initial.js.map +1 -0
  185. package/dist/esm/migrations/0002_checkpoints.d.ts +27 -0
  186. package/dist/esm/migrations/0002_checkpoints.d.ts.map +1 -0
  187. package/dist/esm/migrations/0002_checkpoints.js +38 -0
  188. package/dist/esm/migrations/0002_checkpoints.js.map +1 -0
  189. package/dist/esm/migrations/0003_startup_index.d.ts +15 -0
  190. package/dist/esm/migrations/0003_startup_index.d.ts.map +1 -0
  191. package/dist/esm/migrations/0003_startup_index.js +18 -0
  192. package/dist/esm/migrations/0003_startup_index.js.map +1 -0
  193. package/dist/esm/migrations/0004_dedup.d.ts +15 -0
  194. package/dist/esm/migrations/0004_dedup.d.ts.map +1 -0
  195. package/dist/esm/migrations/0004_dedup.js +38 -0
  196. package/dist/esm/migrations/0004_dedup.js.map +1 -0
  197. package/dist/esm/migrations/0005_run_event_type.d.ts +15 -0
  198. package/dist/esm/migrations/0005_run_event_type.d.ts.map +1 -0
  199. package/dist/esm/migrations/0005_run_event_type.js +18 -0
  200. package/dist/esm/migrations/0005_run_event_type.js.map +1 -0
  201. package/dist/esm/migrations/0006_consensus.d.ts +22 -0
  202. package/dist/esm/migrations/0006_consensus.d.ts.map +1 -0
  203. package/dist/esm/migrations/0006_consensus.js +69 -0
  204. package/dist/esm/migrations/0006_consensus.js.map +1 -0
  205. package/dist/esm/test/Notifying.d.ts +65 -0
  206. package/dist/esm/test/Notifying.d.ts.map +1 -0
  207. package/dist/esm/test/Notifying.js +62 -0
  208. package/dist/esm/test/Notifying.js.map +1 -0
  209. package/dist/esm/test/TestJournal.d.ts +43 -0
  210. package/dist/esm/test/TestJournal.d.ts.map +1 -0
  211. package/dist/esm/test/TestJournal.js +27 -0
  212. package/dist/esm/test/TestJournal.js.map +1 -0
  213. package/docs/README.md +164 -0
  214. package/docs/api.md +639 -0
  215. package/docs/concepts/compaction.md +157 -0
  216. package/docs/concepts/idempotency.md +127 -0
  217. package/docs/concepts/owner-fence.md +124 -0
  218. package/docs/concepts/redaction.md +218 -0
  219. package/docs/concepts/state-event-authority.md +159 -0
  220. package/docs/concepts/two-channels.md +167 -0
  221. package/docs/guides/commit-state-and-entry.md +114 -0
  222. package/docs/guides/compact-a-run.md +134 -0
  223. package/docs/guides/fold-a-projection.md +106 -0
  224. package/docs/guides/read-a-run.md +139 -0
  225. package/docs/guides/redact-log-output.md +94 -0
  226. package/docs/guides/testing.md +124 -0
  227. package/docs/guides/write-lifecycle-events.md +110 -0
  228. package/docs/installation.md +133 -0
  229. package/docs/quickstart.md +143 -0
  230. package/docs/troubleshooting.md +214 -0
  231. package/package.json +263 -3
  232. package/src/CallFact.ts +117 -0
  233. package/src/Consensus.ts +683 -0
  234. package/src/EngineEvent.ts +823 -0
  235. package/src/ExecutionFact.ts +433 -0
  236. package/src/Journal.ts +688 -0
  237. package/src/JournalEvent.ts +654 -0
  238. package/src/JournalGeneration.ts +77 -0
  239. package/src/JournalMetrics.ts +89 -0
  240. package/src/Migrations.ts +62 -0
  241. package/src/OwnerId.ts +42 -0
  242. package/src/Projection.ts +31 -0
  243. package/src/RedactedLogger.ts +483 -0
  244. package/src/Redaction.ts +1784 -0
  245. package/src/SqlConsensus.ts +344 -0
  246. package/src/SqlJournal.ts +2732 -0
  247. package/src/StepFact.ts +69 -0
  248. package/src/index.ts +109 -0
  249. package/src/internal/JournalInternalFault.ts +40 -0
  250. package/src/migrations/0001_initial.ts +41 -0
  251. package/src/migrations/0002_checkpoints.ts +46 -0
  252. package/src/migrations/0003_startup_index.ts +19 -0
  253. package/src/migrations/0004_dedup.ts +43 -0
  254. package/src/migrations/0005_run_event_type.ts +19 -0
  255. package/src/migrations/0006_consensus.ts +83 -0
  256. package/src/test/Notifying.ts +101 -0
  257. package/src/test/TestJournal.ts +52 -0
@@ -0,0 +1,1568 @@
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 SqlJournal_exports = {};
30
+ __export(SqlJournal_exports, {
31
+ layer: () => layer,
32
+ layerWith: () => layerWith,
33
+ make: () => make
34
+ });
35
+ module.exports = __toCommonJS(SqlJournal_exports);
36
+ var Dialect = __toESM(require("@smthrs/database/Dialect"), 1);
37
+ var import_DurableWriter = require("@smthrs/database/DurableWriter");
38
+ var Cause = __toESM(require("effect/Cause"), 1);
39
+ var Clock = __toESM(require("effect/Clock"), 1);
40
+ var Deferred = __toESM(require("effect/Deferred"), 1);
41
+ var Effect = __toESM(require("effect/Effect"), 1);
42
+ var Exit = __toESM(require("effect/Exit"), 1);
43
+ var Layer = __toESM(require("effect/Layer"), 1);
44
+ var Metric = __toESM(require("effect/Metric"), 1);
45
+ var Option = __toESM(require("effect/Option"), 1);
46
+ var PubSub = __toESM(require("effect/PubSub"), 1);
47
+ var Queue = __toESM(require("effect/Queue"), 1);
48
+ var Result = __toESM(require("effect/Result"), 1);
49
+ var Schema = __toESM(require("effect/Schema"), 1);
50
+ var Scope = __toESM(require("effect/Scope"), 1);
51
+ var Semaphore = __toESM(require("effect/Semaphore"), 1);
52
+ var Stream = __toESM(require("effect/Stream"), 1);
53
+ var SqlClient = __toESM(require("effect/unstable/sql/SqlClient"), 1);
54
+ var import_Consensus = require("./Consensus.js");
55
+ var import_JournalInternalFault = require("./internal/JournalInternalFault.js");
56
+ var import_Journal = require("./Journal.js");
57
+ var import_JournalEvent = require("./JournalEvent.js");
58
+ var JournalGeneration = __toESM(require("./JournalGeneration.js"), 1);
59
+ var JournalMetrics = __toESM(require("./JournalMetrics.js"), 1);
60
+ var import_OwnerId = require("./OwnerId.js");
61
+ var Redaction = __toESM(require("./Redaction.js"), 1);
62
+ var SqlConsensus = __toESM(require("./SqlConsensus.js"), 1);
63
+ const UnknownFromJsonString = Schema.fromJsonString(Schema.Unknown);
64
+ const defaultSourceEventCache = 4096;
65
+ const compactionCaptureTimeout = "30 seconds";
66
+ const error = (code, message, cause) => new import_Journal.JournalError({
67
+ code,
68
+ message,
69
+ ...cause === void 0 ? {} : { cause }
70
+ });
71
+ const sourceKey = (runId, sourceId) => `${runId.length}:${runId}${sourceId.length}:${sourceId}`;
72
+ const sourceEventKey = (runId, sourceId, sourceSeq) => `${sourceKey(runId, sourceId)}:${sourceSeq}`;
73
+ const canonicalizeFingerprint = (value, depth) => {
74
+ if (depth > Redaction.maxDepth) {
75
+ throw new RangeError(`journal value nests deeper than ${Redaction.maxDepth} containers`);
76
+ }
77
+ if (Array.isArray(value)) return value.map((element) => canonicalizeFingerprint(element, depth + 1));
78
+ if (value === null || typeof value !== "object") return value;
79
+ const record = value;
80
+ return Object.fromEntries(
81
+ Object.keys(record).sort().map((key) => [key, canonicalizeFingerprint(record[key], depth + 1)])
82
+ );
83
+ };
84
+ const encodeJson = (value, field) => Schema.encodeUnknownResult(UnknownFromJsonString)(value).pipe(
85
+ Result.mapError((cause) => error("invalid_event", `${field} must be JSON-serializable`, cause))
86
+ );
87
+ const encodeFingerprint = (value, field) => Result.flatMap(encodeJson(value, field), (json) => Result.try({
88
+ try: () => JSON.stringify(canonicalizeFingerprint(JSON.parse(json), 0)),
89
+ catch: (cause) => error("invalid_event", `${field} could not be canonicalized`, cause)
90
+ }));
91
+ const unfenced = { _tag: "Unfenced" };
92
+ const decodeOwner = Schema.decodeUnknownResult(import_OwnerId.OwnerId);
93
+ const requireFence = (owner, method) => Result.match(decodeOwner(owner), {
94
+ onFailure: (cause) => Result.fail(error("invalid_event", `${method} requires a well-formed owner fence`, cause)),
95
+ onSuccess: (value) => Result.succeed({ _tag: "Owned", owner: value })
96
+ });
97
+ const decodeInput = Schema.decodeUnknownResult(import_JournalEvent.Input);
98
+ const decodeEntry = Schema.decodeUnknownEffect(import_JournalEvent.Entry);
99
+ const decodeEntriesOptions = Schema.decodeUnknownResult(import_Journal.EntriesOptions);
100
+ const decodeStreamOptions = Schema.decodeUnknownResult(import_Journal.StreamOptions);
101
+ const decodeCheckpointOptions = Schema.decodeUnknownResult(import_Journal.CheckpointOptions);
102
+ const decodeCompactOptions = Schema.decodeUnknownResult(import_Journal.CompactOptions);
103
+ const decodeRunId = Schema.decodeUnknownResult(import_JournalEvent.RunId);
104
+ const decodeRow = (row) => Effect.all({
105
+ payload: Schema.decodeUnknownEffect(UnknownFromJsonString)(row.payload_json),
106
+ meta: Schema.decodeUnknownEffect(UnknownFromJsonString)(row.meta_json)
107
+ }).pipe(
108
+ Effect.map(({ meta, payload }) => ({
109
+ runId: row.run_id,
110
+ seq: Number(row.seq),
111
+ eventId: row.event_id,
112
+ sourceId: row.source_id,
113
+ sourceSeq: Number(row.source_seq),
114
+ emittedAtMs: Number(row.emitted_at_ms),
115
+ eventType: row.event_type,
116
+ payload,
117
+ meta
118
+ })),
119
+ Effect.flatMap(decodeEntry),
120
+ Effect.mapError((cause) => error("decode_failed", "could not decode a durable journal row", cause))
121
+ );
122
+ const decodeCheckpoint = Schema.decodeUnknownEffect(import_Journal.Checkpoint);
123
+ const decodeCheckpointRow = (row) => Schema.decodeUnknownEffect(UnknownFromJsonString)(row.state_json).pipe(
124
+ Effect.flatMap(
125
+ (state) => decodeCheckpoint({
126
+ runId: row.run_id,
127
+ seq: Number(row.seq),
128
+ state,
129
+ createdAtMs: Number(row.created_at_ms),
130
+ compactedAtMs: row.compacted_at_ms === null ? null : Number(row.compacted_at_ms)
131
+ })
132
+ ),
133
+ Effect.mapError((cause) => error("decode_failed", "could not decode a durable checkpoint row", cause))
134
+ );
135
+ const freezePublished = (value) => {
136
+ const seen = /* @__PURE__ */ new WeakSet();
137
+ const walk = (node) => {
138
+ if (node === null || typeof node !== "object") return;
139
+ if (seen.has(node)) return;
140
+ seen.add(node);
141
+ for (const key of Reflect.ownKeys(node)) {
142
+ walk(node[key]);
143
+ }
144
+ Object.freeze(node);
145
+ };
146
+ walk(value);
147
+ return value;
148
+ };
149
+ const encodedBytes = (encoder, payloadJson, metaJson) => encoder.encode(payloadJson).length + encoder.encode(metaJson).length;
150
+ const validateOptions = (options) => Effect.suspend(() => {
151
+ if (!Number.isSafeInteger(options.capacity) || options.capacity <= 0) {
152
+ return Effect.fail(error("invalid_event", "capacity must be a positive safe integer"));
153
+ }
154
+ const batchSize = options.batchSize ?? Math.min(options.capacity, 64);
155
+ if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
156
+ return Effect.fail(error("invalid_event", "batchSize must be a positive safe integer"));
157
+ }
158
+ const sourceEventCache = options.sourceEventCache ?? defaultSourceEventCache;
159
+ if (!Number.isSafeInteger(sourceEventCache) || sourceEventCache <= 0) {
160
+ return Effect.fail(error("invalid_event", "sourceEventCache must be a positive safe integer"));
161
+ }
162
+ if (options.maxEntryBytes !== void 0 && (!Number.isSafeInteger(options.maxEntryBytes) || options.maxEntryBytes <= 0)) {
163
+ return Effect.fail(error("invalid_event", "maxEntryBytes must be a positive safe integer"));
164
+ }
165
+ if (options.compaction !== void 0 && (!Number.isSafeInteger(options.compaction.entryThreshold) || options.compaction.entryThreshold <= 0)) {
166
+ return Effect.fail(error("invalid_event", "compaction.entryThreshold must be a positive safe integer"));
167
+ }
168
+ return Effect.succeed({
169
+ batchSize,
170
+ sourceEventCache,
171
+ maxEntryBytes: options.maxEntryBytes,
172
+ redact: options.redact ?? Redaction.make()
173
+ });
174
+ });
175
+ const isJournalError = Schema.is(import_Journal.JournalError);
176
+ const make = (options) => Effect.gen(function* () {
177
+ const { batchSize, maxEntryBytes, redact, sourceEventCache } = yield* validateOptions(options);
178
+ const readPageSize = Math.min(batchSize, import_Journal.maxEntriesLimit);
179
+ const sql = yield* Effect.service(SqlClient.SqlClient);
180
+ const writer = yield* import_DurableWriter.DurableWriter;
181
+ const consensus = yield* import_Consensus.Consensus;
182
+ yield* JournalGeneration.initialize.pipe(
183
+ Effect.mapError((cause) => error("read_failed", "could not initialize journal generations", cause))
184
+ );
185
+ const byteCounter = new TextEncoder();
186
+ const contentFingerprint = (eventType, payloadJson, metaJson) => Effect.tryPromise({
187
+ try: () => crypto.subtle.digest("SHA-256", byteCounter.encode(JSON.stringify([eventType, payloadJson, metaJson]))),
188
+ catch: (cause) => error("sink_failed", "could not fingerprint journal content", cause)
189
+ }).pipe(
190
+ Effect.map(
191
+ (digest) => Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")
192
+ )
193
+ );
194
+ const queue = yield* Queue.dropping(options.capacity);
195
+ const changes = yield* PubSub.sliding(options.capacity);
196
+ const wakes = /* @__PURE__ */ new Map();
197
+ const readers = /* @__PURE__ */ new Map();
198
+ const sourceEventRows = yield* sql`
199
+ SELECT run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
200
+ event_type, payload_json, meta_json
201
+ FROM flows_journal_events
202
+ ORDER BY emitted_at_ms DESC, run_id DESC, seq DESC
203
+ LIMIT ${sourceEventCache}
204
+ `.pipe(
205
+ Effect.mapError((cause) => error("sink_failed", "could not initialize journal source events", cause))
206
+ );
207
+ const durableEntries = yield* Effect.forEach(sourceEventRows, decodeRow);
208
+ const initialized = yield* Effect.fromResult(
209
+ Result.gen(function* () {
210
+ const sequences = /* @__PURE__ */ new Map();
211
+ const sourceSequences = /* @__PURE__ */ new Map();
212
+ const sourceEvents = /* @__PURE__ */ new Map();
213
+ for (const entry of [...durableEntries].reverse()) {
214
+ if (!Number.isSafeInteger(entry.seq) || !Number.isSafeInteger(entry.sourceSeq) || entry.seq === Number.MAX_SAFE_INTEGER || entry.sourceSeq === Number.MAX_SAFE_INTEGER) {
215
+ return yield* Result.fail(
216
+ error("decode_failed", "durable event sequence is outside the allocatable safe integer range")
217
+ );
218
+ }
219
+ sourceEvents.set(sourceEventKey(entry.runId, entry.sourceId, entry.sourceSeq), {
220
+ seq: entry.seq,
221
+ eventType: entry.eventType,
222
+ payloadJson: yield* encodeFingerprint(entry.payload, "payload"),
223
+ metaJson: yield* encodeFingerprint(entry.meta, "meta"),
224
+ status: "committed"
225
+ });
226
+ }
227
+ return {
228
+ sequences,
229
+ sourceSequences,
230
+ sourceEvents
231
+ };
232
+ })
233
+ );
234
+ const state = {
235
+ status: "open",
236
+ sinkFailure: void 0,
237
+ lossEpoch: 0,
238
+ flushedLossEpoch: 0,
239
+ pending: 0,
240
+ pendingByRun: /* @__PURE__ */ new Map(),
241
+ sequences: initialized.sequences,
242
+ sourceSequences: initialized.sourceSequences,
243
+ sourceEvents: initialized.sourceEvents,
244
+ flushWaiters: /* @__PURE__ */ new Set()
245
+ };
246
+ const allocation = yield* Semaphore.make(1);
247
+ const maintenanceScope = yield* Scope.make();
248
+ yield* Effect.addFinalizer((exit) => Scope.close(maintenanceScope, exit));
249
+ let pendingMaintenance = 0;
250
+ const compactionCounts = /* @__PURE__ */ new Map();
251
+ const compactingRuns = /* @__PURE__ */ new Set();
252
+ const runBarriers = /* @__PURE__ */ new Map();
253
+ const runDrainWaiters = /* @__PURE__ */ new Map();
254
+ const activeWritesByRun = /* @__PURE__ */ new Map();
255
+ const barrierFor = (runId) => {
256
+ const existing = runBarriers.get(runId);
257
+ if (existing !== void 0) return existing;
258
+ const created = {
259
+ semaphore: Semaphore.makeUnsafe(1),
260
+ compactionLock: Semaphore.makeUnsafe(1),
261
+ maintenance: Semaphore.makeUnsafe(1),
262
+ users: 0,
263
+ compaction: void 0
264
+ };
265
+ runBarriers.set(runId, created);
266
+ return created;
267
+ };
268
+ const withRunBarrier = (runId, use) => Effect.suspend(() => {
269
+ const barrier = barrierFor(runId);
270
+ barrier.users += 1;
271
+ return use(barrier).pipe(Effect.ensuring(Effect.sync(() => {
272
+ barrier.users -= 1;
273
+ })));
274
+ });
275
+ const retireQuiescentRuns = () => {
276
+ for (const [runId, barrier] of runBarriers) {
277
+ if (barrier.users > 0 || barrier.compaction !== void 0 || state.pendingByRun.has(runId) || activeWritesByRun.has(runId) || runDrainWaiters.has(runId) || readers.has(runId) || wakes.has(runId) || compactingRuns.has(runId)) continue;
278
+ runBarriers.delete(runId);
279
+ compactionCounts.delete(runId);
280
+ }
281
+ };
282
+ const withRunAdmission = (runId, effect) => withRunBarrier(runId, (barrier) => barrier.semaphore.withPermit(
283
+ Effect.suspend(() => {
284
+ const gate = barrier.compaction;
285
+ return gate === void 0 ? Effect.map(effect, (value) => ({ _tag: "Done", value })) : Effect.succeed({ _tag: "Wait", gate });
286
+ })
287
+ ).pipe(
288
+ Effect.flatMap(
289
+ (attempt) => attempt._tag === "Done" ? Effect.succeed(attempt.value) : Deferred.await(attempt.gate).pipe(
290
+ Effect.andThen(withRunAdmission(runId, effect))
291
+ )
292
+ )
293
+ ));
294
+ const withRunPermits = (runIds, effect) => {
295
+ const ordered = [...new Set(runIds)].sort();
296
+ const acquire = (index) => index === ordered.length ? effect : withRunBarrier(ordered[index], (barrier) => barrier.semaphore.withPermit(acquire(index + 1)));
297
+ return acquire(0);
298
+ };
299
+ const admitRunPending = (queued) => {
300
+ const pending = state.pendingByRun.get(queued.runId) ?? /* @__PURE__ */ new Set();
301
+ pending.add(queued);
302
+ state.pendingByRun.set(queued.runId, pending);
303
+ };
304
+ const completeRunDrain = (runId) => {
305
+ if ((state.pendingByRun.get(runId)?.size ?? 0) + (activeWritesByRun.get(runId) ?? 0) > 0) return;
306
+ const waiters = runDrainWaiters.get(runId);
307
+ runDrainWaiters.delete(runId);
308
+ for (const waiter of waiters ?? []) {
309
+ Deferred.doneUnsafe(waiter, Effect.void);
310
+ }
311
+ };
312
+ const settleRunPending = (batch) => {
313
+ for (const queued of batch) {
314
+ const pending = state.pendingByRun.get(queued.runId);
315
+ pending.delete(queued);
316
+ if (pending.size > 0) continue;
317
+ state.pendingByRun.delete(queued.runId);
318
+ completeRunDrain(queued.runId);
319
+ }
320
+ };
321
+ const endRunWrite = (runId) => {
322
+ const remaining = activeWritesByRun.get(runId) - 1;
323
+ if (remaining > 0) {
324
+ activeWritesByRun.set(runId, remaining);
325
+ return;
326
+ }
327
+ activeWritesByRun.delete(runId);
328
+ completeRunDrain(runId);
329
+ };
330
+ const withActiveRunWrite = (runId, effect) => withRunBarrier(runId, () => Effect.suspend(() => {
331
+ activeWritesByRun.set(runId, (activeWritesByRun.get(runId) ?? 0) + 1);
332
+ return effect.pipe(Effect.ensuring(Effect.sync(() => endRunWrite(runId))));
333
+ }));
334
+ const awaitRunDrained = (runId) => Effect.suspend(() => {
335
+ if ((state.pendingByRun.get(runId)?.size ?? 0) + (activeWritesByRun.get(runId) ?? 0) === 0) {
336
+ return Effect.void;
337
+ }
338
+ const waiter = Deferred.makeUnsafe();
339
+ const waiters = runDrainWaiters.get(runId) ?? /* @__PURE__ */ new Set();
340
+ waiters.add(waiter);
341
+ runDrainWaiters.set(runId, waiters);
342
+ return Deferred.await(waiter);
343
+ });
344
+ const withCompactionBarrier = (runId, effect) => Effect.flatMap(Effect.serviceOption(sql.transactionService), (enclosing) => withRunBarrier(runId, (barrier) => {
345
+ const nested = Option.isSome(enclosing);
346
+ const attempt = (nested ? Effect.void : awaitRunDrained(runId)).pipe(
347
+ Effect.andThen(writer.write(
348
+ barrier.compactionLock.withPermit(Effect.uninterruptibleMask(
349
+ (restore) => barrier.semaphore.withPermit(Effect.sync(() => {
350
+ if (!nested && (state.pendingByRun.get(runId)?.size ?? 0) + (activeWritesByRun.get(runId) ?? 0) > 0) {
351
+ return void 0;
352
+ }
353
+ const gate = Deferred.makeUnsafe();
354
+ barrier.compaction = gate;
355
+ return gate;
356
+ })).pipe(Effect.flatMap(
357
+ (gate) => gate === void 0 ? Effect.succeed(Option.none()) : restore(Effect.gen(function* () {
358
+ if (nested) {
359
+ const outcome = yield* insertBatch([...state.pendingByRun.get(runId) ?? []]);
360
+ for (const { queued, commit } of outcome.commits) {
361
+ yield* settleCommit(queued, commit, restore);
362
+ }
363
+ }
364
+ return yield* Effect.asSome(effect);
365
+ })).pipe(Effect.ensuring(Effect.sync(() => {
366
+ barrier.compaction = void 0;
367
+ Deferred.doneUnsafe(gate, Effect.void);
368
+ })))
369
+ ))
370
+ ))
371
+ )),
372
+ Effect.flatMap(
373
+ (result) => Option.isSome(result) ? Effect.succeed(result.value) : attempt
374
+ )
375
+ );
376
+ return attempt;
377
+ }));
378
+ const raiseSequenceFloor = (runId, seq) => {
379
+ state.sequences.set(runId, Math.max(state.sequences.get(runId) ?? 0, seq + 1));
380
+ };
381
+ const raiseSourceSequenceFloor = (runId, sourceId, sourceSeq) => {
382
+ const key = sourceKey(runId, sourceId);
383
+ state.sourceSequences.set(key, Math.max(state.sourceSequences.get(key) ?? 0, sourceSeq + 1));
384
+ };
385
+ const retain = (identity, event) => {
386
+ state.sourceEvents.delete(identity);
387
+ state.sourceEvents.set(identity, event);
388
+ if (state.sourceEvents.size <= sourceEventCache) {
389
+ return;
390
+ }
391
+ for (const [candidate, retained] of state.sourceEvents) {
392
+ if (retained.status !== "committed" || candidate === identity) {
393
+ continue;
394
+ }
395
+ state.sourceEvents.delete(candidate);
396
+ return;
397
+ }
398
+ };
399
+ const completeFlushWaiters = (exit) => {
400
+ const waiters = Array.from(state.flushWaiters);
401
+ state.flushWaiters.clear();
402
+ for (const waiter of waiters) {
403
+ Deferred.doneUnsafe(waiter, exit);
404
+ }
405
+ };
406
+ const flushInternal = Effect.suspend(() => {
407
+ if (state.sinkFailure !== void 0 && state.lossEpoch > state.flushedLossEpoch) {
408
+ state.flushedLossEpoch = state.lossEpoch;
409
+ return Effect.fail(state.sinkFailure);
410
+ }
411
+ if (state.status === "closed") {
412
+ return Effect.fail(error("journal_closed", "journal is closed"));
413
+ }
414
+ if (state.pending === 0 && pendingMaintenance === 0) {
415
+ return Effect.void;
416
+ }
417
+ const waiter = Deferred.makeUnsafe();
418
+ state.flushWaiters.add(waiter);
419
+ return Deferred.await(waiter).pipe(
420
+ Effect.ensuring(Effect.sync(() => {
421
+ state.flushWaiters.delete(waiter);
422
+ }))
423
+ );
424
+ }).pipe(Effect.ensuring(Effect.sync(retireQuiescentRuns)));
425
+ const prepare = (input, emittedAtMs) => Result.gen(function* () {
426
+ if (state.status !== "open") {
427
+ return yield* Result.fail(error("journal_closed", "journal is closed"));
428
+ }
429
+ const validated = yield* Result.mapError(
430
+ decodeInput(input),
431
+ (cause) => error("invalid_event", "event violates the journal input contract", cause)
432
+ );
433
+ if (!Number.isSafeInteger(emittedAtMs) || emittedAtMs < 0) {
434
+ return yield* Result.fail(
435
+ error("invalid_event", "emittedAtMs must be a non-negative safe integer")
436
+ );
437
+ }
438
+ const redactedPayload = yield* Result.try({
439
+ try: () => redact(validated.payload),
440
+ catch: () => error("invalid_event", "payload could not be redacted")
441
+ });
442
+ const redactedMeta = yield* Result.try({
443
+ try: () => redact(validated.meta ?? null),
444
+ catch: () => error("invalid_event", "meta could not be redacted")
445
+ });
446
+ const payloadJson = yield* encodeFingerprint(redactedPayload, "payload");
447
+ const metaJson = yield* encodeFingerprint(redactedMeta, "meta");
448
+ if (maxEntryBytes !== void 0) {
449
+ const bytes = encodedBytes(byteCounter, payloadJson, metaJson);
450
+ if (bytes > maxEntryBytes) {
451
+ return yield* Result.fail(
452
+ error(
453
+ "invalid_event",
454
+ `event is ${bytes} bytes, over the ${maxEntryBytes}-byte maxEntryBytes bound`
455
+ )
456
+ );
457
+ }
458
+ }
459
+ return { validated, payloadJson, metaJson };
460
+ });
461
+ const compareSourceEvent = (prepared, sourceSeq, existing) => {
462
+ const { metaJson, payloadJson, validated } = prepared;
463
+ if (validated.dedupe !== "identity" && (existing.eventType !== validated.eventType || existing.payloadJson !== payloadJson || existing.metaJson !== metaJson)) {
464
+ return Result.fail(error(
465
+ "idempotency_conflict",
466
+ `source event ${validated.sourceId}:${sourceSeq} for run ${validated.runId} was reused with different content`
467
+ ));
468
+ }
469
+ return Result.succeed({
470
+ _tag: "Duplicate",
471
+ seq: existing.seq,
472
+ sourceSeq,
473
+ status: existing.status
474
+ });
475
+ };
476
+ const admitFromIndex = (prepared) => {
477
+ const { validated } = prepared;
478
+ const sourceSeq = validated.sourceSeq;
479
+ if (sourceSeq === void 0) return Result.succeed(void 0);
480
+ if (!Number.isSafeInteger(sourceSeq) || sourceSeq < 0 || sourceSeq === Number.MAX_SAFE_INTEGER) {
481
+ return Result.fail(
482
+ error("invalid_event", "journal sequence is outside the allocatable safe integer range")
483
+ );
484
+ }
485
+ const cached = state.sourceEvents.get(sourceEventKey(validated.runId, validated.sourceId, sourceSeq));
486
+ return cached === void 0 ? Result.succeed(void 0) : compareSourceEvent(prepared, sourceSeq, cached);
487
+ };
488
+ const admitQueued = (prepared, floors, emittedAtMs) => Effect.suspend(() => {
489
+ if (state.status !== "open") {
490
+ return Effect.fail(error("journal_closed", "journal is closed"));
491
+ }
492
+ return Effect.flatMap(
493
+ Effect.fromResult(admitFromIndex(prepared)),
494
+ (indexed) => indexed !== void 0 ? Effect.succeed(indexed) : allocation.withPermit(Effect.fromResult(
495
+ Result.gen(function* () {
496
+ const { metaJson, payloadJson, validated } = prepared;
497
+ const key = sourceKey(validated.runId, validated.sourceId);
498
+ const nextSourceSeq = Math.max(
499
+ floors.sourceSeq,
500
+ state.sourceSequences.get(key) ?? 0
501
+ );
502
+ const sourceSeq = validated.sourceSeq ?? nextSourceSeq;
503
+ if (sourceSeq === Number.MAX_SAFE_INTEGER) {
504
+ return yield* Result.fail(
505
+ error(
506
+ "invalid_event",
507
+ "journal sequence is outside the allocatable safe integer range"
508
+ )
509
+ );
510
+ }
511
+ const nextSeq = Math.max(
512
+ floors.seq,
513
+ state.sequences.get(validated.runId) ?? 0
514
+ );
515
+ if (nextSeq === Number.MAX_SAFE_INTEGER) {
516
+ return yield* Result.fail(
517
+ error(
518
+ "invalid_event",
519
+ "journal sequence is outside the allocatable safe integer range"
520
+ )
521
+ );
522
+ }
523
+ const seq = nextSeq;
524
+ raiseSequenceFloor(validated.runId, seq);
525
+ state.sourceSequences.set(key, Math.max(nextSourceSeq, sourceSeq + 1));
526
+ const queued = {
527
+ runId: validated.runId,
528
+ seq,
529
+ eventId: (0, import_JournalEvent.makeEventId)(validated.runId, validated.sourceId, sourceSeq),
530
+ sourceId: validated.sourceId,
531
+ sourceSeq,
532
+ emittedAtMs,
533
+ eventType: validated.eventType,
534
+ payloadJson,
535
+ metaJson,
536
+ dedupe: validated.dedupe ?? "content"
537
+ };
538
+ let evicted;
539
+ if (Queue.sizeUnsafe(queue) >= options.capacity && options.overflow === "drop-oldest") {
540
+ const exit = Queue.takeUnsafe(queue);
541
+ if (exit === void 0 || !Exit.isSuccess(exit)) {
542
+ return yield* Result.fail(
543
+ error("journal_closed", "journal admission queue is unavailable")
544
+ );
545
+ }
546
+ evicted = exit.value;
547
+ const evictedIdentity = sourceEventKey(
548
+ evicted.runId,
549
+ evicted.sourceId,
550
+ evicted.sourceSeq
551
+ );
552
+ state.sourceEvents.delete(evictedIdentity);
553
+ state.pending = Math.max(0, state.pending - 1);
554
+ settleRunPending([evicted]);
555
+ }
556
+ const accepted = Queue.offerUnsafe(queue, queued);
557
+ if (!accepted) {
558
+ if (options.overflow === "reject") {
559
+ return yield* Result.fail(
560
+ error("queue_overflow", "journal admission queue is full")
561
+ );
562
+ }
563
+ return {
564
+ _tag: "Dropped",
565
+ seq,
566
+ sourceSeq,
567
+ policy: "drop-newest"
568
+ };
569
+ }
570
+ retain(sourceEventKey(validated.runId, validated.sourceId, sourceSeq), {
571
+ seq,
572
+ eventType: validated.eventType,
573
+ payloadJson,
574
+ metaJson,
575
+ status: "pending"
576
+ });
577
+ state.pending += 1;
578
+ admitRunPending(queued);
579
+ if (evicted !== void 0) {
580
+ return {
581
+ _tag: "Accepted",
582
+ seq,
583
+ sourceSeq,
584
+ evicted: {
585
+ policy: "drop-oldest",
586
+ count: 1
587
+ }
588
+ };
589
+ }
590
+ return {
591
+ _tag: "Accepted",
592
+ seq,
593
+ sourceSeq
594
+ };
595
+ })
596
+ ))
597
+ );
598
+ });
599
+ const queuedEmit = Effect.fn("Journal.emitLossy")(
600
+ (input) => Effect.annotateCurrentSpan({
601
+ runId: input.runId,
602
+ sourceId: input.sourceId,
603
+ eventType: input.eventType
604
+ }).pipe(
605
+ Effect.andThen(Clock.currentTimeMillis.pipe(Effect.map(Math.floor))),
606
+ Effect.flatMap(
607
+ (emittedAtMs) => Effect.flatMap(
608
+ withRunAdmission(
609
+ input.runId,
610
+ Effect.suspend(
611
+ () => Effect.fromResult(Result.gen(function* () {
612
+ const prepared = yield* prepare(input, emittedAtMs);
613
+ const indexed = yield* admitFromIndex(prepared);
614
+ return { prepared, indexed };
615
+ }))
616
+ )
617
+ ),
618
+ ({ prepared, indexed }) => (
619
+ // A cached duplicate needs neither allocation floor, including
620
+ // after startup when the identity index is warm but floors are
621
+ // cold. On a miss, release the permit before reading SQL, then
622
+ // recheck identity and allocation under a fresh admission.
623
+ indexed !== void 0 ? Effect.succeed(indexed) : Effect.flatMap(
624
+ ensureFloors(prepared.validated),
625
+ (floors) => withRunAdmission(prepared.validated.runId, admitQueued(prepared, floors, emittedAtMs))
626
+ )
627
+ )
628
+ )
629
+ ),
630
+ Effect.tap((receipt) => Metric.update(JournalMetrics.lossy[receipt._tag], 1))
631
+ )
632
+ );
633
+ const compactionFloor = (runId) => Effect.map(
634
+ sql`
635
+ SELECT MAX(seq) AS floor FROM flows_journal_checkpoints
636
+ WHERE run_id = ${runId} AND compacted_at_ms IS NOT NULL
637
+ `,
638
+ (rows) => {
639
+ const floor = rows[0]?.floor;
640
+ return floor === null || floor === void 0 ? void 0 : Number(floor);
641
+ }
642
+ );
643
+ const readPage = Effect.fn("Journal.entries")(
644
+ (pageOptions) => Effect.gen(function* () {
645
+ const options2 = {
646
+ ...pageOptions,
647
+ ...Array.isArray(pageOptions.eventTypes) ? { eventTypes: [...pageOptions.eventTypes] } : {}
648
+ };
649
+ yield* Effect.annotateCurrentSpan({
650
+ runId: options2.runId,
651
+ limit: options2.limit,
652
+ ...options2.eventTypes === void 0 ? {} : { eventTypes: options2.eventTypes },
653
+ ...options2.after === void 0 ? {} : { after: options2.after }
654
+ });
655
+ yield* Effect.fromResult(Result.mapError(
656
+ decodeEntriesOptions(options2),
657
+ (cause) => error("invalid_event", "entries options violate the journal read contract", cause)
658
+ ));
659
+ const after = options2.after ?? -1;
660
+ const rows = yield* (options2.eventTypes === void 0 ? sql`
661
+ SELECT run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
662
+ event_type, payload_json, meta_json
663
+ FROM flows_journal_events
664
+ WHERE run_id = ${options2.runId} AND seq > ${after}
665
+ ORDER BY seq ASC
666
+ LIMIT ${options2.limit + 1}
667
+ ` : sql`
668
+ SELECT run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
669
+ event_type, payload_json, meta_json
670
+ FROM flows_journal_events ${Dialect.indexHint(sql, "flows_journal_events_run_event_type_idx")}
671
+ WHERE run_id = ${options2.runId} AND ${sql.in("event_type", options2.eventTypes)} AND seq > ${after}
672
+ ORDER BY seq ASC
673
+ LIMIT ${options2.limit + 1}
674
+ `).pipe(
675
+ Effect.mapError((cause) => error("read_failed", "durable journal read failed", cause))
676
+ );
677
+ const floor = yield* compactionFloor(options2.runId).pipe(
678
+ Effect.mapError((cause) => error("read_failed", "durable journal read failed", cause))
679
+ );
680
+ if (floor !== void 0 && after < floor - 1) {
681
+ return yield* Effect.fail(
682
+ new import_Journal.JournalError({
683
+ code: "compacted",
684
+ message: `run ${options2.runId} is compacted through sequence ${floor}; resync from its checkpoint`,
685
+ checkpointSeq: floor
686
+ })
687
+ );
688
+ }
689
+ const page = rows.slice(0, options2.limit);
690
+ const entries = yield* Effect.forEach(page, decodeRow);
691
+ return {
692
+ entries,
693
+ hasMore: rows.length > options2.limit
694
+ };
695
+ })
696
+ );
697
+ const subscribeRun = (runId) => Effect.gen(function* () {
698
+ const wake = yield* PubSub.sliding(1);
699
+ const subscription = yield* PubSub.subscribe(wake);
700
+ yield* Effect.acquireRelease(
701
+ Effect.sync(() => {
702
+ const subscribers = wakes.get(runId) ?? /* @__PURE__ */ new Set();
703
+ subscribers.add(wake);
704
+ wakes.set(runId, subscribers);
705
+ }),
706
+ () => Effect.sync(() => {
707
+ const subscribers = wakes.get(runId);
708
+ subscribers?.delete(wake);
709
+ if (subscribers?.size === 0) {
710
+ wakes.delete(runId);
711
+ }
712
+ }).pipe(Effect.andThen(PubSub.shutdown(wake)))
713
+ );
714
+ return subscription;
715
+ });
716
+ const stream = (streamOptions) => Stream.unwrap(
717
+ Effect.fn("Journal.stream")(function* () {
718
+ yield* Effect.annotateCurrentSpan({
719
+ runId: streamOptions.runId,
720
+ ...streamOptions.afterSequence === void 0 ? {} : { afterSequence: streamOptions.afterSequence }
721
+ });
722
+ yield* Effect.fromResult(Result.mapError(
723
+ decodeStreamOptions(streamOptions),
724
+ (cause) => error("invalid_event", "stream options violate the journal read contract", cause)
725
+ ));
726
+ const wake = yield* subscribeRun(streamOptions.runId);
727
+ const reader = { cursor: streamOptions.afterSequence ?? -1 };
728
+ yield* Effect.acquireRelease(
729
+ Effect.sync(() => {
730
+ const registered = readers.get(streamOptions.runId) ?? /* @__PURE__ */ new Set();
731
+ registered.add(reader);
732
+ readers.set(streamOptions.runId, registered);
733
+ }),
734
+ () => Effect.sync(() => {
735
+ const registered = readers.get(streamOptions.runId);
736
+ registered?.delete(reader);
737
+ if (registered?.size === 0) {
738
+ readers.delete(streamOptions.runId);
739
+ }
740
+ })
741
+ );
742
+ const subscribedLossEpoch = state.lossEpoch;
743
+ const readPages = (initialCursor) => Stream.paginate(initialCursor, (cursor) => Effect.suspend(
744
+ () => state.sinkFailure !== void 0 && state.lossEpoch > subscribedLossEpoch ? Effect.fail(state.sinkFailure) : readPage({
745
+ runId: streamOptions.runId,
746
+ ...cursor < 0 ? {} : { after: cursor },
747
+ limit: readPageSize
748
+ })
749
+ ).pipe(
750
+ Effect.map((page) => {
751
+ const last = page.entries.at(-1);
752
+ if (last !== void 0) {
753
+ reader.cursor = last.seq;
754
+ }
755
+ return [
756
+ page.entries,
757
+ page.hasMore ? Option.some(reader.cursor) : Option.none()
758
+ ];
759
+ })
760
+ ));
761
+ const historical = readPages(reader.cursor);
762
+ const live = Stream.fromEffectRepeat(
763
+ Effect.raceFirst(PubSub.take(wake), Effect.sleep("1 second"))
764
+ ).pipe(Stream.flatMap(
765
+ () => readPages(reader.cursor)
766
+ ));
767
+ return Stream.concat(historical, live);
768
+ })()
769
+ );
770
+ const fenceGuard = (runId, owner) => consensus.guard(runId, owner).pipe(
771
+ Effect.mapError(
772
+ (cause) => cause.code === "fence_lost" ? error("fence_lost", cause.message) : error("sink_failed", "consensus guard failed", cause)
773
+ )
774
+ );
775
+ const selectExisting = (queued) => Effect.gen(function* () {
776
+ const existing = yield* sql`
777
+ SELECT run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
778
+ event_type, payload_json, meta_json, NULL AS content_hash
779
+ FROM flows_journal_events
780
+ WHERE event_id = ${queued.eventId}
781
+ OR (
782
+ run_id = ${queued.runId}
783
+ AND source_id = ${queued.sourceId}
784
+ AND source_seq = ${queued.sourceSeq}
785
+ )
786
+ UNION ALL
787
+ SELECT run_id, seq, event_id, source_id, source_seq, 0 AS emitted_at_ms,
788
+ '' AS event_type, '' AS payload_json, '' AS meta_json, content_hash
789
+ FROM flows_journal_dedup
790
+ WHERE event_id = ${queued.eventId}
791
+ OR (
792
+ run_id = ${queued.runId}
793
+ AND source_id = ${queued.sourceId}
794
+ AND source_seq = ${queued.sourceSeq}
795
+ )
796
+ ORDER BY seq ASC
797
+ LIMIT 1
798
+ `;
799
+ if (existing.length === 0) {
800
+ return void 0;
801
+ }
802
+ const row = existing[0];
803
+ if (queued.dedupe !== "identity" && (row.content_hash === null ? row.event_type !== queued.eventType || row.payload_json !== queued.payloadJson || row.meta_json !== queued.metaJson : row.content_hash !== (yield* contentFingerprint(queued.eventType, queued.payloadJson, queued.metaJson)))) {
804
+ return yield* Effect.fail(
805
+ error(
806
+ "idempotency_conflict",
807
+ `source event ${queued.sourceId}:${queued.sourceSeq} for run ${queued.runId} was reused with different content`
808
+ )
809
+ );
810
+ }
811
+ return {
812
+ entry: row.content_hash === null ? yield* decodeRow(row) : { seq: Number(row.seq) },
813
+ inserted: false
814
+ };
815
+ });
816
+ const insertOne = (queued, fence, enforceCompactionFloor = false) => Effect.gen(function* () {
817
+ if (fence._tag === "Owned") {
818
+ yield* fenceGuard(queued.runId, fence.owner);
819
+ } else if (!enforceCompactionFloor) {
820
+ const duplicate = yield* selectExisting(queued);
821
+ if (duplicate !== void 0) {
822
+ return duplicate;
823
+ }
824
+ }
825
+ const insert = enforceCompactionFloor ? sql`
826
+ INSERT INTO flows_journal_events (
827
+ run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
828
+ event_type, payload_json, meta_json
829
+ )
830
+ SELECT
831
+ ${queued.runId},
832
+ (SELECT ${Dialect.greatest(sql)}(${queued.seq}, COALESCE(MAX(seq), -1) + 1)
833
+ FROM flows_journal_events WHERE run_id = ${queued.runId}),
834
+ ${queued.eventId},
835
+ ${queued.sourceId},
836
+ ${queued.sourceSeq},
837
+ ${queued.emittedAtMs},
838
+ ${queued.eventType},
839
+ ${queued.payloadJson},
840
+ ${queued.metaJson}
841
+ WHERE NOT EXISTS (
842
+ SELECT 1 FROM flows_journal_checkpoints
843
+ WHERE run_id = ${queued.runId}
844
+ AND compacted_at_ms IS NOT NULL
845
+ AND seq >= ${queued.seq}
846
+ ) AND NOT EXISTS (
847
+ SELECT 1 FROM flows_journal_events
848
+ WHERE run_id = ${queued.runId} AND seq >= ${Number.MAX_SAFE_INTEGER - 1}
849
+ )
850
+ ON CONFLICT DO NOTHING
851
+ RETURNING run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
852
+ event_type, payload_json, meta_json
853
+ ` : sql`
854
+ INSERT INTO flows_journal_events (
855
+ run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
856
+ event_type, payload_json, meta_json
857
+ ) VALUES (
858
+ ${queued.runId},
859
+ ${queued.seq},
860
+ ${queued.eventId},
861
+ ${queued.sourceId},
862
+ ${queued.sourceSeq},
863
+ ${queued.emittedAtMs},
864
+ ${queued.eventType},
865
+ ${queued.payloadJson},
866
+ ${queued.metaJson}
867
+ )
868
+ ON CONFLICT DO NOTHING
869
+ RETURNING run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
870
+ event_type, payload_json, meta_json
871
+ `;
872
+ const inserted = yield* insert;
873
+ if (inserted.length > 0) {
874
+ return {
875
+ entry: yield* decodeRow(inserted[0]),
876
+ inserted: true
877
+ };
878
+ }
879
+ const racedDuplicate = yield* selectExisting(queued);
880
+ if (racedDuplicate !== void 0) {
881
+ return racedDuplicate;
882
+ }
883
+ {
884
+ const floor = yield* compactionFloor(queued.runId);
885
+ if (floor !== void 0 && queued.seq <= floor) {
886
+ return yield* Effect.fail(
887
+ new import_Journal.JournalError({
888
+ code: "compacted",
889
+ message: `queued sequence ${queued.seq} for run ${queued.runId} was dropped below compaction floor ${floor}`,
890
+ checkpointSeq: floor
891
+ })
892
+ );
893
+ }
894
+ }
895
+ return yield* Effect.fail(
896
+ error(
897
+ "invalid_event",
898
+ "journal sequence is outside the allocatable safe integer range"
899
+ )
900
+ );
901
+ });
902
+ const insertBatch = (batch) => Effect.gen(function* () {
903
+ const results = yield* Effect.forEach(
904
+ batch,
905
+ (queued) => Effect.map(Effect.result(insertOne(queued, unfenced, true)), (result) => ({ queued, result }))
906
+ );
907
+ const commits = [];
908
+ const losses = [];
909
+ for (const settled of results) {
910
+ if (Result.isSuccess(settled.result)) {
911
+ commits.push({ queued: settled.queued, commit: settled.result.success });
912
+ } else if (isJournalError(settled.result.failure)) {
913
+ losses.push({ queued: settled.queued, cause: settled.result.failure });
914
+ } else {
915
+ return yield* Effect.fail(settled.result.failure);
916
+ }
917
+ }
918
+ return { commits, losses };
919
+ });
920
+ const persistBatch = (batch) => writer.write(withRunPermits(
921
+ batch.map((queued) => queued.runId),
922
+ insertBatch(batch)
923
+ )).pipe(
924
+ // Every per-entry `JournalError` is settled inside the transaction
925
+ // above, so the only failure that escapes is the transaction itself:
926
+ // a database outage, which is what `sink_failed` names.
927
+ Effect.mapError((cause) => error("sink_failed", "journal sink failed", cause))
928
+ );
929
+ const publish = (commits) => Effect.forEach(
930
+ commits,
931
+ (commit) => {
932
+ if (!commit.inserted) {
933
+ return Effect.void;
934
+ }
935
+ return PubSub.publish(changes, freezePublished(commit.entry)).pipe(
936
+ Effect.andThen(
937
+ Effect.forEach(
938
+ wakes.get(commit.entry.runId) ?? [],
939
+ (wake) => PubSub.publish(wake, void 0),
940
+ { discard: true }
941
+ )
942
+ ),
943
+ Effect.asVoid
944
+ );
945
+ },
946
+ { discard: true }
947
+ );
948
+ const rememberCommitted = (queued, seq) => {
949
+ retain(sourceEventKey(queued.runId, queued.sourceId, queued.sourceSeq), {
950
+ seq,
951
+ eventType: queued.eventType,
952
+ payloadJson: queued.payloadJson,
953
+ metaJson: queued.metaJson,
954
+ status: "committed"
955
+ });
956
+ raiseSequenceFloor(queued.runId, seq);
957
+ raiseSourceSequenceFloor(queued.runId, queued.sourceId, queued.sourceSeq);
958
+ };
959
+ const nextDurable = (column, runId, sourceId) => Effect.map(
960
+ column === "seq" ? sql`
961
+ SELECT MAX(seq) + 1 AS next FROM flows_journal_events WHERE run_id = ${runId}
962
+ ` : sql`
963
+ SELECT MAX(source_seq) + 1 AS next FROM (
964
+ SELECT MAX(source_seq) AS source_seq FROM flows_journal_events
965
+ WHERE run_id = ${runId} AND source_id = ${sourceId}
966
+ UNION ALL
967
+ SELECT MAX(source_seq) AS source_seq FROM flows_journal_dedup
968
+ WHERE run_id = ${runId} AND source_id = ${sourceId}
969
+ )
970
+ `,
971
+ (rows) => Number(rows[0]?.next ?? 0)
972
+ );
973
+ const ensureFloors = (input) => Effect.suspend(() => {
974
+ const runId = input.runId;
975
+ const key = sourceKey(runId, input.sourceId);
976
+ const seq = state.sequences.get(runId);
977
+ const sourceSeq = input.sourceSeq === void 0 ? state.sourceSequences.get(key) : 0;
978
+ if (seq !== void 0 && sourceSeq !== void 0) {
979
+ return Effect.succeed({ seq, sourceSeq });
980
+ }
981
+ return Effect.all({
982
+ seq: seq === void 0 ? nextDurable("seq", runId, void 0) : Effect.succeed(seq),
983
+ sourceSeq: sourceSeq === void 0 ? nextDurable("source_seq", runId, input.sourceId) : Effect.succeed(sourceSeq)
984
+ }).pipe(
985
+ Effect.mapError((cause) => error("sink_failed", "could not read journal allocation floor", cause)),
986
+ Effect.flatMap((floors) => {
987
+ if (!Number.isSafeInteger(floors.seq) || floors.seq < 0 || !Number.isSafeInteger(floors.sourceSeq) || floors.sourceSeq < 0) {
988
+ return Effect.fail(
989
+ error("invalid_event", "journal sequence is outside the allocatable safe integer range")
990
+ );
991
+ }
992
+ return Effect.succeed(floors);
993
+ })
994
+ );
995
+ });
996
+ const settleCommit = (queued, commit, restoreMaintenance) => Effect.gen(function* () {
997
+ const mandatory = Effect.sync(() => rememberCommitted(queued, commit.entry.seq)).pipe(
998
+ Effect.andThen(publish([commit]))
999
+ );
1000
+ const maintenance = restoreMaintenance(noteCommitted(queued.runId, commit.inserted ? 1 : 0));
1001
+ const enclosing = yield* Effect.serviceOption(sql.transactionService);
1002
+ if (Option.isNone(enclosing)) {
1003
+ yield* mandatory;
1004
+ return maintenance;
1005
+ }
1006
+ yield* (0, import_DurableWriter.afterCommit)(mandatory.pipe(Effect.andThen(maintenance)), sql);
1007
+ return Effect.void;
1008
+ });
1009
+ const transact = (effect) => writer.write(effect).pipe(
1010
+ Effect.catchIf(
1011
+ (cause) => cause instanceof import_DurableWriter.DatabaseError,
1012
+ (cause) => Effect.fail(error("sink_failed", "journal transaction failed", cause))
1013
+ )
1014
+ );
1015
+ const writeDurable = (input, fence) => Effect.uninterruptibleMask(
1016
+ (restoreMaintenance) => restoreMaintenance(Effect.gen(function* () {
1017
+ yield* Effect.annotateCurrentSpan({
1018
+ runId: input.runId,
1019
+ sourceId: input.sourceId,
1020
+ eventType: input.eventType
1021
+ });
1022
+ const emittedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1023
+ const prepared = yield* Effect.fromResult(prepare(input, emittedAtMs));
1024
+ const committed = yield* Effect.gen(function* () {
1025
+ const { metaJson, payloadJson, validated } = prepared;
1026
+ return yield* Effect.uninterruptibleMask(
1027
+ (restore) => restore(writer.write(withActiveRunWrite(
1028
+ prepared.validated.runId,
1029
+ Effect.gen(function* () {
1030
+ const durableSourceSeq = validated.sourceSeq === void 0 ? yield* nextDurable("source_seq", validated.runId, validated.sourceId) : 0;
1031
+ const durableSeq = yield* nextDurable("seq", validated.runId, void 0);
1032
+ const reserved = yield* allocation.withPermit(Effect.fromResult(Result.gen(function* () {
1033
+ const key = sourceKey(validated.runId, validated.sourceId);
1034
+ const sourceSeq2 = validated.sourceSeq ?? Math.max(
1035
+ durableSourceSeq,
1036
+ state.sourceSequences.get(key) ?? 0
1037
+ );
1038
+ if (!Number.isSafeInteger(sourceSeq2) || sourceSeq2 < 0 || sourceSeq2 === Number.MAX_SAFE_INTEGER) {
1039
+ return yield* Result.fail(
1040
+ error("invalid_event", "journal sequence is outside the allocatable safe integer range")
1041
+ );
1042
+ }
1043
+ const seq2 = Math.max(
1044
+ durableSeq,
1045
+ state.sequences.get(validated.runId) ?? 0
1046
+ );
1047
+ if (!Number.isSafeInteger(seq2) || seq2 === Number.MAX_SAFE_INTEGER) {
1048
+ return yield* Result.fail(
1049
+ error("invalid_event", "journal sequence is outside the allocatable safe integer range")
1050
+ );
1051
+ }
1052
+ raiseSequenceFloor(validated.runId, seq2);
1053
+ raiseSourceSequenceFloor(validated.runId, validated.sourceId, sourceSeq2);
1054
+ return { seq: seq2, sourceSeq: sourceSeq2 };
1055
+ })));
1056
+ const { seq, sourceSeq } = reserved;
1057
+ const queued = {
1058
+ runId: validated.runId,
1059
+ seq,
1060
+ eventId: (0, import_JournalEvent.makeEventId)(validated.runId, validated.sourceId, sourceSeq),
1061
+ sourceId: validated.sourceId,
1062
+ sourceSeq,
1063
+ emittedAtMs,
1064
+ eventType: validated.eventType,
1065
+ payloadJson,
1066
+ metaJson,
1067
+ dedupe: validated.dedupe ?? "content"
1068
+ };
1069
+ const commit = yield* insertOne(queued, fence);
1070
+ return { commit, queued, sourceSeq };
1071
+ })
1072
+ ))).pipe(
1073
+ /**
1074
+ * `writer.write` is a retrying transaction: its body replays on
1075
+ * `SQLITE_BUSY(_SNAPSHOT)` and can still abort at COMMIT after the
1076
+ * body succeeded. Cache mutation and publication therefore happen
1077
+ * strictly after the transaction returns, so subscribers never
1078
+ * observe a rolled-back entry and a replayed body never publishes
1079
+ * twice. Mirrors the queued path, which publishes in a `.tap`
1080
+ * outside `persistBatch`. Only the transaction is restored: an
1081
+ * interruption that lands after it returns waits for the index
1082
+ * update and publication, so a committed row always reaches
1083
+ * `changes`.
1084
+ *
1085
+ * Under `transact` "after the transaction returns" is not yet
1086
+ * "after COMMIT": this write is a savepoint of the caller's
1087
+ * transaction, so `settleCommit` parks both effects until the
1088
+ * outermost transaction commits. The active-write reference
1089
+ * is released before that outer commit, which lets automatic
1090
+ * compaction drain the run before its maintenance transaction.
1091
+ */
1092
+ Effect.flatMap(
1093
+ (written) => Effect.gen(function* () {
1094
+ const maintenance = yield* settleCommit(written.queued, written.commit, restoreMaintenance);
1095
+ const receipt = written.commit.inserted ? { _tag: "Accepted", seq: written.commit.entry.seq, sourceSeq: written.sourceSeq } : {
1096
+ _tag: "Duplicate",
1097
+ seq: written.commit.entry.seq,
1098
+ sourceSeq: written.sourceSeq,
1099
+ status: "committed"
1100
+ };
1101
+ yield* Metric.update(JournalMetrics.durable[receipt._tag], 1);
1102
+ return { maintenance, receipt };
1103
+ })
1104
+ )
1105
+ )
1106
+ );
1107
+ }).pipe(
1108
+ Effect.mapError(
1109
+ (cause) => isJournalError(cause) ? cause : error("sink_failed", "durable journal write failed", cause)
1110
+ )
1111
+ );
1112
+ yield* committed.maintenance;
1113
+ return committed.receipt;
1114
+ }))
1115
+ );
1116
+ const emitDurable = Effect.fn("Journal.emitDurable")(
1117
+ (input, owner) => Effect.flatMap(
1118
+ Effect.fromResult(requireFence(owner, "emitDurable")),
1119
+ (fence) => writeDurable(input, fence)
1120
+ )
1121
+ );
1122
+ const emitDurableUnfenced = Effect.fn("Journal.emitDurableUnfenced")((input) => writeDurable(input, unfenced));
1123
+ const emitLossy = queuedEmit;
1124
+ const checkpointInternal = (checkpointOptions, fence) => Effect.gen(function* () {
1125
+ yield* Effect.annotateCurrentSpan({
1126
+ runId: checkpointOptions.runId,
1127
+ seq: checkpointOptions.seq
1128
+ });
1129
+ yield* Effect.fromResult(Result.mapError(
1130
+ decodeCheckpointOptions(checkpointOptions),
1131
+ (cause) => error("invalid_event", "checkpoint options violate the journal contract", cause)
1132
+ ));
1133
+ const stateJson = yield* Effect.fromResult(encodeJson(checkpointOptions.state, "state"));
1134
+ const receiptState = yield* Schema.decodeUnknownEffect(UnknownFromJsonString)(stateJson).pipe(
1135
+ Effect.mapError(
1136
+ /* v8 ignore next -- encodeJson produced these valid JSON bytes immediately above */
1137
+ (cause) => error("decode_failed", "could not decode persisted checkpoint state", cause)
1138
+ )
1139
+ );
1140
+ const createdAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1141
+ return yield* writer.write(Effect.gen(function* () {
1142
+ if (fence._tag === "Owned") {
1143
+ yield* fenceGuard(checkpointOptions.runId, fence.owner);
1144
+ }
1145
+ const target = yield* sql`
1146
+ SELECT 1 AS ok FROM flows_journal_events
1147
+ WHERE run_id = ${checkpointOptions.runId} AND seq = ${checkpointOptions.seq}
1148
+ `;
1149
+ if (target.length === 0) {
1150
+ return yield* Effect.fail(error(
1151
+ "checkpoint_invalid",
1152
+ `checkpoint sequence ${checkpointOptions.seq} names no committed entry of run ${checkpointOptions.runId}`
1153
+ ));
1154
+ }
1155
+ const floor = yield* compactionFloor(checkpointOptions.runId);
1156
+ if (floor !== void 0 && checkpointOptions.seq <= floor) {
1157
+ return yield* Effect.fail(
1158
+ new import_Journal.JournalError({
1159
+ code: "checkpoint_invalid",
1160
+ message: `run ${checkpointOptions.runId} is already compacted through sequence ${floor}`,
1161
+ checkpointSeq: floor
1162
+ })
1163
+ );
1164
+ }
1165
+ yield* sql`
1166
+ INSERT INTO flows_journal_checkpoints (run_id, seq, state_json, created_at_ms)
1167
+ VALUES (${checkpointOptions.runId}, ${checkpointOptions.seq}, ${stateJson}, ${createdAtMs})
1168
+ ON CONFLICT (run_id, seq) DO UPDATE SET
1169
+ state_json = excluded.state_json,
1170
+ created_at_ms = excluded.created_at_ms
1171
+ `;
1172
+ return new import_Journal.Checkpoint({
1173
+ runId: checkpointOptions.runId,
1174
+ seq: checkpointOptions.seq,
1175
+ state: receiptState,
1176
+ createdAtMs,
1177
+ compactedAtMs: null
1178
+ });
1179
+ })).pipe(
1180
+ Effect.mapError(
1181
+ (cause) => isJournalError(cause) ? cause : error("sink_failed", "durable checkpoint write failed", cause)
1182
+ )
1183
+ );
1184
+ });
1185
+ const checkpoint = Effect.fn("Journal.checkpoint")(
1186
+ (checkpointOptions, owner) => Effect.flatMap(
1187
+ Effect.fromResult(requireFence(owner, "checkpoint")),
1188
+ (fence) => checkpointInternal(checkpointOptions, fence)
1189
+ )
1190
+ );
1191
+ const latestCheckpoint = Effect.fn("Journal.latestCheckpoint")(
1192
+ (runId) => Effect.gen(function* () {
1193
+ yield* Effect.annotateCurrentSpan({ runId });
1194
+ yield* Effect.fromResult(Result.mapError(
1195
+ decodeRunId(runId),
1196
+ (cause) => error("invalid_event", "runId violates the journal identifier contract", cause)
1197
+ ));
1198
+ const rows = yield* sql`
1199
+ SELECT run_id, seq, state_json, created_at_ms, compacted_at_ms
1200
+ FROM flows_journal_checkpoints
1201
+ WHERE run_id = ${runId}
1202
+ ORDER BY seq DESC
1203
+ LIMIT 1
1204
+ `.pipe(Effect.mapError((cause) => error("read_failed", "durable checkpoint read failed", cause)));
1205
+ const row = rows[0];
1206
+ return row === void 0 ? Option.none() : Option.some(yield* decodeCheckpointRow(row));
1207
+ })
1208
+ );
1209
+ const compactInternal = (compactOptions, fence) => Effect.gen(function* () {
1210
+ yield* Effect.annotateCurrentSpan({
1211
+ runId: compactOptions.runId,
1212
+ ...compactOptions.upTo === void 0 ? {} : { upTo: compactOptions.upTo }
1213
+ });
1214
+ yield* Effect.fromResult(Result.mapError(
1215
+ decodeCompactOptions(compactOptions),
1216
+ (cause) => error("invalid_event", "compact options violate the journal contract", cause)
1217
+ ));
1218
+ const compactedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor));
1219
+ return yield* withCompactionBarrier(
1220
+ compactOptions.runId,
1221
+ Effect.gen(function* () {
1222
+ if (fence._tag === "Owned") {
1223
+ yield* fenceGuard(compactOptions.runId, fence.owner);
1224
+ }
1225
+ const upTo = compactOptions.upTo;
1226
+ const rows = upTo === void 0 ? yield* sql`
1227
+ SELECT run_id, seq, state_json, created_at_ms, compacted_at_ms
1228
+ FROM flows_journal_checkpoints
1229
+ WHERE run_id = ${compactOptions.runId}
1230
+ ORDER BY seq DESC
1231
+ LIMIT 1
1232
+ ` : yield* sql`
1233
+ SELECT run_id, seq, state_json, created_at_ms, compacted_at_ms
1234
+ FROM flows_journal_checkpoints
1235
+ WHERE run_id = ${compactOptions.runId} AND seq = ${upTo}
1236
+ `;
1237
+ const row = rows[0];
1238
+ if (row === void 0) {
1239
+ return yield* Effect.fail(error(
1240
+ "checkpoint_invalid",
1241
+ `run ${compactOptions.runId} has no checkpoint${upTo === void 0 ? "" : ` at sequence ${upTo}`} to compact to`
1242
+ ));
1243
+ }
1244
+ const checkpointSeq = Number(row.seq);
1245
+ if (row.compacted_at_ms !== null) {
1246
+ return { runId: compactOptions.runId, checkpointSeq, deleted: 0 };
1247
+ }
1248
+ for (const reader of readers.get(compactOptions.runId) ?? []) {
1249
+ if (reader.cursor < checkpointSeq - 1) {
1250
+ return yield* Effect.fail(
1251
+ new import_Journal.JournalError({
1252
+ code: "reader_behind",
1253
+ message: `a live stream of run ${compactOptions.runId} still needs sequences below checkpoint ${checkpointSeq}`,
1254
+ checkpointSeq
1255
+ })
1256
+ );
1257
+ }
1258
+ }
1259
+ const doomed = yield* sql`
1260
+ SELECT COUNT(*) AS total FROM flows_journal_events
1261
+ WHERE run_id = ${compactOptions.runId} AND seq < ${checkpointSeq}
1262
+ `;
1263
+ let after = -1;
1264
+ while (true) {
1265
+ const retiring = yield* sql`
1266
+ SELECT run_id, seq, event_id, source_id, source_seq, emitted_at_ms,
1267
+ event_type, payload_json, meta_json
1268
+ FROM flows_journal_events
1269
+ WHERE run_id = ${compactOptions.runId} AND seq > ${after} AND seq < ${checkpointSeq}
1270
+ ORDER BY seq ASC LIMIT 256
1271
+ `;
1272
+ if (retiring.length === 0) break;
1273
+ for (const entry of retiring) {
1274
+ const fingerprint = yield* contentFingerprint(entry.event_type, entry.payload_json, entry.meta_json);
1275
+ yield* sql`
1276
+ INSERT INTO flows_journal_dedup (run_id, source_id, source_seq, event_id, seq, content_hash)
1277
+ VALUES (${entry.run_id}, ${entry.source_id}, ${entry.source_seq}, ${entry.event_id},
1278
+ ${entry.seq}, ${fingerprint})
1279
+ `;
1280
+ }
1281
+ after = Number(retiring[retiring.length - 1].seq);
1282
+ }
1283
+ yield* sql`
1284
+ DELETE FROM flows_journal_events
1285
+ WHERE run_id = ${compactOptions.runId} AND seq < ${checkpointSeq}
1286
+ `;
1287
+ yield* sql`
1288
+ DELETE FROM flows_journal_checkpoints
1289
+ WHERE run_id = ${compactOptions.runId} AND seq < ${checkpointSeq}
1290
+ `;
1291
+ yield* sql`
1292
+ UPDATE flows_journal_checkpoints
1293
+ SET compacted_at_ms = ${compactedAtMs}
1294
+ WHERE run_id = ${compactOptions.runId} AND seq = ${checkpointSeq}
1295
+ `;
1296
+ return {
1297
+ runId: compactOptions.runId,
1298
+ checkpointSeq,
1299
+ deleted: Number(doomed[0]?.total ?? 0)
1300
+ };
1301
+ })
1302
+ ).pipe(
1303
+ Effect.mapError(
1304
+ (cause) => isJournalError(cause) ? cause : error("sink_failed", "journal compaction failed", cause)
1305
+ )
1306
+ );
1307
+ });
1308
+ const compact = Effect.fn("Journal.compact")(
1309
+ (compactOptions, owner) => Effect.flatMap(
1310
+ Effect.fromResult(requireFence(owner, "compact")),
1311
+ (fence) => compactInternal(compactOptions, fence)
1312
+ )
1313
+ );
1314
+ const compactionPolicy = options.compaction;
1315
+ const countEntries = (runId) => Effect.map(
1316
+ sql`
1317
+ SELECT COUNT(*) AS total FROM flows_journal_events WHERE run_id = ${runId}
1318
+ `,
1319
+ (rows) => Number(rows[0]?.total ?? 0)
1320
+ );
1321
+ const policyCompact = (policy, runId) => Effect.gen(function* () {
1322
+ const tail = yield* sql`
1323
+ SELECT MAX(seq) AS last FROM flows_journal_events WHERE run_id = ${runId}
1324
+ `;
1325
+ const last = tail[0]?.last;
1326
+ if (last === null || last === void 0) {
1327
+ return;
1328
+ }
1329
+ const upTo = Number(last);
1330
+ const captured = yield* Effect.timeoutOrElse(policy.capture(runId, upTo), {
1331
+ duration: compactionCaptureTimeout,
1332
+ orElse: () => Effect.fail(
1333
+ new import_JournalInternalFault.JournalInternalFault({
1334
+ code: "compaction_capture_timeout",
1335
+ message: `journal compaction capture for run ${runId} exceeded ${compactionCaptureTimeout}`
1336
+ })
1337
+ )
1338
+ });
1339
+ yield* checkpointInternal({ runId, seq: upTo, state: captured }, unfenced);
1340
+ yield* compactInternal({ runId, upTo }, unfenced);
1341
+ compactionCounts.set(runId, yield* countEntries(runId));
1342
+ }).pipe(
1343
+ Effect.catchCause(
1344
+ (cause) => Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : Effect.sync(() => {
1345
+ compactionCounts.set(runId, 0);
1346
+ }).pipe(
1347
+ Effect.andThen(
1348
+ Effect.logWarning("journal auto-compaction failed; retrying after the next threshold", cause)
1349
+ )
1350
+ )
1351
+ )
1352
+ );
1353
+ const noteCommitted = (runId, committed, background = false) => {
1354
+ const policy = compactionPolicy;
1355
+ if (policy === void 0 || committed <= 0) {
1356
+ return Effect.void;
1357
+ }
1358
+ return Effect.uninterruptibleMask(
1359
+ (restore) => withRunBarrier(runId, (barrier) => restore(barrier.maintenance.withPermit(
1360
+ Effect.gen(function* () {
1361
+ if (compactingRuns.has(runId)) {
1362
+ return false;
1363
+ }
1364
+ const known = compactionCounts.get(runId);
1365
+ const current = known === void 0 ? yield* countEntries(runId) : known + committed;
1366
+ compactionCounts.set(runId, current);
1367
+ if (current < policy.entryThreshold) {
1368
+ return false;
1369
+ }
1370
+ compactingRuns.add(runId);
1371
+ pendingMaintenance += 1;
1372
+ return true;
1373
+ })
1374
+ )).pipe(
1375
+ Effect.flatMap((triggered) => {
1376
+ if (!triggered) return Effect.void;
1377
+ const attempt = policyCompact(policy, runId).pipe(Effect.ensuring(Effect.sync(() => {
1378
+ compactingRuns.delete(runId);
1379
+ pendingMaintenance -= 1;
1380
+ if (state.pending === 0 && pendingMaintenance === 0) completeFlushWaiters(Effect.void);
1381
+ })));
1382
+ return background ? Effect.asVoid(Effect.forkIn(Effect.interruptible(attempt), maintenanceScope)) : restore(attempt);
1383
+ }),
1384
+ Effect.catchCause(
1385
+ (cause) => Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : Effect.logWarning("journal compaction policy bookkeeping failed", cause)
1386
+ )
1387
+ ))
1388
+ );
1389
+ };
1390
+ const recordCommits = (commits) => {
1391
+ for (const { commit, queued } of commits) {
1392
+ rememberCommitted(queued, commit.entry.seq);
1393
+ }
1394
+ };
1395
+ const settle = (count) => {
1396
+ state.pending = Math.max(0, state.pending - count);
1397
+ if (state.pending === 0 && pendingMaintenance === 0) {
1398
+ completeFlushWaiters(Effect.void);
1399
+ }
1400
+ };
1401
+ const reportLoss = (cause, batch) => {
1402
+ for (const queued of batch) {
1403
+ const identity = sourceEventKey(queued.runId, queued.sourceId, queued.sourceSeq);
1404
+ state.sourceEvents.delete(identity);
1405
+ }
1406
+ state.sinkFailure = cause;
1407
+ state.lossEpoch += 1;
1408
+ state.pending = Math.max(0, state.pending - batch.length);
1409
+ if (state.flushWaiters.size > 0) {
1410
+ state.flushedLossEpoch = state.lossEpoch;
1411
+ }
1412
+ completeFlushWaiters(Effect.fail(cause));
1413
+ for (const subscribers of wakes.values()) {
1414
+ for (const wake of subscribers) {
1415
+ PubSub.publishUnsafe(wake, void 0);
1416
+ }
1417
+ }
1418
+ };
1419
+ const observeLoss = (cause, batch) => Effect.andThen(
1420
+ Metric.update(JournalMetrics.lost(cause.code), batch.length),
1421
+ Effect.logWarning(
1422
+ `journal lost ${batch.length} lossy entries (${cause.code}) for runs ${[...new Set(batch.map((queued) => queued.runId))].join(", ")}`,
1423
+ cause
1424
+ )
1425
+ );
1426
+ const failSink = (cause, batch) => {
1427
+ settleRunPending(batch);
1428
+ reportLoss(cause, batch);
1429
+ };
1430
+ const writeBatch = Queue.takeBetween(queue, 1, batchSize).pipe(
1431
+ Effect.flatMap(
1432
+ (batch) => persistBatch(batch).pipe(
1433
+ Effect.tap((outcome) => Effect.sync(() => recordCommits(outcome.commits))),
1434
+ Effect.tap((outcome) => publish(outcome.commits.map(({ commit }) => commit))),
1435
+ // The transaction has committed and publication is complete. Drop
1436
+ // the barrier counts before policy compaction takes this run's
1437
+ // permit. Maintenance has its own global flush count.
1438
+ Effect.tap(() => Effect.sync(() => settleRunPending(batch))),
1439
+ // Register maintenance BEFORE settling the batch, so flush cannot
1440
+ // observe a gap between queued work and its policy attempt.
1441
+ Effect.tap((outcome) => {
1442
+ const perRun = /* @__PURE__ */ new Map();
1443
+ outcome.commits.forEach(({ commit, queued }) => {
1444
+ if (!commit.inserted) {
1445
+ return;
1446
+ }
1447
+ perRun.set(queued.runId, (perRun.get(queued.runId) ?? 0) + 1);
1448
+ });
1449
+ return Effect.forEach(perRun, ([runId, committed]) => noteCommitted(runId, committed, true), {
1450
+ discard: true
1451
+ });
1452
+ }),
1453
+ Effect.tap(
1454
+ (outcome) => (
1455
+ // Observed before it is reported, so a flush the report wakes
1456
+ // already sees the loss in the log and the registry.
1457
+ Effect.uninterruptible(Effect.andThen(
1458
+ Effect.forEach(outcome.losses, (loss) => observeLoss(loss.cause, [loss.queued]), { discard: true }),
1459
+ Effect.sync(() => {
1460
+ for (const loss of outcome.losses) {
1461
+ reportLoss(loss.cause, [loss.queued]);
1462
+ }
1463
+ settle(outcome.commits.length);
1464
+ })
1465
+ ))
1466
+ )
1467
+ ),
1468
+ Effect.catch(
1469
+ (cause) => Effect.uninterruptible(
1470
+ Effect.andThen(observeLoss(cause, batch), Effect.sync(() => failSink(cause, batch)))
1471
+ )
1472
+ ),
1473
+ // Defects only: an interruption is scope closure, and it must end
1474
+ // the writer rather than be reported as a lost batch.
1475
+ Effect.catchDefect((defect) => {
1476
+ const cause = error("sink_failed", "journal writer failed", Cause.die(defect));
1477
+ return Effect.uninterruptible(
1478
+ Effect.andThen(observeLoss(cause, batch), Effect.sync(() => failSink(cause, batch)))
1479
+ );
1480
+ })
1481
+ )
1482
+ )
1483
+ );
1484
+ const drain = Effect.forever(writeBatch);
1485
+ yield* Effect.forkScoped(drain);
1486
+ yield* Effect.addFinalizer(
1487
+ () => Effect.gen(function* () {
1488
+ yield* Effect.sync(() => {
1489
+ state.status = "closing";
1490
+ });
1491
+ yield* flushInternal.pipe(
1492
+ Effect.catch((cause) => Effect.logWarning("journal final flush failed while closing", cause))
1493
+ );
1494
+ yield* Effect.sync(() => {
1495
+ state.status = "closed";
1496
+ });
1497
+ yield* Queue.shutdown(queue);
1498
+ yield* PubSub.shutdown(changes);
1499
+ wakes.clear();
1500
+ })
1501
+ );
1502
+ const project = (projection, streamOptions) => Stream.unwrap(
1503
+ Effect.fn("Journal.project")(
1504
+ (activeProjection, activeOptions) => Effect.annotateCurrentSpan({
1505
+ projection: activeProjection.name,
1506
+ runId: activeOptions.runId,
1507
+ ...activeOptions.afterSequence === void 0 ? {} : { afterSequence: activeOptions.afterSequence }
1508
+ }).pipe(Effect.andThen(Effect.succeed(
1509
+ stream(activeOptions).pipe(
1510
+ Stream.scanEffect(activeProjection.initial, (state2, entry) => Effect.suspend(() => activeProjection.reduce(state2, entry)).pipe(
1511
+ Effect.catchCause(
1512
+ (cause) => Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : Effect.fail(
1513
+ error(
1514
+ "projection_failed",
1515
+ `projection ${activeProjection.name} failed`,
1516
+ cause
1517
+ )
1518
+ )
1519
+ )
1520
+ ))
1521
+ )
1522
+ )))
1523
+ )(projection, streamOptions)
1524
+ );
1525
+ yield* JournalGeneration.onTruncate((runIds) => {
1526
+ for (const runId of runIds) {
1527
+ state.sequences.delete(runId);
1528
+ const prefix = `${runId.length}:${runId}`;
1529
+ for (const key of state.sourceSequences.keys()) {
1530
+ if (key.startsWith(prefix)) state.sourceSequences.delete(key);
1531
+ }
1532
+ for (const key of state.sourceEvents.keys()) {
1533
+ if (key.startsWith(prefix)) state.sourceEvents.delete(key);
1534
+ }
1535
+ }
1536
+ });
1537
+ return (0, import_Journal.make)({
1538
+ generation: (runId) => sql`
1539
+ SELECT generation, after_seq AS "afterSeq" FROM flows_journal_generations WHERE run_id = ${runId}
1540
+ `.pipe(
1541
+ Effect.map((rows) => rows[0] ?? { generation: 0, afterSeq: -1 }),
1542
+ Effect.mapError((cause) => error("read_failed", "could not read journal generation", cause))
1543
+ ),
1544
+ emitLossy,
1545
+ emitDurable,
1546
+ emitDurableUnfenced,
1547
+ transact,
1548
+ whenCommitted: (update) => Effect.flatMap(
1549
+ Effect.serviceOption(sql.transactionService),
1550
+ (transaction) => Option.isNone(transaction) ? Effect.as(update, true) : (0, import_DurableWriter.afterCommit)(update, sql)
1551
+ ),
1552
+ stream,
1553
+ entries: readPage,
1554
+ changes: PubSub.subscribe(changes),
1555
+ project,
1556
+ flush: Effect.fn("Journal.flush")(
1557
+ () => Effect.suspend(() => Effect.annotateCurrentSpan({ pending: state.pending })).pipe(
1558
+ Effect.andThen(flushInternal)
1559
+ )
1560
+ )(),
1561
+ checkpoint,
1562
+ latestCheckpoint,
1563
+ compact
1564
+ });
1565
+ });
1566
+ const layerWith = (options) => Layer.effect(import_Journal.Journal, make(options));
1567
+ const layer = (options) => layerWith(options).pipe(Layer.provide(SqlConsensus.layer));
1568
+ //# sourceMappingURL=SqlJournal.js.map