@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,139 @@
1
+ ---
2
+ title: "Read and follow a run"
3
+ description: "Choose between entries, stream, and changes; page a long history; and know which reader reports a loss and which one drops entries silently."
4
+ sidebar:
5
+ order: 3
6
+ ---
7
+
8
+ The journal has three reads, and they differ in exactly one dimension: what
9
+ happens when the reader cannot keep up. Pick by that.
10
+
11
+ | Read | Shape | Behind a fast writer |
12
+ | --------- | ----------------------------------------- | ----------------------------- |
13
+ | `entries` | one page, in sequence order | nothing is lost; you page |
14
+ | `stream` | replay then follow, per run | lossless; reports a sink loss |
15
+ | `changes` | live tail, every run this process commits | silently drops entries |
16
+
17
+ ## Page a history with entries
18
+
19
+ `entries` reads one page in sequence order and reports whether another follows.
20
+ Use it for a poller, an export, or any bounded read:
21
+
22
+ ```ts
23
+ import { Journal, JournalEvent } from "@smthrs/journal"
24
+ import * as Effect from "effect/Effect"
25
+
26
+ const readAll = (runId: JournalEvent.RunId) =>
27
+ Effect.gen(function*() {
28
+ const journal = yield* Journal.Journal
29
+ let after: JournalEvent.Seq | undefined = undefined
30
+
31
+ for (;;) {
32
+ const page: Journal.EntriesPage = after === undefined
33
+ ? yield* journal.entries({ runId, limit: 500 })
34
+ : yield* journal.entries({ runId, after, limit: 500 })
35
+
36
+ for (const entry of page.entries) console.log(entry.seq, entry.eventType)
37
+
38
+ const last = page.entries.at(-1)
39
+ if (!page.hasMore || last === undefined) break
40
+ after = last.seq
41
+ }
42
+ })
43
+ ```
44
+
45
+ `limit` is capped at `Journal.maxEntriesLimit` (10,000). A page is decoded into
46
+ memory in full before the caller sees its first entry, so an unbounded limit
47
+ would let one call materialize a whole run.
48
+
49
+ For a nonempty list of at most 64 exact event types, use the optional `eventTypes` field on `entries`:
50
+
51
+ ```ts
52
+ const finished = yield * journal.entries({ runId, eventTypes: ["build.finished", "build.failed"], limit: 100 })
53
+ ```
54
+
55
+ This is a new public query option. Filtering happens in SQL before the page
56
+ limit, using the journal's `(run_id, event_type, seq)` index. `SqlJournal` is the
57
+ existing SQLite-specific adapter and explicitly selects that index: SQLite can
58
+ otherwise choose the run/sequence index for an `IN` query and scan unrelated
59
+ history. Multiple types may sort matching rows by sequence. The dialect-specific
60
+ hint stays inside `SqlJournal`; `Journal.Service` remains platform-independent. `after` remains the
61
+ canonical run sequence, and `hasMore` counts only matching events. Gaps between
62
+ matching sequences are expected. Duplicate types do not duplicate entries. No match returns an empty final page. Omitting
63
+ the option preserves the existing full-run read. The event type uses the same
64
+ bounded identifier schema as journal emission.
65
+
66
+ Filtering does not restore compacted evidence: a cursor below the run's
67
+ compaction floor still fails with `compacted`, even when no matching row remains.
68
+ Apply journal migration `0005_run_event_type` before using the updated service.
69
+ Custom `Journal.Service` adapters must implement the filter before a caller
70
+ relies on it; an older adapter may silently ignore an unknown option.
71
+
72
+ ## Follow a run with stream
73
+
74
+ `stream` replays the run's durable history from `afterSequence`, then follows
75
+ its committed tail. It is the lossless reader, and the one to use wherever a
76
+ missing entry would be a correctness bug:
77
+
78
+ ```ts
79
+ import * as Stream from "effect/Stream"
80
+
81
+ const follow = Effect.gen(function*() {
82
+ const journal = yield* Journal.Journal
83
+ yield* journal.stream({ runId }).pipe(
84
+ Stream.runForEach((entry) => Effect.log(`${entry.seq} ${entry.eventType}`))
85
+ )
86
+ })
87
+ ```
88
+
89
+ Three properties are worth knowing before you use it:
90
+
91
+ - It pages the history rather than materializing it, so the first entry arrives
92
+ after one page rather than after the whole run.
93
+ - It reports a sink loss that happens while it is following, to every consumer
94
+ that was following at the time.
95
+ - **It never completes.** Following is the point, so bound the stream yourself:
96
+ `Stream.take`, `Stream.takeUntil`, or a forked fiber you interrupt.
97
+
98
+ A cursor below the run's compaction floor fails with `compacted` and the
99
+ sequence to resync from, never a silently shortened history. See
100
+ [Checkpoints and compaction](../concepts/compaction.md).
101
+
102
+ ## Tail this process with changes
103
+
104
+ `changes` is a scoped subscription to the entries this process commits, across
105
+ every run:
106
+
107
+ ```ts
108
+ import * as PubSub from "effect/PubSub"
109
+
110
+ const tail = Effect.gen(function*() {
111
+ const journal = yield* Journal.Journal
112
+ const subscription = yield* journal.changes
113
+ const entry = yield* PubSub.take(subscription)
114
+ console.log(entry.runId, entry.seq)
115
+ }).pipe(Effect.scoped)
116
+ ```
117
+
118
+ Two limits make it a local convenience rather than a reader of record:
119
+
120
+ - It is a bounded sliding buffer sized by the layer's `capacity`. A slow
121
+ subscriber loses entries with no error and no gap signal.
122
+ - Only this process's commits arrive. Another writer on the same database
123
+ publishes into its own buffer, which is the second reason `stream` exists: it
124
+ rechecks the durable tail.
125
+
126
+ Entries published to `changes` are deeply frozen, so one subscriber cannot
127
+ mutate the value another subscriber is about to read.
128
+
129
+ ## Make a lossy write readable first
130
+
131
+ A `flush` before a read makes queued lossy entries visible. A durable entry
132
+ needs no flush; it was committed when its receipt arrived.
133
+
134
+ ## Next steps
135
+
136
+ - [Fold a run into a projection](./fold-a-projection.md) to turn entries into a
137
+ served view.
138
+ - [Compact a long-running run](./compact-a-run.md) for what a reader below the
139
+ floor should do.
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: "Keep credentials out of log output"
3
+ description: "Install RedactedLogger so the journal's redaction rules also cover the terminal, the JSON log document, and the spans a tracer logger exports."
4
+ sidebar:
5
+ order: 6
6
+ ---
7
+
8
+ The journal redacts on the write path, so a credential an action logs or
9
+ returns never reaches a committed row. The operator's terminal is the other
10
+ place the same credential surfaces: every built-in Effect logger renders through
11
+ the fiber's `Console`, so an action that hands a token to `Effect.logInfo`
12
+ writes it to stderr in full, and it reaches whatever collects that stream.
13
+
14
+ `RedactedLogger` closes that half with the same rules rather than a second rule
15
+ set. A log line is a diagnostic, so it takes `Redaction.diagnosticRules`: the
16
+ default rules plus the spellings too broad for a durable row.
17
+
18
+ ## Install the layer
19
+
20
+ ```ts
21
+ import { RedactedLogger } from "@smthrs/journal"
22
+ import * as Layer from "effect/Layer"
23
+
24
+ const logging = RedactedLogger.layer()
25
+ ```
26
+
27
+ The layer composes over whatever loggers are already installed rather than
28
+ choosing one, so an operator keeps the format they had: their own logger, their
29
+ own layout. A host that installed its own logger keeps it too.
30
+
31
+ Pass `Redaction.Options` to extend or replace the rules, exactly as you would
32
+ for the journal write path. Spread `Redaction.diagnosticRules` to add to them.
33
+
34
+ ## What it covers
35
+
36
+ Two halves carry load, and both are necessary:
37
+
38
+ - **The log event itself.** The message, the cause, and the log annotations are
39
+ redacted before the wrapped logger is handed them. This matters because a
40
+ logger is free to read the event instead of rendering it, and one in Effect's
41
+ default set does: `Logger.tracerLogger` publishes the message as a span event
42
+ name and the pretty cause as the span's `effect.cause` attribute, and never
43
+ touches `Console`. Redacting only the console would hide a credential on
44
+ stderr and export it in clear to whatever OTLP collector is configured.
45
+ Cause stack annotations are redacted too: span names, lazy stack text, parent
46
+ frames, and interruptor frames. The original cause is unchanged; the delegate
47
+ receives rebuilt reasons with redacted frame chains.
48
+ - **The fiber's console.** The wrapped logger is handed a view of the fiber
49
+ whose `Console` is a redacting console, so whatever the logger renders for
50
+ itself, a pretty line or a JSON document, passes through the rules on its way
51
+ to the stream.
52
+
53
+ Cost is bounded by the rules: each rule is linear in the length of the line,
54
+ with no catastrophic backtracking.
55
+
56
+ ## Two differences from the write path
57
+
58
+ **A too-deep value is named, not refused.** The journal throws on a payload
59
+ past `Redaction.maxDepth` so the write fails with a typed `invalid_event`. A
60
+ logger cannot: a throw there is caught one frame up and replaces every argument
61
+ on the line with a placeholder, so one deep member would cost the operator the
62
+ whole line. `wrap` therefore defaults `onTooDeep` to `"name"`. A caller passing
63
+ its own `onTooDeep` still wins.
64
+
65
+ **An `Error` is rebuilt as a plain `Error`.** Its `message` and `stack` live on
66
+ non-enumerable properties, so the own-key rebuild the write path uses would
67
+ print `{}` where the operator expected a failure. The copy keeps the name,
68
+ message, stack, and own members with the rules applied, but not the class. A
69
+ copy built on the original's prototype would inherit everything the prototype
70
+ defines, and `name`, `cause`, `toJSON`, `Symbol.toStringTag`, and
71
+ `nodejs.util.inspect.custom` each carried a credential out that way.
72
+
73
+ ## Wrap a single logger
74
+
75
+ Use `wrap` when you are constructing a logger rather than composing a layer:
76
+
77
+ ```ts
78
+ import { RedactedLogger } from "@smthrs/journal"
79
+ import * as Logger from "effect/Logger"
80
+
81
+ const json = RedactedLogger.wrap(Logger.formatJson)
82
+ ```
83
+
84
+ `wrap` is idempotent. Both the CLI and the durable runtime install the layer,
85
+ and a detached run is both at once, so a logger this module already wrapped is
86
+ returned unchanged rather than paying the rules twice per line.
87
+ `RedactedLogger.isRedacted` reports whether a logger is already wrapped.
88
+
89
+ ## Next steps
90
+
91
+ - [Redaction](../concepts/redaction.md) for the rules, the bounds, and where
92
+ redaction deliberately stops.
93
+ - [Observability](/docs/guides/observability/) for exporting the spans this
94
+ logger writes into.
@@ -0,0 +1,124 @@
1
+ ---
2
+ title: "Test against a real journal"
3
+ description: "Use TestJournal for the production journal over in-memory SQLite, Notifying to inject a crash at an exact transition, and the noop layer for a composition that must not write."
4
+ sidebar:
5
+ order: 7
6
+ ---
7
+
8
+ The journal is deterministic to test because its only nondeterministic input is
9
+ the database, and the database is a layer. This package ships two doubles and
10
+ one stub, and none of them reimplements the journal.
11
+
12
+ Neither double is exported from the root entry point, so import both by
13
+ subpath. `TestJournal` binds a Node SQLite database and runs on Node only;
14
+ `Notifying` wraps a service and is platform independent.
15
+
16
+ ## Run the real journal in memory
17
+
18
+ `TestJournal.layer` provides the production `SqlJournal` over a fresh in-memory
19
+ database, with migrations already run:
20
+
21
+ ```ts
22
+ import * as TestJournal from "@smthrs/journal/test/TestJournal"
23
+
24
+ const layer = TestJournal.layer()
25
+ ```
26
+
27
+ `TestJournalOptions` is `SqlJournalOptions` with `capacity` and `overflow` made
28
+ optional, so every option forwards to `SqlJournal.layer` unchanged and a suite
29
+ can exercise index-bound eviction, the `maxEntryBytes` entry bound, a custom or
30
+ noop redactor, and the compaction policy through the bundle instead of
31
+ hand-assembling the layer stack the bundle exists to hide:
32
+
33
+ ```ts
34
+ const strict = TestJournal.layer({
35
+ capacity: 8,
36
+ overflow: "drop-oldest",
37
+ sourceEventCache: 2,
38
+ redact: Redaction.makeNoop()
39
+ })
40
+ ```
41
+
42
+ The defaults are `capacity: 1024` and `overflow: "reject"`.
43
+
44
+ Fenced writes need the run claimed and activated through the journal's
45
+ `Consensus` strategy. This bundle fences through `SqlConsensus`, so a suite
46
+ that exercises `emitDurable`, `checkpoint`, or `compact` takes the run through
47
+ `Consensus.claim` and `activate` first, or takes the whole engine bundle
48
+ instead.
49
+
50
+ ## Get every store over one database
51
+
52
+ `@smthrs/journal/test/TestJournal` provides the journal and nothing else. For
53
+ the journal, run, attempt, and cache services over one database, take
54
+ `@smthrs/engine-store/test/TestStores` from
55
+ [`@smthrs/engine-store`](/api/engine-store). That bundle includes the run-store
56
+ migrations, so the fence has a table to read.
57
+
58
+ ## Inject a crash at an exact transition
59
+
60
+ `Notifying` wraps a record-of-Effect-methods service so a hook fires around
61
+ every operation. The hook runs in the calling fiber, so a hook that dies,
62
+ interrupts, or awaits a `Latch` injects crashes, fence loss, and exact
63
+ sequencing at any durable transition point:
64
+
65
+ ```ts
66
+ import { Journal } from "@smthrs/journal"
67
+ import * as Notifying from "@smthrs/journal/test/Notifying"
68
+ import * as Effect from "effect/Effect"
69
+
70
+ const crashAfterCommit = Notifying.layer(
71
+ Journal.Journal,
72
+ (op, order) => op === "emitDurable" && order === "after" ? Effect.die("crash") : Effect.void
73
+ )
74
+ ```
75
+
76
+ Three details shape how you write a hook:
77
+
78
+ - `op` is the service method name and `args` are the call arguments, so one
79
+ hook can select the exact call it cares about.
80
+ - The `after` firing only happens when the operation succeeds. A failed
81
+ operation is already the interesting event.
82
+ - Effect-returning methods and plain `Effect` properties are hooked;
83
+ non-Effect members, such as the `Stream`-returning `stream` and `project`,
84
+ delegate untouched.
85
+
86
+ `Notifying.layer` reads the real service from the environment and re-provides
87
+ the wrapper under the same key, so an existing stack gains a listener without
88
+ touching its construction. `Notifying.wrap` is the direct form, and it is
89
+ generic over any such service, not just the journal.
90
+
91
+ ## Assert that nothing wrote
92
+
93
+ `Journal.layerNoop()` provides a closed journal whose every operation fails
94
+ `journal_closed`. Use it for a composition that must not write, or to prove a
95
+ code path never reaches the journal. Override individual methods to record
96
+ calls:
97
+
98
+ ```ts
99
+ import { Journal } from "@smthrs/journal"
100
+ import * as Effect from "effect/Effect"
101
+
102
+ const recorded: Array<string> = []
103
+
104
+ const layer = Journal.layerNoop({
105
+ emitLossy: (input) => {
106
+ recorded.push(input.eventType)
107
+ return Effect.succeed({ _tag: "Accepted", seq: 0, sourceSeq: 0 } as Journal.EmitReceipt)
108
+ }
109
+ })
110
+ ```
111
+
112
+ The closed stub's `transact` runs its effect directly, because a stub with no
113
+ sink has no transaction to open and no crash window to close. A double that
114
+ models rollback overrides it.
115
+
116
+ ## Remember the flush barrier
117
+
118
+ A test that writes on the lossy channel and then reads must `flush` in between.
119
+ A durable write needs no flush; it was committed when its receipt arrived.
120
+
121
+ ## Next steps
122
+
123
+ - [The two channels](../concepts/two-channels.md) for what each receipt proves.
124
+ - [Testing flows](/docs/guides/testing-flows/) for the layer above this one.
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: "Write a fenced lifecycle event"
3
+ description: "Emit a durable lifecycle entry with an OwnerId, handle fence_lost as ownership news rather than an error, and know when the unfenced channel is correct."
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ A lifecycle event is the evidence that a run moved: created, started,
9
+ finished, failed. It belongs on the durable channel, and the durable channel is
10
+ fenced, so the write also proves the process still owns the run.
11
+
12
+ ## Before you start
13
+
14
+ The fence is the journal's `Consensus` strategy: a fenced write commits only
15
+ while the strategy records the owner as holding the run, so the run must have
16
+ been claimed and activated first. [`@smthrs/run-store`](/api/run-store) drives
17
+ that for a durable run. See
18
+ [Installation](../installation.md#what-a-fenced-write-needs).
19
+
20
+ ## Emit the entry
21
+
22
+ Pass the owner your process holds for the run:
23
+
24
+ ```ts
25
+ import { Journal, JournalEvent } from "@smthrs/journal"
26
+ import type * as OwnerId from "@smthrs/journal/OwnerId"
27
+ import * as Effect from "effect/Effect"
28
+
29
+ const runId = "run-1" as JournalEvent.RunId
30
+ const sourceId = "engine" as JournalEvent.SourceId
31
+
32
+ const owner: OwnerId.OwnerId = {
33
+ hostId: "host-1",
34
+ pid: process.pid,
35
+ nonce: "run-1-claim"
36
+ }
37
+
38
+ const started = Effect.gen(function*() {
39
+ const journal = yield* Journal.Journal
40
+ return yield* journal.emitDurable({
41
+ runId,
42
+ sourceId,
43
+ eventType: "run.started",
44
+ payload: { attempt: 1 }
45
+ }, owner)
46
+ })
47
+ ```
48
+
49
+ The receipt arrives after the transaction commits, so the `seq` it carries is
50
+ already on disk. Do not advance the run or expose its result before this
51
+ returns: that ordering is the whole reason the channel exists.
52
+
53
+ ## Handle a lost fence
54
+
55
+ `fence_lost` is not an error to retry. It is news: another process owns this
56
+ run now, and this one must stop writing.
57
+
58
+ ```ts
59
+ const guarded = started.pipe(
60
+ Effect.catchTag("@smthrs/journal/JournalError", (failure) =>
61
+ failure.code === "fence_lost"
62
+ ? Effect.logWarning("run reclaimed by another owner; standing down").pipe(Effect.as(undefined))
63
+ : Effect.fail(failure))
64
+ )
65
+ ```
66
+
67
+ Retrying with the same token cannot succeed, and reaching for
68
+ `emitDurableUnfenced` to get past it writes exactly the zombie entry the fence
69
+ exists to reject.
70
+
71
+ A neighbouring code needs a different response. `invalid_event` on a fenced
72
+ call means the owner you passed is not an `OwnerId`, most often a fractional or
73
+ negative `pid`. Fix the argument.
74
+
75
+ ## Give the producer a stable identity
76
+
77
+ Supply `sourceSeq` when the caller can derive a stable number for the event.
78
+ The journal then dedupes a retry against the committed row instead of writing a
79
+ second entry:
80
+
81
+ ```ts
82
+ const finished = Effect.gen(function*() {
83
+ const journal = yield* Journal.Journal
84
+ return yield* journal.emitDurable({
85
+ runId,
86
+ sourceId,
87
+ sourceSeq: 7 as JournalEvent.SourceSeq,
88
+ eventType: "run.finished",
89
+ payload: { outcome: "succeeded" }
90
+ }, owner)
91
+ })
92
+ ```
93
+
94
+ An exact retry returns `Duplicate` with the original `seq`. A reused sequence
95
+ carrying different content fails `idempotency_conflict`, unless the input
96
+ declares `dedupe: "identity"`. See
97
+ [Producer identity and idempotency](../concepts/idempotency.md).
98
+
99
+ ## When to use the unfenced channel
100
+
101
+ `emitDurableUnfenced` is correct for an admission that owns no run and is
102
+ first-writer-wins by design: an external trigger delivered by a sweeper, an
103
+ import, a repair tool. It is the wrong answer to a `fence_lost`.
104
+
105
+ ## Next steps
106
+
107
+ - [Commit state and its entry together](./commit-state-and-entry.md) when the
108
+ entry describes a state transition that must not disagree with it.
109
+ - [The owner fence](../concepts/owner-fence.md) for what the fence checks and
110
+ why the token lives in this package.
@@ -0,0 +1,133 @@
1
+ ---
2
+ title: "Installation"
3
+ description: "Install @smthrs/journal, choose a database layer, install the migration sets a journal needs, and learn which extra table the fenced write path reads."
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ ## Install the package
9
+
10
+ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
11
+
12
+ The package requires Node.js 26.4.0 or later and ships as both ESM and
13
+ CommonJS with TypeScript declarations. Its only runtime dependencies are
14
+ [`effect`](https://effect.website) and [`@smthrs/database`](/api/database).
15
+ Composing a journal layer imports from both of them by name, so install them as
16
+ direct dependencies rather than relying on them being hoisted.
17
+
18
+ `effect` is a peer dependency pinned at `4.0.0-rc.115`. Install exactly that
19
+ version: two copies of `effect` in one program are two sets of service tags, so
20
+ a journal layer built against one copy cannot be provided to a program holding
21
+ the other, and the mismatch shows up as a missing service rather than as a
22
+ version error.
23
+
24
+ ## Add a database
25
+
26
+ `SqlJournal.layer` needs two services from `@smthrs/database`: a
27
+ `SqlClient.SqlClient` to read through and a `DurableWriter` to write through.
28
+ On Node, `NodeDatabase.layer` provides the client and `DurableWriter.layer()`
29
+ provides the writer. This composition selects the optional Node SQLite driver:
30
+
31
+ ```bash
32
+ pnpm add @effect/sql-sqlite-node@4.0.0-rc.115
33
+ ```
34
+
35
+ ```ts
36
+ import * as DurableWriter from "@smthrs/database/DurableWriter"
37
+ import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
38
+ import * as Layer from "effect/Layer"
39
+
40
+ const database = Layer.provideMerge(
41
+ DurableWriter.layer(),
42
+ NodeDatabase.layer({ filename: "flows.db" })
43
+ )
44
+ ```
45
+
46
+ SQLite is the supported backend at `1.0.0-rc.0`. PostgreSQL and PGlite are
47
+ not; see [storage compatibility](/docs/migration/compatibility#storage).
48
+
49
+ ## Install the migrations
50
+
51
+ `Migrations.layer` creates this package's two tables and nothing else:
52
+ `flows_journal_events` with its event-type index, and
53
+ `flows_journal_checkpoints`.
54
+
55
+ ```ts
56
+ import { Migrations, SqlJournal } from "@smthrs/journal"
57
+ import * as Layer from "effect/Layer"
58
+
59
+ const journal = SqlJournal.layer({ capacity: 1024, overflow: "reject" }).pipe(
60
+ Layer.provide(Layer.provideMerge(Migrations.layer, database))
61
+ )
62
+ ```
63
+
64
+ An application that also needs run, cache, or engine tables composes migration
65
+ sets rather than stacking layers. `Migrations.set` is the journal's namespaced
66
+ set, and `@smthrs/database`'s `Migrations.layer` runs several sets over one
67
+ `flows_migrations` table, giving each package a reserved id block so two
68
+ packages' `0001_initial` cannot collide:
69
+
70
+ ```ts
71
+ import * as DatabaseMigrations from "@smthrs/database/Migrations"
72
+ import * as JournalMigrations from "@smthrs/journal/Migrations"
73
+ import * as RunStoreMigrations from "@smthrs/run-store/Migrations"
74
+
75
+ const migrations = DatabaseMigrations.layer([
76
+ JournalMigrations.set,
77
+ RunStoreMigrations.set
78
+ ])
79
+ ```
80
+
81
+ `@smthrs/engine-store/Migrations` already exports `sets`, the whole durable
82
+ schema a durable engine needs, in dependency order. Take that list if you are
83
+ composing an engine rather than a standalone journal.
84
+
85
+ ## What a fenced write needs
86
+
87
+ `emitDurable`, `checkpoint`, and `compact` gate their write on the `Consensus`
88
+ strategy the journal was built with. `SqlJournal.layer` fences through
89
+ `SqlConsensus`, whose lease table the journal's own migrations create, so the
90
+ fenced channel needs nothing beyond this package's migration set; an owner
91
+ takes the run through the strategy's `claim` and `activate`, which
92
+ [`@smthrs/run-store`](/api/run-store) drives for a durable run.
93
+
94
+ `emitLossy`, `emitDurableUnfenced`,
95
+ `entries`, `stream`, `project`, and `flush` read no table but the journal's
96
+ own, so a standalone journal serves them without run-store.
97
+
98
+ For why the fence exists and when the unfenced channel is the correct one, see
99
+ [the owner fence](./concepts/owner-fence.md).
100
+
101
+ ## Import forms
102
+
103
+ The root entry point re-exports every module as a namespace:
104
+
105
+ ```ts
106
+ import { Journal, JournalEvent, Migrations, Redaction, SqlJournal } from "@smthrs/journal"
107
+ ```
108
+
109
+ Each module is also importable from its own subpath, which is the form the API
110
+ reference uses:
111
+
112
+ ```ts
113
+ import * as Journal from "@smthrs/journal/Journal"
114
+ import * as SqlJournal from "@smthrs/journal/SqlJournal"
115
+ ```
116
+
117
+ The root is written against the driver-neutral `@smthrs/database` contract and
118
+ bundles for the browser. Neither test double is exported from the root, so both
119
+ are imported by subpath. `TestJournal` binds a Node SQLite database and runs on
120
+ Node only; `Notifying` wraps a service and is platform independent:
121
+
122
+ ```ts
123
+ import * as Notifying from "@smthrs/journal/test/Notifying"
124
+ import * as TestJournal from "@smthrs/journal/test/TestJournal"
125
+ ```
126
+
127
+ Three subpath forms are not public and are blocked in the export map:
128
+ `@smthrs/journal/internal/*`, `@smthrs/journal/migrations/*`, and
129
+ `@smthrs/journal/*/index`. `@smthrs/journal/package.json` is exported.
130
+
131
+ ## Next step
132
+
133
+ Write and read one run in the [Quickstart](./quickstart.md).