@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,654 @@
1
+ /**
2
+ * Durable event-envelope schemas for the journal.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+
7
+ import * as Schema from "effect/Schema"
8
+
9
+ /**
10
+ * A UTF-16 surrogate with no partner: a high surrogate not followed by a low
11
+ * one, or a low surrogate not preceded by a high one. `String.isWellFormed`
12
+ * answers the same question but needs the ES2024 lib, which this package does
13
+ * not target.
14
+ */
15
+ const loneSurrogate = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/
16
+
17
+ /**
18
+ * A NUL anywhere in an identifier.
19
+ *
20
+ * SQLite's `length()` counts the characters BEFORE the first NUL, and every
21
+ * identifier column carries `CHECK (length(...) > 0)`, so a leading-NUL run id
22
+ * measures zero and the write fails the constraint. Measured on this tree, an
23
+ * emit with a leading-NUL run id came back as `sink_failed` with a
24
+ * `DatabaseError` cause, which tells the caller the database is down when the
25
+ * real fault is the identifier it just supplied. A NUL after the first
26
+ * character is refused on the same terms: it makes the column's own length
27
+ * check disagree with the identifier's length.
28
+ *
29
+ * Built from a code point rather than written as a pattern literal: a control
30
+ * character in a regex is exactly the typo `no-control-regex` exists to catch,
31
+ * and no reader can see one in the source.
32
+ */
33
+ const embeddedNul = new RegExp(String.fromCharCode(0))
34
+
35
+ /**
36
+ * Longest identifier the journal persists.
37
+ *
38
+ * A run id, a source id and an event type are index columns of a permanent
39
+ * table and are held in per-run maps for the layer's lifetime, so an
40
+ * unbounded one costs durable index space and heap that nothing ever
41
+ * reclaims. Every identifier this repository mints is a uuid or a short
42
+ * dotted name, two orders of magnitude below this bound, so the ceiling
43
+ * refuses only the shapes that were never identifiers.
44
+ *
45
+ * @since 1.0.0
46
+ * @category constants
47
+ */
48
+ export const maxIdentifierLength = 1024
49
+
50
+ /**
51
+ * A persistable identifier: non-empty, and representable in the store.
52
+ *
53
+ * SQLite binds a lone UTF-16 surrogate as U+FFFD, so two ill-formed
54
+ * identifiers that differ only in their surrogates land on ONE persisted key.
55
+ * The second run's first event then dedupes into the first run's row and reads
56
+ * by either id return the same history, which destroys run isolation at the
57
+ * persistence boundary. Rejecting ill-formed text at the schema keeps the
58
+ * identifier the caller decoded and the identifier the database stores the
59
+ * same value.
60
+ *
61
+ * A NUL is rejected for the reason {@link embeddedNul} gives: the column's own
62
+ * length check cannot see past one, so the store refuses the identifier as a
63
+ * constraint violation and the caller is told the sink failed.
64
+ *
65
+ * The empty string is rejected for a plainer reason: it names nothing, and the
66
+ * journal used to accept it at decode and then reject it at the service, so a
67
+ * caller could hold a "valid" identifier the next call refused.
68
+ * The length is bounded because every identifier occupies permanent index
69
+ * space and layer-lifetime heap.
70
+ */
71
+ const identifier = Schema.String.check(
72
+ Schema.isMinLength(1),
73
+ Schema.isMaxLength(maxIdentifierLength),
74
+ Schema.makeFilter((value: string) => !loneSurrogate.test(value), { title: "wellFormedIdentifier" }),
75
+ Schema.makeFilter((value: string) => !embeddedNul.test(value), { title: "nulFreeIdentifier" })
76
+ )
77
+
78
+ /**
79
+ * Schema for an identifier of one durable run.
80
+ *
81
+ * Between 1 and 1,024 UTF-16 code units, free of unpaired UTF-16 surrogates
82
+ * because the store cannot tell two ill-formed identifiers apart, and free of
83
+ * NUL because the store's own length check cannot see past one.
84
+ *
85
+ * @category schemas
86
+ * @since 0.1.0
87
+ */
88
+ export const RunId = identifier.pipe(Schema.brand("@smthrs/journal/JournalEvent/RunId"))
89
+
90
+ /**
91
+ * Branded identifier of one durable run.
92
+ *
93
+ * @category models
94
+ * @since 0.1.0
95
+ */
96
+ export type RunId = typeof RunId.Type
97
+
98
+ /**
99
+ * Schema for the canonical, durable sequence number within a run.
100
+ *
101
+ * `Number.MAX_SAFE_INTEGER` is excluded because the journal must always be
102
+ * able to allocate the next sequence. `Number.MAX_SAFE_INTEGER + 1` is not a
103
+ * distinct integer, so the maximum is not an allocatable sequence.
104
+ *
105
+ * @category schemas
106
+ * @since 0.1.0
107
+ */
108
+ export const Seq = Schema.Int.check(
109
+ Schema.isGreaterThanOrEqualTo(0),
110
+ Schema.isLessThan(Number.MAX_SAFE_INTEGER)
111
+ ).pipe(
112
+ Schema.brand("@smthrs/journal/JournalEvent/Seq")
113
+ )
114
+
115
+ /**
116
+ * Canonical durable per-run sequence number.
117
+ *
118
+ * @category models
119
+ * @since 0.1.0
120
+ */
121
+ export type Seq = typeof Seq.Type
122
+
123
+ /**
124
+ * Schema for an event producer identifier.
125
+ *
126
+ * The same shape `RunId` accepts, on the same terms: the pair identifies a
127
+ * producer's retries in the database.
128
+ *
129
+ * @category schemas
130
+ * @since 0.1.0
131
+ */
132
+ export const SourceId = identifier.pipe(Schema.brand("@smthrs/journal/JournalEvent/SourceId"))
133
+
134
+ /**
135
+ * Identifier of an event producer.
136
+ *
137
+ * @category models
138
+ * @since 0.1.0
139
+ */
140
+ export type SourceId = typeof SourceId.Type
141
+
142
+ /**
143
+ * Schema for a producer-local event sequence number.
144
+ *
145
+ * `Number.MAX_SAFE_INTEGER` is excluded because the journal must always be
146
+ * able to allocate the next sequence. `Number.MAX_SAFE_INTEGER + 1` is not a
147
+ * distinct integer, so the maximum is not an allocatable sequence.
148
+ *
149
+ * @category schemas
150
+ * @since 0.1.0
151
+ */
152
+ export const SourceSeq = Schema.Int.check(
153
+ Schema.isGreaterThanOrEqualTo(0),
154
+ Schema.isLessThan(Number.MAX_SAFE_INTEGER)
155
+ ).pipe(
156
+ Schema.brand("@smthrs/journal/JournalEvent/SourceSeq")
157
+ )
158
+
159
+ /**
160
+ * Producer-local event sequence number.
161
+ *
162
+ * @category models
163
+ * @since 0.1.0
164
+ */
165
+ export type SourceSeq = typeof SourceSeq.Type
166
+
167
+ /**
168
+ * Schema for what a re-emitted `(runId, sourceId, sourceSeq)` identity means.
169
+ *
170
+ * `content` is the default and the strict reading: the identity names one set
171
+ * of bytes, so a producer that re-emits it with different bytes has a bug and
172
+ * the journal says so with `idempotency_conflict`.
173
+ *
174
+ * `identity` is for a producer that derives the sequence from the event
175
+ * itself. There a collision IS the same event observed twice, and the bytes
176
+ * that differ between the two observations are metadata ABOUT the observation
177
+ * rather than the event: when a replayed frame was re-recorded, how long a
178
+ * step took the second time a durable engine served it from its record. The
179
+ * first admitted row stands and the re-emission settles as `Duplicate`. Only
180
+ * declare it with a sequence derived from the event's own content, because
181
+ * the journal then has nothing else with which to notice two different events
182
+ * wearing one identity.
183
+ *
184
+ * @category schemas
185
+ * @since 0.1.0
186
+ */
187
+ export const Dedupe = Schema.Literals(["content", "identity"])
188
+
189
+ /**
190
+ * What a re-emitted producer identity means.
191
+ *
192
+ * @category models
193
+ * @since 0.1.0
194
+ */
195
+ export type Dedupe = typeof Dedupe.Type
196
+
197
+ /**
198
+ * Schema for an event submitted to the journal.
199
+ *
200
+ * Event types and values intentionally remain an open envelope. The durable
201
+ * core never closes this into an interpreter-specific union.
202
+ *
203
+ * @category schemas
204
+ * @since 0.1.0
205
+ */
206
+ export class Input extends Schema.Class<Input>("@smthrs/journal/JournalEvent/Input")({
207
+ runId: RunId,
208
+ sourceId: SourceId,
209
+ sourceSeq: Schema.optional(SourceSeq),
210
+ /** How a collision on this event's identity is settled. Defaults to `content`. */
211
+ dedupe: Schema.optional(Dedupe),
212
+ /** Non-empty, NUL-free, well-formed UTF-16 of at most 1,024 code units. */
213
+ eventType: identifier,
214
+ payload: Schema.Unknown,
215
+ meta: Schema.optional(Schema.Unknown)
216
+ }) {}
217
+
218
+ /**
219
+ * Schema for a committed durable journal row.
220
+ *
221
+ * `seq` is allocated synchronously at journal admission and is the only
222
+ * sequence used for replay and durable provenance. `sourceSeq` identifies
223
+ * retries from one producer.
224
+ *
225
+ * @category schemas
226
+ * @since 0.1.0
227
+ */
228
+ export class Entry extends Schema.Class<Entry>("@smthrs/journal/JournalEvent/Entry")({
229
+ runId: RunId,
230
+ seq: Seq,
231
+ /**
232
+ * Derived by {@link makeEventId} from the other three identity members, never
233
+ * supplied by a caller, so it carries no check of its own.
234
+ */
235
+ eventId: Schema.String,
236
+ sourceId: SourceId,
237
+ sourceSeq: SourceSeq,
238
+ emittedAtMs: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
239
+ /**
240
+ * The same bounded identifier `Input.eventType` accepts. It used to be a bare
241
+ * `Schema.String` here, so a consumer decoding an `Entry` accepted an empty,
242
+ * over-long, NUL-bearing, or ill-formed-UTF-16 event type that no emit could
243
+ * ever have produced.
244
+ */
245
+ eventType: identifier,
246
+ payload: Schema.Unknown,
247
+ meta: Schema.Unknown
248
+ }) {}
249
+
250
+ /**
251
+ * Makes a collision-free deterministic event identifier from the idempotency
252
+ * key `(runId, sourceId, sourceSeq)`.
253
+ *
254
+ * Length prefixes preserve tuple boundaries even when identifiers contain the
255
+ * separator. The value is deliberately not random: retrying the same source
256
+ * event must produce the same durable id.
257
+ *
258
+ * @category constructors
259
+ * @since 0.1.0
260
+ */
261
+ export const makeEventId = (runId: RunId, sourceId: SourceId, sourceSeq: SourceSeq): string =>
262
+ `flows:event:${runId.length}:${runId}${sourceId.length}:${sourceId}${sourceSeq}`
263
+
264
+ /**
265
+ * The reserved prefix of every companion stream id.
266
+ *
267
+ * A run id that begins with it names a companion stream, never a run: stores
268
+ * that admit run ids (`@smthrs/run-store`'s `RunStore.create`) refuse it.
269
+ *
270
+ * @category constants
271
+ * @since 1.0.0
272
+ */
273
+ export const companionPrefix = "flows.companion/"
274
+
275
+ /** SHA-256 round constants: the first 32 bits of the cube roots of the first 64 primes. */
276
+ const sha256Rounds = Uint32Array.from([
277
+ 0x428a2f98,
278
+ 0x71374491,
279
+ 0xb5c0fbcf,
280
+ 0xe9b5dba5,
281
+ 0x3956c25b,
282
+ 0x59f111f1,
283
+ 0x923f82a4,
284
+ 0xab1c5ed5,
285
+ 0xd807aa98,
286
+ 0x12835b01,
287
+ 0x243185be,
288
+ 0x550c7dc3,
289
+ 0x72be5d74,
290
+ 0x80deb1fe,
291
+ 0x9bdc06a7,
292
+ 0xc19bf174,
293
+ 0xe49b69c1,
294
+ 0xefbe4786,
295
+ 0x0fc19dc6,
296
+ 0x240ca1cc,
297
+ 0x2de92c6f,
298
+ 0x4a7484aa,
299
+ 0x5cb0a9dc,
300
+ 0x76f988da,
301
+ 0x983e5152,
302
+ 0xa831c66d,
303
+ 0xb00327c8,
304
+ 0xbf597fc7,
305
+ 0xc6e00bf3,
306
+ 0xd5a79147,
307
+ 0x06ca6351,
308
+ 0x14292967,
309
+ 0x27b70a85,
310
+ 0x2e1b2138,
311
+ 0x4d2c6dfc,
312
+ 0x53380d13,
313
+ 0x650a7354,
314
+ 0x766a0abb,
315
+ 0x81c2c92e,
316
+ 0x92722c85,
317
+ 0xa2bfe8a1,
318
+ 0xa81a664b,
319
+ 0xc24b8b70,
320
+ 0xc76c51a3,
321
+ 0xd192e819,
322
+ 0xd6990624,
323
+ 0xf40e3585,
324
+ 0x106aa070,
325
+ 0x19a4c116,
326
+ 0x1e376c08,
327
+ 0x2748774c,
328
+ 0x34b0bcb5,
329
+ 0x391c0cb3,
330
+ 0x4ed8aa4a,
331
+ 0x5b9cca4f,
332
+ 0x682e6ff3,
333
+ 0x748f82ee,
334
+ 0x78a5636f,
335
+ 0x84c87814,
336
+ 0x8cc70208,
337
+ 0x90befffa,
338
+ 0xa4506ceb,
339
+ 0xbef9a3f7,
340
+ 0xc67178f2
341
+ ])
342
+
343
+ const rotate = (value: number, bits: number): number => (value >>> bits) | (value << (32 - bits))
344
+
345
+ /**
346
+ * SHA-256 of the UTF-8 encoding of `text`, as lowercase hex.
347
+ *
348
+ * Synchronous on purpose: companion ids are derived inside write
349
+ * transactions, and an asynchronous digest would yield the event loop while
350
+ * the writer is held.
351
+ */
352
+ const sha256Hex = (text: string): string => {
353
+ const bytes = new TextEncoder().encode(text)
354
+ const length = ((bytes.length + 9 + 63) >> 6) << 6
355
+ const padded = new Uint8Array(length)
356
+ padded.set(bytes)
357
+ padded[bytes.length] = 0x80
358
+ const view = new DataView(padded.buffer)
359
+ view.setUint32(length - 8, Math.floor(bytes.length / 0x20000000))
360
+ view.setUint32(length - 4, (bytes.length << 3) >>> 0)
361
+ const hash = Uint32Array.from([
362
+ 0x6a09e667,
363
+ 0xbb67ae85,
364
+ 0x3c6ef372,
365
+ 0xa54ff53a,
366
+ 0x510e527f,
367
+ 0x9b05688c,
368
+ 0x1f83d9ab,
369
+ 0x5be0cd19
370
+ ])
371
+ const words = new Uint32Array(64)
372
+ for (let block = 0; block < length; block += 64) {
373
+ for (let index = 0; index < 16; index++) words[index] = view.getUint32(block + index * 4)
374
+ for (let index = 16; index < 64; index++) {
375
+ const low = words[index - 15]!
376
+ const high = words[index - 2]!
377
+ const s0 = rotate(low, 7) ^ rotate(low, 18) ^ (low >>> 3)
378
+ const s1 = rotate(high, 17) ^ rotate(high, 19) ^ (high >>> 10)
379
+ words[index] = (words[index - 16]! + s0 + words[index - 7]! + s1) >>> 0
380
+ }
381
+ let a = hash[0]!
382
+ let b = hash[1]!
383
+ let c = hash[2]!
384
+ let d = hash[3]!
385
+ let e = hash[4]!
386
+ let f = hash[5]!
387
+ let g = hash[6]!
388
+ let h = hash[7]!
389
+ for (let index = 0; index < 64; index++) {
390
+ const choose = (e & f) ^ (~e & g)
391
+ const t1 =
392
+ (h + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + choose + sha256Rounds[index]! + words[index]!) >>> 0
393
+ const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0
394
+ h = g
395
+ g = f
396
+ f = e
397
+ e = (d + t1) >>> 0
398
+ d = c
399
+ c = b
400
+ b = a
401
+ a = (t1 + t2) >>> 0
402
+ }
403
+ hash[0] = (hash[0]! + a) >>> 0
404
+ hash[1] = (hash[1]! + b) >>> 0
405
+ hash[2] = (hash[2]! + c) >>> 0
406
+ hash[3] = (hash[3]! + d) >>> 0
407
+ hash[4] = (hash[4]! + e) >>> 0
408
+ hash[5] = (hash[5]! + f) >>> 0
409
+ hash[6] = (hash[6]! + g) >>> 0
410
+ hash[7] = (hash[7]! + h) >>> 0
411
+ }
412
+ return Array.from(hash, (word) => word.toString(16).padStart(8, "0")).join("")
413
+ }
414
+
415
+ /**
416
+ * The digest a journal fact uses to reference bytes kept outside the journal:
417
+ * the SHA-256 of the UTF-8 encoding of `text`, as 64 lowercase hex digits.
418
+ *
419
+ * The journal redacts every payload, so a fact never carries executable
420
+ * values. A store that folds state from its facts keeps the bytes in its own
421
+ * rows and records only their digest, which a rebuild checks the row against.
422
+ *
423
+ * @category constructors
424
+ * @since 1.0.0
425
+ */
426
+ export const contentDigest = (text: string): string => sha256Hex(text)
427
+
428
+ /**
429
+ * The id of the companion stream that carries one store's facts about a run.
430
+ *
431
+ * A companion stream is an ordinary journal stream with its own sequence
432
+ * clock, kept beside the run's own stream rather than inside it. A consumer
433
+ * that reads the run's stream by position never sees a companion fact, and a
434
+ * rewind or compaction of the run's stream never truncates one. The id is
435
+ * `flows.companion/<stream>/<sha-256 of the run id>`, so it stays within
436
+ * {@link maxIdentifierLength} for every run id; each fact carries its run id
437
+ * in its payload.
438
+ *
439
+ * @category constructors
440
+ * @since 1.0.0
441
+ */
442
+ export const companionRunId = (stream: string, runId: string): RunId =>
443
+ `${companionPrefix}${stream}/${contentDigest(runId)}` as RunId
444
+
445
+ /**
446
+ * Whether a run id names a companion stream.
447
+ *
448
+ * @category predicates
449
+ * @since 1.0.0
450
+ */
451
+ export const isCompanionRunId = (runId: string): boolean => runId.startsWith(companionPrefix)
452
+
453
+ /**
454
+ * Bounded durable text shared by namespace contracts.
455
+ *
456
+ * @category schemas
457
+ * @since 1.0.0
458
+ */
459
+ export const Identifier = identifier
460
+
461
+ /**
462
+ * A lineage coordinate, distinct from the run that currently hosts it.
463
+ *
464
+ * @category schemas
465
+ * @since 1.0.0
466
+ */
467
+ export const LineageId = identifier.pipe(Schema.brand("@smthrs/journal/LineageId"))
468
+ /**
469
+ * A lineage coordinate.
470
+ *
471
+ * @category models
472
+ * @since 1.0.0
473
+ */
474
+ export type LineageId = typeof LineageId.Type
475
+
476
+ /**
477
+ * A concrete wait identity.
478
+ *
479
+ * @category schemas
480
+ * @since 1.0.0
481
+ */
482
+ export const WaitId = identifier.pipe(Schema.brand("@smthrs/journal/WaitId"))
483
+ /**
484
+ * A concrete wait identity.
485
+ *
486
+ * @category models
487
+ * @since 1.0.0
488
+ */
489
+ export type WaitId = typeof WaitId.Type
490
+
491
+ /**
492
+ * An admitted command identity.
493
+ *
494
+ * @category schemas
495
+ * @since 1.0.0
496
+ */
497
+ export const CommandId = identifier.pipe(Schema.brand("@smthrs/journal/CommandId"))
498
+ /**
499
+ * An admitted command identity.
500
+ *
501
+ * @category models
502
+ * @since 1.0.0
503
+ */
504
+ export type CommandId = typeof CommandId.Type
505
+
506
+ /**
507
+ * A plan identity, never interchangeable with a dispatch or artifact.
508
+ *
509
+ * @category schemas
510
+ * @since 1.0.0
511
+ */
512
+ export const PlanId = identifier.pipe(Schema.brand("@smthrs/journal/PlanId"))
513
+ /**
514
+ * A plan identity.
515
+ *
516
+ * @category models
517
+ * @since 1.0.0
518
+ */
519
+ export type PlanId = typeof PlanId.Type
520
+
521
+ /**
522
+ * A dispatch identity. Branding does not verify its digest.
523
+ *
524
+ * @category schemas
525
+ * @since 1.0.0
526
+ */
527
+ export const DispatchId = identifier.pipe(Schema.brand("@smthrs/journal/DispatchId"))
528
+ /**
529
+ * A dispatch identity.
530
+ *
531
+ * @category models
532
+ * @since 1.0.0
533
+ */
534
+ export type DispatchId = typeof DispatchId.Type
535
+
536
+ /**
537
+ * An artifact identity. Branding does not verify content.
538
+ *
539
+ * @category schemas
540
+ * @since 1.0.0
541
+ */
542
+ export const ArtifactId = identifier.pipe(Schema.brand("@smthrs/journal/ArtifactId"))
543
+ /**
544
+ * An artifact identity.
545
+ *
546
+ * @category models
547
+ * @since 1.0.0
548
+ */
549
+ export type ArtifactId = typeof ArtifactId.Type
550
+
551
+ /**
552
+ * Integer quantities in the inclusive safe range, including zero.
553
+ *
554
+ * @category schemas
555
+ * @since 1.0.0
556
+ */
557
+ export const NonNegativeQuantity = Schema.Int.check(
558
+ Schema.isGreaterThanOrEqualTo(0),
559
+ Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER)
560
+ )
561
+
562
+ /**
563
+ * Integer quantities in the inclusive safe range, excluding zero.
564
+ *
565
+ * @category schemas
566
+ * @since 1.0.0
567
+ */
568
+ export const PositiveQuantity = NonNegativeQuantity.check(Schema.isGreaterThan(0))
569
+
570
+ /**
571
+ * Non-negative integral milliseconds since the Unix epoch.
572
+ *
573
+ * @category schemas
574
+ * @since 1.0.0
575
+ */
576
+ export const TimestampMs = NonNegativeQuantity.pipe(Schema.brand("@smthrs/journal/TimestampMs"))
577
+ /**
578
+ * Non-negative integral milliseconds since the Unix epoch.
579
+ *
580
+ * @category models
581
+ * @since 1.0.0
582
+ */
583
+ export type TimestampMs = typeof TimestampMs.Type
584
+
585
+ /**
586
+ * Decode a run id without a cast; schema failures retain their issue.
587
+ *
588
+ * @category decoders
589
+ * @since 1.0.0
590
+ */
591
+ export const decodeRunId = Schema.decodeUnknownEffect(RunId)
592
+ /**
593
+ * Decode a lineage id without a cast.
594
+ *
595
+ * @category decoders
596
+ * @since 1.0.0
597
+ */
598
+ export const decodeLineageId = Schema.decodeUnknownEffect(LineageId)
599
+ /**
600
+ * Decode a wait id without a cast.
601
+ *
602
+ * @category decoders
603
+ * @since 1.0.0
604
+ */
605
+ export const decodeWaitId = Schema.decodeUnknownEffect(WaitId)
606
+ /**
607
+ * Decode a command id without a cast.
608
+ *
609
+ * @category decoders
610
+ * @since 1.0.0
611
+ */
612
+ export const decodeCommandId = Schema.decodeUnknownEffect(CommandId)
613
+ /**
614
+ * Decode a plan id without a cast.
615
+ *
616
+ * @category decoders
617
+ * @since 1.0.0
618
+ */
619
+ export const decodePlanId = Schema.decodeUnknownEffect(PlanId)
620
+ /**
621
+ * Decode a dispatch id without a cast.
622
+ *
623
+ * @category decoders
624
+ * @since 1.0.0
625
+ */
626
+ export const decodeDispatchId = Schema.decodeUnknownEffect(DispatchId)
627
+ /**
628
+ * Decode an artifact id without a cast.
629
+ *
630
+ * @category decoders
631
+ * @since 1.0.0
632
+ */
633
+ export const decodeArtifactId = Schema.decodeUnknownEffect(ArtifactId)
634
+ /**
635
+ * Decode an absolute timestamp without coercion.
636
+ *
637
+ * @category decoders
638
+ * @since 1.0.0
639
+ */
640
+ export const decodeTimestampMs = Schema.decodeUnknownEffect(TimestampMs)
641
+ /**
642
+ * Decode a budget that permits zero. Units belong to the field.
643
+ *
644
+ * @category decoders
645
+ * @since 1.0.0
646
+ */
647
+ export const decodeNonNegativeQuantity = Schema.decodeUnknownEffect(NonNegativeQuantity)
648
+ /**
649
+ * Decode a budget that requires positive capacity.
650
+ *
651
+ * @category decoders
652
+ * @since 1.0.0
653
+ */
654
+ export const decodePositiveQuantity = Schema.decodeUnknownEffect(PositiveQuantity)