@smthrs/sync 0.0.0-stage → 1.0.0-rc.4

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 (266) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +21 -0
  3. package/README.md +208 -2
  4. package/dist/cjs/BranchCommands.d.ts +228 -0
  5. package/dist/cjs/BranchCommands.d.ts.map +1 -0
  6. package/dist/cjs/BranchCommands.js +316 -0
  7. package/dist/cjs/BranchCommands.js.map +7 -0
  8. package/dist/cjs/BranchIds.d.ts +65 -0
  9. package/dist/cjs/BranchIds.d.ts.map +1 -0
  10. package/dist/cjs/BranchIds.js +53 -0
  11. package/dist/cjs/BranchIds.js.map +7 -0
  12. package/dist/cjs/BranchPresence.d.ts +195 -0
  13. package/dist/cjs/BranchPresence.d.ts.map +1 -0
  14. package/dist/cjs/BranchPresence.js +276 -0
  15. package/dist/cjs/BranchPresence.js.map +7 -0
  16. package/dist/cjs/BranchProjection.d.ts +179 -0
  17. package/dist/cjs/BranchProjection.d.ts.map +1 -0
  18. package/dist/cjs/BranchProjection.js +197 -0
  19. package/dist/cjs/BranchProjection.js.map +7 -0
  20. package/dist/cjs/BranchProtocol.d.ts +415 -0
  21. package/dist/cjs/BranchProtocol.d.ts.map +1 -0
  22. package/dist/cjs/BranchProtocol.js +172 -0
  23. package/dist/cjs/BranchProtocol.js.map +7 -0
  24. package/dist/cjs/BranchRpcs.d.ts +177 -0
  25. package/dist/cjs/BranchRpcs.d.ts.map +1 -0
  26. package/dist/cjs/BranchRpcs.js +85 -0
  27. package/dist/cjs/BranchRpcs.js.map +7 -0
  28. package/dist/cjs/BranchServer.d.ts +55 -0
  29. package/dist/cjs/BranchServer.d.ts.map +1 -0
  30. package/dist/cjs/BranchServer.js +147 -0
  31. package/dist/cjs/BranchServer.js.map +7 -0
  32. package/dist/cjs/BranchShare.d.ts +159 -0
  33. package/dist/cjs/BranchShare.d.ts.map +1 -0
  34. package/dist/cjs/BranchShare.js +147 -0
  35. package/dist/cjs/BranchShare.js.map +7 -0
  36. package/dist/cjs/RunCatalog.d.ts +207 -0
  37. package/dist/cjs/RunCatalog.d.ts.map +1 -0
  38. package/dist/cjs/RunCatalog.js +122 -0
  39. package/dist/cjs/RunCatalog.js.map +7 -0
  40. package/dist/cjs/SyncAuth.d.ts +70 -0
  41. package/dist/cjs/SyncAuth.d.ts.map +1 -0
  42. package/dist/cjs/SyncAuth.js +109 -0
  43. package/dist/cjs/SyncAuth.js.map +7 -0
  44. package/dist/cjs/SyncClient.d.ts +274 -0
  45. package/dist/cjs/SyncClient.d.ts.map +1 -0
  46. package/dist/cjs/SyncClient.js +474 -0
  47. package/dist/cjs/SyncClient.js.map +7 -0
  48. package/dist/cjs/SyncError.d.ts +107 -0
  49. package/dist/cjs/SyncError.d.ts.map +1 -0
  50. package/dist/cjs/SyncError.js +110 -0
  51. package/dist/cjs/SyncError.js.map +7 -0
  52. package/dist/cjs/SyncPrincipal.d.ts +100 -0
  53. package/dist/cjs/SyncPrincipal.d.ts.map +1 -0
  54. package/dist/cjs/SyncPrincipal.js +48 -0
  55. package/dist/cjs/SyncPrincipal.js.map +7 -0
  56. package/dist/cjs/SyncProtocol.d.ts +592 -0
  57. package/dist/cjs/SyncProtocol.d.ts.map +1 -0
  58. package/dist/cjs/SyncProtocol.js +165 -0
  59. package/dist/cjs/SyncProtocol.js.map +7 -0
  60. package/dist/cjs/SyncRpcs.d.ts +92 -0
  61. package/dist/cjs/SyncRpcs.d.ts.map +1 -0
  62. package/dist/cjs/SyncRpcs.js +35 -0
  63. package/dist/cjs/SyncRpcs.js.map +7 -0
  64. package/dist/cjs/SyncServer.d.ts +184 -0
  65. package/dist/cjs/SyncServer.d.ts.map +1 -0
  66. package/dist/cjs/SyncServer.js +513 -0
  67. package/dist/cjs/SyncServer.js.map +7 -0
  68. package/dist/cjs/WorkspaceShare.d.ts +174 -0
  69. package/dist/cjs/WorkspaceShare.d.ts.map +1 -0
  70. package/dist/cjs/WorkspaceShare.js +149 -0
  71. package/dist/cjs/WorkspaceShare.js.map +7 -0
  72. package/dist/cjs/index.d.ts +91 -0
  73. package/dist/cjs/index.d.ts.map +1 -0
  74. package/dist/cjs/index.js +67 -0
  75. package/dist/cjs/index.js.map +7 -0
  76. package/dist/cjs/internal/Admission.d.ts +43 -0
  77. package/dist/cjs/internal/Admission.d.ts.map +1 -0
  78. package/dist/cjs/internal/Admission.js +90 -0
  79. package/dist/cjs/internal/Admission.js.map +7 -0
  80. package/dist/cjs/internal/CauseText.d.ts +55 -0
  81. package/dist/cjs/internal/CauseText.d.ts.map +1 -0
  82. package/dist/cjs/internal/CauseText.js +38 -0
  83. package/dist/cjs/internal/CauseText.js.map +7 -0
  84. package/dist/cjs/internal/JournalErrorCode.d.ts +23 -0
  85. package/dist/cjs/internal/JournalErrorCode.d.ts.map +1 -0
  86. package/dist/cjs/internal/JournalErrorCode.js +34 -0
  87. package/dist/cjs/internal/JournalErrorCode.js.map +7 -0
  88. package/dist/cjs/internal/PolicyOptions.d.ts +57 -0
  89. package/dist/cjs/internal/PolicyOptions.d.ts.map +1 -0
  90. package/dist/cjs/internal/PolicyOptions.js +62 -0
  91. package/dist/cjs/internal/PolicyOptions.js.map +7 -0
  92. package/dist/cjs/internal/ProtocolVersion.d.ts +17 -0
  93. package/dist/cjs/internal/ProtocolVersion.d.ts.map +1 -0
  94. package/dist/cjs/internal/ProtocolVersion.js +44 -0
  95. package/dist/cjs/internal/ProtocolVersion.js.map +7 -0
  96. package/dist/cjs/internal/ShareSigner.d.ts +173 -0
  97. package/dist/cjs/internal/ShareSigner.d.ts.map +1 -0
  98. package/dist/cjs/internal/ShareSigner.js +150 -0
  99. package/dist/cjs/internal/ShareSigner.js.map +7 -0
  100. package/dist/cjs/internal/SnapshotBoundary.d.ts +17 -0
  101. package/dist/cjs/internal/SnapshotBoundary.d.ts.map +1 -0
  102. package/dist/cjs/internal/SnapshotBoundary.js +97 -0
  103. package/dist/cjs/internal/SnapshotBoundary.js.map +7 -0
  104. package/dist/cjs/package.json +1 -0
  105. package/dist/cjs/test/TestSocket.d.ts +55 -0
  106. package/dist/cjs/test/TestSocket.d.ts.map +1 -0
  107. package/dist/cjs/test/TestSocket.js +145 -0
  108. package/dist/cjs/test/TestSocket.js.map +7 -0
  109. package/dist/cjs/test/TestSync.d.ts +54 -0
  110. package/dist/cjs/test/TestSync.d.ts.map +1 -0
  111. package/dist/cjs/test/TestSync.js +96 -0
  112. package/dist/cjs/test/TestSync.js.map +7 -0
  113. package/dist/esm/BranchCommands.d.ts +228 -0
  114. package/dist/esm/BranchCommands.d.ts.map +1 -0
  115. package/dist/esm/BranchCommands.js +496 -0
  116. package/dist/esm/BranchCommands.js.map +1 -0
  117. package/dist/esm/BranchIds.d.ts +65 -0
  118. package/dist/esm/BranchIds.d.ts.map +1 -0
  119. package/dist/esm/BranchIds.js +61 -0
  120. package/dist/esm/BranchIds.js.map +1 -0
  121. package/dist/esm/BranchPresence.d.ts +195 -0
  122. package/dist/esm/BranchPresence.d.ts.map +1 -0
  123. package/dist/esm/BranchPresence.js +361 -0
  124. package/dist/esm/BranchPresence.js.map +1 -0
  125. package/dist/esm/BranchProjection.d.ts +179 -0
  126. package/dist/esm/BranchProjection.d.ts.map +1 -0
  127. package/dist/esm/BranchProjection.js +269 -0
  128. package/dist/esm/BranchProjection.js.map +1 -0
  129. package/dist/esm/BranchProtocol.d.ts +415 -0
  130. package/dist/esm/BranchProtocol.d.ts.map +1 -0
  131. package/dist/esm/BranchProtocol.js +323 -0
  132. package/dist/esm/BranchProtocol.js.map +1 -0
  133. package/dist/esm/BranchRpcs.d.ts +177 -0
  134. package/dist/esm/BranchRpcs.d.ts.map +1 -0
  135. package/dist/esm/BranchRpcs.js +103 -0
  136. package/dist/esm/BranchRpcs.js.map +1 -0
  137. package/dist/esm/BranchServer.d.ts +55 -0
  138. package/dist/esm/BranchServer.d.ts.map +1 -0
  139. package/dist/esm/BranchServer.js +168 -0
  140. package/dist/esm/BranchServer.js.map +1 -0
  141. package/dist/esm/BranchShare.d.ts +159 -0
  142. package/dist/esm/BranchShare.d.ts.map +1 -0
  143. package/dist/esm/BranchShare.js +213 -0
  144. package/dist/esm/BranchShare.js.map +1 -0
  145. package/dist/esm/RunCatalog.d.ts +207 -0
  146. package/dist/esm/RunCatalog.d.ts.map +1 -0
  147. package/dist/esm/RunCatalog.js +196 -0
  148. package/dist/esm/RunCatalog.js.map +1 -0
  149. package/dist/esm/SyncAuth.d.ts +70 -0
  150. package/dist/esm/SyncAuth.d.ts.map +1 -0
  151. package/dist/esm/SyncAuth.js +119 -0
  152. package/dist/esm/SyncAuth.js.map +1 -0
  153. package/dist/esm/SyncClient.d.ts +274 -0
  154. package/dist/esm/SyncClient.d.ts.map +1 -0
  155. package/dist/esm/SyncClient.js +586 -0
  156. package/dist/esm/SyncClient.js.map +1 -0
  157. package/dist/esm/SyncError.d.ts +107 -0
  158. package/dist/esm/SyncError.d.ts.map +1 -0
  159. package/dist/esm/SyncError.js +133 -0
  160. package/dist/esm/SyncError.js.map +1 -0
  161. package/dist/esm/SyncPrincipal.d.ts +100 -0
  162. package/dist/esm/SyncPrincipal.d.ts.map +1 -0
  163. package/dist/esm/SyncPrincipal.js +58 -0
  164. package/dist/esm/SyncPrincipal.js.map +1 -0
  165. package/dist/esm/SyncProtocol.d.ts +592 -0
  166. package/dist/esm/SyncProtocol.d.ts.map +1 -0
  167. package/dist/esm/SyncProtocol.js +434 -0
  168. package/dist/esm/SyncProtocol.js.map +1 -0
  169. package/dist/esm/SyncRpcs.d.ts +92 -0
  170. package/dist/esm/SyncRpcs.d.ts.map +1 -0
  171. package/dist/esm/SyncRpcs.js +35 -0
  172. package/dist/esm/SyncRpcs.js.map +1 -0
  173. package/dist/esm/SyncServer.d.ts +184 -0
  174. package/dist/esm/SyncServer.d.ts.map +1 -0
  175. package/dist/esm/SyncServer.js +897 -0
  176. package/dist/esm/SyncServer.js.map +1 -0
  177. package/dist/esm/WorkspaceShare.d.ts +174 -0
  178. package/dist/esm/WorkspaceShare.d.ts.map +1 -0
  179. package/dist/esm/WorkspaceShare.js +208 -0
  180. package/dist/esm/WorkspaceShare.js.map +1 -0
  181. package/dist/esm/index.d.ts +91 -0
  182. package/dist/esm/index.d.ts.map +1 -0
  183. package/dist/esm/index.js +91 -0
  184. package/dist/esm/index.js.map +1 -0
  185. package/dist/esm/internal/Admission.d.ts +43 -0
  186. package/dist/esm/internal/Admission.d.ts.map +1 -0
  187. package/dist/esm/internal/Admission.js +80 -0
  188. package/dist/esm/internal/Admission.js.map +1 -0
  189. package/dist/esm/internal/CauseText.d.ts +55 -0
  190. package/dist/esm/internal/CauseText.d.ts.map +1 -0
  191. package/dist/esm/internal/CauseText.js +67 -0
  192. package/dist/esm/internal/CauseText.js.map +1 -0
  193. package/dist/esm/internal/JournalErrorCode.d.ts +23 -0
  194. package/dist/esm/internal/JournalErrorCode.d.ts.map +1 -0
  195. package/dist/esm/internal/JournalErrorCode.js +44 -0
  196. package/dist/esm/internal/JournalErrorCode.js.map +1 -0
  197. package/dist/esm/internal/PolicyOptions.d.ts +57 -0
  198. package/dist/esm/internal/PolicyOptions.d.ts.map +1 -0
  199. package/dist/esm/internal/PolicyOptions.js +70 -0
  200. package/dist/esm/internal/PolicyOptions.js.map +1 -0
  201. package/dist/esm/internal/ProtocolVersion.d.ts +17 -0
  202. package/dist/esm/internal/ProtocolVersion.d.ts.map +1 -0
  203. package/dist/esm/internal/ProtocolVersion.js +24 -0
  204. package/dist/esm/internal/ProtocolVersion.js.map +1 -0
  205. package/dist/esm/internal/ShareSigner.d.ts +173 -0
  206. package/dist/esm/internal/ShareSigner.d.ts.map +1 -0
  207. package/dist/esm/internal/ShareSigner.js +219 -0
  208. package/dist/esm/internal/ShareSigner.js.map +1 -0
  209. package/dist/esm/internal/SnapshotBoundary.d.ts +17 -0
  210. package/dist/esm/internal/SnapshotBoundary.d.ts.map +1 -0
  211. package/dist/esm/internal/SnapshotBoundary.js +68 -0
  212. package/dist/esm/internal/SnapshotBoundary.js.map +1 -0
  213. package/dist/esm/test/TestSocket.d.ts +55 -0
  214. package/dist/esm/test/TestSocket.d.ts.map +1 -0
  215. package/dist/esm/test/TestSocket.js +128 -0
  216. package/dist/esm/test/TestSocket.js.map +1 -0
  217. package/dist/esm/test/TestSync.d.ts +54 -0
  218. package/dist/esm/test/TestSync.d.ts.map +1 -0
  219. package/dist/esm/test/TestSync.js +76 -0
  220. package/dist/esm/test/TestSync.js.map +1 -0
  221. package/docs/README.md +156 -0
  222. package/docs/api.md +328 -0
  223. package/docs/concepts/authorization.md +117 -0
  224. package/docs/concepts/branches.md +195 -0
  225. package/docs/concepts/compaction.md +144 -0
  226. package/docs/concepts/replay-then-follow.md +129 -0
  227. package/docs/concepts/scopes-and-cursors.md +135 -0
  228. package/docs/guides/authorize-a-connection.md +174 -0
  229. package/docs/guides/follow-a-run.md +174 -0
  230. package/docs/guides/handle-a-compacted-run.md +158 -0
  231. package/docs/guides/list-workspace-runs.md +120 -0
  232. package/docs/guides/run-a-long-soak.md +80 -0
  233. package/docs/guides/serve-the-read-path.md +146 -0
  234. package/docs/guides/test-a-follower.md +273 -0
  235. package/docs/installation.md +92 -0
  236. package/docs/protocol.md +183 -0
  237. package/docs/quickstart.md +159 -0
  238. package/docs/troubleshooting.md +234 -0
  239. package/package.json +304 -3
  240. package/src/BranchCommands.ts +684 -0
  241. package/src/BranchIds.ts +76 -0
  242. package/src/BranchPresence.ts +512 -0
  243. package/src/BranchProjection.ts +308 -0
  244. package/src/BranchProtocol.ts +411 -0
  245. package/src/BranchRpcs.ts +163 -0
  246. package/src/BranchServer.ts +202 -0
  247. package/src/BranchShare.ts +297 -0
  248. package/src/RunCatalog.ts +308 -0
  249. package/src/SyncAuth.ts +146 -0
  250. package/src/SyncClient.ts +902 -0
  251. package/src/SyncError.ts +147 -0
  252. package/src/SyncPrincipal.ts +115 -0
  253. package/src/SyncProtocol.ts +594 -0
  254. package/src/SyncRpcs.ts +40 -0
  255. package/src/SyncServer.ts +1184 -0
  256. package/src/WorkspaceShare.ts +292 -0
  257. package/src/index.ts +107 -0
  258. package/src/internal/Admission.ts +119 -0
  259. package/src/internal/CauseText.ts +74 -0
  260. package/src/internal/JournalErrorCode.ts +46 -0
  261. package/src/internal/PolicyOptions.ts +97 -0
  262. package/src/internal/ProtocolVersion.ts +28 -0
  263. package/src/internal/ShareSigner.ts +300 -0
  264. package/src/internal/SnapshotBoundary.ts +91 -0
  265. package/src/test/TestSocket.ts +192 -0
  266. package/src/test/TestSync.ts +113 -0
@@ -0,0 +1,496 @@
1
+ /**
2
+ * Idempotent admission of collaborative commands onto a branch document.
3
+ *
4
+ * Three different things look identical from the server's side — an optimistic
5
+ * client re-sending after a timeout, a reconnecting client flushing its
6
+ * outbox, and two people pressing the same button at once — and all three must
7
+ * produce exactly one durable command. The client-minted `commandId` is the
8
+ * idempotency key for all three.
9
+ *
10
+ * The exactly-once constraint is durable, not process-local: every append
11
+ * carries the producer identity `(branch run, commandSourceId(commandId),
12
+ * commandSourceSeq)`, which the journal enforces inside its own write
13
+ * transaction. Two independently constructed servers that race the same
14
+ * command therefore collide in the journal — one appends, the other receives
15
+ * a duplicate receipt or an idempotency conflict and resolves the canonical
16
+ * sequence by replaying the branch (audit finding F-14). The in-memory
17
+ * ledger, permit, and replay cursor are a fast path only: they answer known
18
+ * duplicates without a journal write and keep a restarted server from
19
+ * re-executing history, but correctness never depends on them.
20
+ *
21
+ * @since 0.1.0
22
+ */
23
+ import { Journal } from "@smthrs/journal";
24
+ import * as JournalEvent from "@smthrs/journal/JournalEvent";
25
+ import * as Context from "effect/Context";
26
+ import * as Effect from "effect/Effect";
27
+ import * as Layer from "effect/Layer";
28
+ import * as RcMap from "effect/RcMap";
29
+ import * as Semaphore from "effect/Semaphore";
30
+ import { branchRunId, CommandEvent, CommandReceipt, commandSourceId, commandSourceSeq, CommandSubmission, SubmitRequest } from "./BranchProtocol.js";
31
+ import { maximumBranchTtlMs } from "./BranchRpcs.js";
32
+ import * as BranchShare from "./BranchShare.js";
33
+ import * as Admission from "./internal/Admission.js";
34
+ import { causeCode } from "./internal/CauseText.js";
35
+ import { journalErrorCode } from "./internal/JournalErrorCode.js";
36
+ import { positiveInt } from "./internal/PolicyOptions.js";
37
+ import { SyncError } from "./SyncError.js";
38
+ import * as SyncProtocol from "./SyncProtocol.js";
39
+ /**
40
+ * The branch command ledger.
41
+ *
42
+ * @category services
43
+ * @since 0.1.0
44
+ */
45
+ export class BranchCommands extends Context.Service()("@smthrs/sync/BranchCommands") {
46
+ }
47
+ /**
48
+ * Constructs a command ledger that admits nothing.
49
+ *
50
+ * @category constructors
51
+ * @since 0.1.0
52
+ */
53
+ export const makeNoop = (overrides = {}) => BranchCommands.of({
54
+ submit: () => Effect.fail(new SyncError({ code: "closed", message: "Branch commands are unavailable" })),
55
+ ...overrides
56
+ });
57
+ /**
58
+ * Provides a command ledger that admits nothing.
59
+ *
60
+ * @category layers
61
+ * @since 0.1.0
62
+ */
63
+ export const layerNoop = Layer.succeed(BranchCommands, makeNoop());
64
+ /**
65
+ * Projects a journal write failure onto the sync boundary.
66
+ *
67
+ * The public `message` is a constant and the `cause` names the failure's type
68
+ * only. A branch writer may hold nothing but a share link, and the journal's
69
+ * own message is the SQLite driver's, which carries SQL text, table and column
70
+ * names, and constraint identifiers. The CODE is a different question from the
71
+ * message: a journal code this boundary also declares crosses as that code, so
72
+ * a writer whose commit was lost to a closing journal is told that rather than
73
+ * being told nothing.
74
+ */
75
+ const journalFailure = (cause) => new SyncError({
76
+ code: journalErrorCode(cause),
77
+ message: "Branch journal write failed",
78
+ cause: causeCode(cause)
79
+ });
80
+ /** How many entries one rehydration page reads. */
81
+ const pageSize = 256;
82
+ /**
83
+ * Receipts one branch keeps in memory before the oldest are evicted.
84
+ *
85
+ * The ledger is a fast path: it answers a known duplicate without a journal
86
+ * write, and losing an entry costs a round trip, never correctness, because
87
+ * the journal's own producer identity is the durable exactly-once constraint
88
+ * and returns a `Duplicate` receipt for a command already admitted. Retaining
89
+ * one receipt per command forever meant a process serving a hundred branches
90
+ * of a hundred thousand commands held ten million of them, so the fast path
91
+ * is bounded and the durable path is what makes it safe to bound.
92
+ *
93
+ * @category constants
94
+ * @since 1.0.0-rc.0
95
+ */
96
+ export const defaultLedgerCapacity = 4096;
97
+ /**
98
+ * Inclusive UTF-8 JSON submission ceiling. The separate sync entry ceiling
99
+ * leaves room for the durable envelope around every admitted command.
100
+ *
101
+ * @category constants
102
+ * @since 1.0.0-rc.0
103
+ */
104
+ export const defaultMaxCommandBytes = 1024 * 1024;
105
+ /**
106
+ * Milliseconds a branch may have no active submissions before its in-memory state is
107
+ * dropped.
108
+ *
109
+ * The receipt ledger is bounded per branch; this bounds the branches. A
110
+ * dropped branch re-hydrates on its next submission exactly as it does after
111
+ * a restart, so eviction costs a journal read, never correctness. The default
112
+ * is the longest lifetime a branch capability may carry, so a branch is only
113
+ * dropped once every capability minted at its last touch can have expired.
114
+ *
115
+ * @category constants
116
+ * @since 1.0.0-rc.0
117
+ */
118
+ export const defaultBranchIdleMs = maximumBranchTtlMs;
119
+ /**
120
+ * Entries one branch's first-touch hydration reads before it stops.
121
+ *
122
+ * Hydration is the only full history read that ever sat on the WRITE path: the
123
+ * first submission a process makes to a branch paged that branch's whole
124
+ * durable log, 256 entries at a time, while holding the branch's admission
125
+ * permit, before its own append was attempted. A hundred-thousand-entry branch
126
+ * therefore charged its next writer four hundred journal reads for a fast path
127
+ * that keeps four thousand receipts.
128
+ *
129
+ * The budget defaults to {@link defaultLedgerCapacity} because hydration
130
+ * cannot seed more receipts than the ledger holds: everything a longer walk
131
+ * records displaces something the same walk already recorded. What the extra
132
+ * reads buy is a FRESHER selection — the tail of history rather than its head
133
+ * — and the price is an unbounded wait on somebody's write. This trades that
134
+ * back: the walk stops, the cursor stays where it stopped, and a later replay
135
+ * continues forward from there.
136
+ *
137
+ * A branch longer than the budget starts with a partly cold fast path and
138
+ * nothing else. Correctness never rested on the ledger: the journal's producer
139
+ * identity is the durable exactly-once constraint, an exact retry receives a
140
+ * `Duplicate` receipt carrying the canonical sequence, and an
141
+ * `idempotency_conflict` is resolved by a read of the whole history that the
142
+ * ledger's own bound already made necessary.
143
+ *
144
+ * @category constants
145
+ * @since 1.0.0-rc.0
146
+ */
147
+ export const defaultHydrationLimit = defaultLedgerCapacity;
148
+ /** The ledger over an already-validated policy. */
149
+ const makeWith = (resolved) => Effect.gen(function* () {
150
+ const journal = yield* Journal.Journal;
151
+ const share = yield* BranchShare.BranchShare;
152
+ const { branchIdleMs, hydrationLimit, ledgerCapacity, maxCommandBytes, maxFrameBytes } = resolved;
153
+ // Active submissions share one state; its idle lifetime starts at release.
154
+ const branches = yield* RcMap.make({
155
+ idleTimeToLive: branchIdleMs,
156
+ lookup: (_branchId) => Effect.sync(() => ({
157
+ permit: Semaphore.makeUnsafe(1),
158
+ receipts: new Map(),
159
+ cursor: undefined,
160
+ hydrated: false
161
+ }))
162
+ });
163
+ /**
164
+ * Records one receipt, evicting the branch's oldest once the branch is
165
+ * at capacity. `Map` iterates in insertion order, so the first key is
166
+ * the least recently recorded one; re-recording a known command
167
+ * refreshes it in place rather than growing the branch.
168
+ */
169
+ const recordIn = (branch, receipt) => {
170
+ branch.delete(receipt.commandId);
171
+ branch.set(receipt.commandId, receipt);
172
+ for (const oldest of branch.keys()) {
173
+ if (branch.size <= ledgerCapacity)
174
+ break;
175
+ branch.delete(oldest);
176
+ }
177
+ };
178
+ const duplicateOf = (known) => new CommandReceipt({
179
+ branchId: known.branchId,
180
+ commandId: known.commandId,
181
+ status: "duplicate",
182
+ seq: known.seq
183
+ });
184
+ /**
185
+ * Pages a branch's journal forward from `from`, handing each entry to
186
+ * `visit` along with the command identity it carries, if any, and
187
+ * stopping once it has visited `budget` entries. Admission already
188
+ * decoded every command payload in full, so the identity is read off
189
+ * that decoded command rather than decoded a second time.
190
+ *
191
+ * The budget is entries VISITED, not pages read, so a caller names a
192
+ * bound in the unit its own memory is measured in.
193
+ * `Number.POSITIVE_INFINITY` is the unbounded walk the recovery reads
194
+ * take: they answer a question about the whole durable history, and a
195
+ * bounded answer to that question is a wrong one.
196
+ */
197
+ const walk = (branchId, from, budget, visit) => Effect.gen(function* () {
198
+ const runId = branchRunId(branchId);
199
+ let after = from;
200
+ let hasMore = true;
201
+ let remaining = budget;
202
+ while (hasMore && remaining > 0) {
203
+ const page = yield* journal.entries({
204
+ runId,
205
+ ...(after === undefined ? {} : { after }),
206
+ limit: Math.min(pageSize, remaining)
207
+ }).pipe(Effect.mapError(journalFailure));
208
+ const admitted = yield* Admission.withCommands(page.entries, runId, after ?? -1);
209
+ for (const { command, entry } of admitted) {
210
+ after = entry.seq;
211
+ remaining -= 1;
212
+ visit(entry, command?.commandId);
213
+ }
214
+ // An empty page ends the walk whatever the page claims, the same
215
+ // guard `SyncServer.tail` carries: `after` cannot move, so the
216
+ // next read is byte-identical and the loop would never terminate
217
+ // — while holding this branch's admission permit.
218
+ if (page.entries.length === 0)
219
+ return;
220
+ hasMore = page.hasMore;
221
+ }
222
+ });
223
+ /**
224
+ * Replays the branch journal forward from the last replayed sequence into
225
+ * the ledger. Used once on first touch, so a process that restarts
226
+ * mid-collaboration still recognises every command it already admitted,
227
+ * and again after an admission conflict, to read the command another
228
+ * writer admitted after this process last looked.
229
+ *
230
+ * The staged cursor tracks each visited entry. A successful bounded walk
231
+ * commits exactly where it stopped, so the next replay resumes there.
232
+ * Failure or interruption discards the staged cursor and receipts together.
233
+ */
234
+ const replay = (branchId, state, budget) => Effect.gen(function* () {
235
+ // Stage at most ledgerCapacity receipts. A malformed later page or an
236
+ // interruption must leave both the ledger and replay cursor untouched.
237
+ const staged = new Map(state.receipts);
238
+ let position = state.cursor;
239
+ yield* walk(branchId, position, budget, (entry, commandId) => {
240
+ position = entry.seq;
241
+ if (commandId !== undefined) {
242
+ recordIn(staged, new CommandReceipt({ branchId, commandId, status: "admitted", seq: entry.seq }));
243
+ }
244
+ });
245
+ state.cursor = position;
246
+ state.receipts = staged;
247
+ });
248
+ /**
249
+ * The sequence one command was admitted at, read from the branch's whole
250
+ * durable history rather than from the ledger.
251
+ *
252
+ * The ledger is BOUNDED, so the receipt for a command admitted far
253
+ * enough back has been evicted, and a forward replay from the ledger's
254
+ * own cursor can never bring it back — the entry is below that cursor.
255
+ * The durable log still holds it, and this is the read that says so.
256
+ * Neither the cursor nor the ledger moves: this answers one question
257
+ * about history and leaves the fast path exactly as it found it.
258
+ */
259
+ const admittedSeq = (branchId, commandId) => Effect.gen(function* () {
260
+ let found;
261
+ yield* walk(branchId, undefined, Number.POSITIVE_INFINITY, (entry, admitted) => {
262
+ if (admitted === commandId)
263
+ found = entry.seq;
264
+ });
265
+ return found;
266
+ });
267
+ /**
268
+ * Seeds the branch's fast path ONCE, from its durable history, within
269
+ * the configured budget (see {@link defaultHydrationLimit}).
270
+ *
271
+ * It runs on the write path, inside the branch's admission permit, so
272
+ * the budget is what keeps the length of a branch's history out of the
273
+ * latency of the next submission to it. The branch is marked hydrated
274
+ * whether or not the walk reached the tail, because the point of the
275
+ * budget is that no submission pays for the rest: what the walk did not
276
+ * reach is answered by the journal, and by the unbounded replay
277
+ * {@link lostRace} runs when the journal reports a conflict.
278
+ */
279
+ const hydrate = (branchId, state) => Effect.gen(function* () {
280
+ if (state.hydrated)
281
+ return;
282
+ yield* replay(branchId, state, hydrationLimit);
283
+ state.hydrated = true;
284
+ });
285
+ /**
286
+ * The losing side of a cross-server admission race: the journal refused
287
+ * this append because another writer already holds the command's producer
288
+ * identity with different content — same `commandId`, different
289
+ * participant or arguments. The original admission is durable, so
290
+ * replaying the branch forward must surface it; a replay that does not is
291
+ * a journal whose conflict report and entries disagree, and that failure
292
+ * is reported honestly instead of being masked as a duplicate.
293
+ *
294
+ * Two reads, because the ledger is bounded and the answer must not be.
295
+ * The forward replay picks up an admission another writer landed after
296
+ * this process last looked. A command this process admitted itself and
297
+ * has since evicted is BELOW that cursor, so only a read of the whole
298
+ * history finds it — and without one, bounding the ledger turned an
299
+ * ordinary duplicate into a report that the journal contradicts itself.
300
+ */
301
+ const lostRace = (submission, state, cause) => Effect.gen(function* () {
302
+ // Unbounded, unlike hydration: this is the recovery read, and the
303
+ // sequence it is looking for may sit anywhere in the history.
304
+ yield* replay(submission.branchId, state, Number.POSITIVE_INFINITY);
305
+ const known = state.receipts.get(submission.commandId);
306
+ if (known !== undefined)
307
+ return duplicateOf(known);
308
+ // A branch still under the ledger's capacity has never evicted
309
+ // anything, and `replay` has just read this branch to its tail, so a
310
+ // command missing from the ledger is missing from the journal too.
311
+ // Reading the whole history to confirm that would let one small
312
+ // request cost one full log scan.
313
+ const mayHaveEvicted = state.receipts.size >= ledgerCapacity;
314
+ const seq = mayHaveEvicted
315
+ ? yield* admittedSeq(submission.branchId, submission.commandId)
316
+ : undefined;
317
+ if (seq === undefined)
318
+ return yield* Effect.fail(journalFailure(cause));
319
+ return new CommandReceipt({
320
+ branchId: submission.branchId,
321
+ commandId: submission.commandId,
322
+ status: "duplicate",
323
+ seq
324
+ });
325
+ });
326
+ const journalInput = (submission, capabilityId) => new JournalEvent.Input({
327
+ runId: branchRunId(submission.branchId),
328
+ sourceId: commandSourceId(submission.commandId),
329
+ sourceSeq: commandSourceSeq,
330
+ eventType: CommandEvent,
331
+ payload: {
332
+ branchId: submission.branchId,
333
+ commandId: submission.commandId,
334
+ participantId: submission.participantId,
335
+ name: submission.name,
336
+ args: submission.args,
337
+ target: submission.target,
338
+ capabilityId
339
+ },
340
+ meta: null
341
+ });
342
+ const admit = (request, capabilityId, state) => Effect.gen(function* () {
343
+ const submission = request.submission;
344
+ yield* hydrate(submission.branchId, state);
345
+ const known = state.receipts.get(submission.commandId);
346
+ if (known !== undefined)
347
+ return duplicateOf(known);
348
+ // Unfenced: the sync command journal is a multi-writer admission
349
+ // log — participants own no branch run, and command admissions are
350
+ // first-writer-wins on the command id.
351
+ const receipt = yield* journal.emitDurableUnfenced(journalInput(submission, capabilityId)).pipe(
352
+ // A `Duplicate` receipt is another writer landing the identical
353
+ // submission first: the journal deduplicated durably and returned
354
+ // the canonical sequence the original append committed at.
355
+ Effect.map((accepted) => new CommandReceipt({
356
+ branchId: submission.branchId,
357
+ commandId: submission.commandId,
358
+ status: accepted._tag === "Duplicate" ? "duplicate" : "admitted",
359
+ seq: accepted.seq
360
+ })), Effect.catch((cause) => cause.code === "idempotency_conflict"
361
+ ? lostRace(submission, state, cause)
362
+ : Effect.fail(journalFailure(cause))));
363
+ recordIn(state.receipts, new CommandReceipt({
364
+ branchId: submission.branchId,
365
+ commandId: submission.commandId,
366
+ status: "admitted",
367
+ seq: receipt.seq
368
+ }));
369
+ return receipt;
370
+ });
371
+ const submit = Effect.fn("BranchCommands.submit")(function* (supplied) {
372
+ const decoded = yield* Admission.decode(SubmitRequest, supplied, "invalid_request");
373
+ // Freeze the authorized submission across asynchronous signature verification.
374
+ const request = { capability: decoded.capability, submission: { ...decoded.submission } };
375
+ yield* Effect.annotateCurrentSpan({
376
+ branchId: request.submission.branchId,
377
+ commandId: request.submission.commandId,
378
+ participantId: request.submission.participantId
379
+ });
380
+ // The verified capability, not the client's `participantId`, is who
381
+ // submitted: it is stamped on the durable payload for attribution.
382
+ const claims = yield* share.verify(request.capability, {
383
+ branchId: request.submission.branchId,
384
+ access: "write"
385
+ });
386
+ const bytes = SyncProtocol.encodedByteLength(request.submission);
387
+ if (bytes > maxCommandBytes) {
388
+ // Refused BEFORE the append: an oversized command must never reach
389
+ // the journal, and therefore never reaches any follower.
390
+ return yield* Effect.fail(new SyncError({
391
+ code: "frame_too_large",
392
+ message: `Encoded command submission of ${bytes} bytes exceeds the ${maxCommandBytes}-byte ceiling`
393
+ }));
394
+ }
395
+ const input = journalInput(request.submission, claims.capabilityId);
396
+ // The journal owns these fields. Reserve their largest encoded forms,
397
+ // so acceptance does not depend on wall clock or current history length.
398
+ // Event identity repeats run/source IDs; their escaping and UTF-8 cost
399
+ // must be measured, not replaced by a fixed envelope allowance.
400
+ const entryBytes = SyncProtocol.encodedByteLength({
401
+ ...input,
402
+ seq: Number.MAX_SAFE_INTEGER - 1,
403
+ eventId: JournalEvent.makeEventId(input.runId, input.sourceId, commandSourceSeq),
404
+ emittedAtMs: Number.MAX_VALUE
405
+ });
406
+ if (entryBytes > maxFrameBytes) {
407
+ return yield* Effect.fail(new SyncError({
408
+ code: "frame_too_large",
409
+ message: `Encoded command journal entry requires up to ${entryBytes} bytes, exceeding the ${maxFrameBytes}-byte frame ceiling`
410
+ }));
411
+ }
412
+ // The permit is taken AFTER authorization so an unauthorized caller
413
+ // cannot serialize (and therefore stall) legitimate collaborators,
414
+ // and it is this BRANCH's permit so a slow branch stalls only itself.
415
+ return yield* RcMap.get(branches, request.submission.branchId).pipe(Effect.flatMap((state) => state.permit.withPermit(admit(request, claims.capabilityId, state))), Effect.scoped);
416
+ });
417
+ return BranchCommands.of({ submit });
418
+ });
419
+ const defaults = {
420
+ maxCommandBytes: defaultMaxCommandBytes,
421
+ maxFrameBytes: SyncProtocol.defaultMaxFrameBytes,
422
+ ledgerCapacity: defaultLedgerCapacity,
423
+ hydrationLimit: defaultHydrationLimit,
424
+ branchIdleMs: defaultBranchIdleMs
425
+ };
426
+ /**
427
+ * Constructs the journal-backed branch command ledger under an explicit
428
+ * policy. The caller scope owns cached states and their idle-expiration timers.
429
+ *
430
+ * A submission whose encoded form exceeds the command ceiling is refused with
431
+ * `frame_too_large` before anything is appended, so one oversized `args`
432
+ * cannot enter the branch journal and poison every follower that replays it.
433
+ *
434
+ * First-touch hydration reads at most `hydrationLimit` entries, so the length
435
+ * of a branch's history is not charged to the next writer's latency; see
436
+ * {@link defaultHydrationLimit} for why the fast path may be seeded partially
437
+ * without any effect on what the ledger admits.
438
+ *
439
+ * Every option is validated as a positive safe integer at construction: the
440
+ * TypeScript type says `number`, and `NaN` silently disabled the ceiling it
441
+ * was compared against.
442
+ *
443
+ * @category constructors
444
+ * @since 0.1.0
445
+ */
446
+ export const makeLiveWith = (options = {}) => Effect.flatMap(Effect.all({
447
+ maxFrameBytes: positiveInt("BranchCommands.Options.maxFrameBytes", options.maxFrameBytes, defaults.maxFrameBytes),
448
+ maxCommandBytes: positiveInt("BranchCommands.Options.maxCommandBytes", options.maxCommandBytes, defaults.maxCommandBytes),
449
+ ledgerCapacity: positiveInt("BranchCommands.Options.ledgerCapacity", options.ledgerCapacity, defaults.ledgerCapacity),
450
+ hydrationLimit: positiveInt("BranchCommands.Options.hydrationLimit", options.hydrationLimit, defaults.hydrationLimit),
451
+ branchIdleMs: positiveInt("BranchCommands.Options.branchIdleMs", options.branchIdleMs, defaults.branchIdleMs)
452
+ }), makeWith);
453
+ /**
454
+ * Constructs the journal-backed branch command ledger with default policy.
455
+ * The caller scope owns cached states and their idle-expiration timers.
456
+ *
457
+ * @category constructors
458
+ * @since 0.1.0
459
+ */
460
+ export const makeLive = makeWith(defaults);
461
+ /**
462
+ * Provides the journal-backed branch command ledger.
463
+ *
464
+ * @category layers
465
+ * @since 0.1.0
466
+ */
467
+ export const layer = Layer.effect(BranchCommands, makeLive);
468
+ /**
469
+ * Provides the journal-backed branch command ledger under an explicit policy.
470
+ * Fails with `invalid_request` when an option is not a positive safe integer.
471
+ *
472
+ * @category layers
473
+ * @since 0.1.0
474
+ */
475
+ export const layerWith = (options) => Layer.effect(BranchCommands, makeLiveWith(options));
476
+ /**
477
+ * Builds a submission, filling in the fields a plain command never sets, and
478
+ * refusing a field the schema forbids with a `SyncError` rather than a defect.
479
+ *
480
+ * @category constructors
481
+ * @since 0.1.0
482
+ */
483
+ export const submission = (fields) =>
484
+ // Decoded, not constructed: `name` is a `NonEmptyString` the parameter type
485
+ // admits `""` for, and `new CommandSubmission` throws on it. A builder that
486
+ // dies on a caller's argument is the same defect `submit` already refuses
487
+ // typed, one call earlier.
488
+ Admission.decode(CommandSubmission, {
489
+ branchId: fields.branchId,
490
+ commandId: fields.commandId,
491
+ participantId: fields.participantId,
492
+ name: fields.name,
493
+ args: fields.args ?? "",
494
+ target: fields.target ?? ""
495
+ }, "invalid_request");
496
+ //# sourceMappingURL=BranchCommands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BranchCommands.js","sourceRoot":"","sources":["../../src/BranchCommands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AACzC,OAAO,KAAK,YAAY,MAAM,8BAA8B,CAAA;AAC5D,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC,OAAO,KAAK,SAAS,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAEL,WAAW,EACX,YAAY,EAEZ,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACd,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,SAAS,MAAM,yBAAyB,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AAYjD;;;;;GAKG;AACH,MAAM,OAAO,cAAe,SAAQ,OAAO,CAAC,OAAO,EAA2B,CAAC,6BAA6B,CAAC;CAAG;AAEhH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,YAA8B,EAAE,EAAW,EAAE,CACpE,cAAc,CAAC,EAAE,CAAC;IAChB,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC,CAAC;IACxG,GAAG,SAAS;CACb,CAAC,CAAA;AAEJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAgC,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,QAAQ,EAAE,CAAC,CAAA;AAE/F;;;;;;;;;;GAUG;AACH,MAAM,cAAc,GAAG,CAAC,KAAc,EAAa,EAAE,CACnD,IAAI,SAAS,CAAC;IACZ,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC;IAC7B,OAAO,EAAE,6BAA6B;IACtC,KAAK,EAAE,SAAS,CAAC,KAAK,CAAC;CACxB,CAAC,CAAA;AAEJ,mDAAmD;AACnD,MAAM,QAAQ,GAAG,GAAG,CAAA;AAEpB;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAA;AAEzC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,CAAA;AAEjD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAA;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAA;AAqD1D,mDAAmD;AACnD,MAAM,QAAQ,GAAG,CACf,QAMC,EACuF,EAAE,CAC1F,MAAM,CAAC,GAAG,CACR,QAAQ,CAAC;IACP,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAA;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC,WAAW,CAAA;IAC5C,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,GAAG,QAAQ,CAAA;IACjG,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QACjC,cAAc,EAAE,YAAY;QAC5B,MAAM,EAAE,CAAC,SAAmB,EAAE,EAAE,CAC9B,MAAM,CAAC,IAAI,CAAC,GAAgB,EAAE,CAAC,CAAC;YAC9B,MAAM,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;YAC/B,QAAQ,EAAE,IAAI,GAAG,EAAE;YACnB,MAAM,EAAE,SAAS;YACjB,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAC;KACN,CAAC,CAAA;IAEF;;;;;OAKG;IACH,MAAM,QAAQ,GAAG,CAAC,MAAsC,EAAE,OAAuB,EAAQ,EAAE;QACzF,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QAChC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACtC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YACnC,IAAI,MAAM,CAAC,IAAI,IAAI,cAAc;gBAAE,MAAK;YACxC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACvB,CAAC;IACH,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,CAAC,KAAqB,EAAkB,EAAE,CAC5D,IAAI,cAAc,CAAC;QACjB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,MAAM,EAAE,WAAW;QACnB,GAAG,EAAE,KAAK,CAAC,GAAG;KACf,CAAC,CAAA;IAEJ;;;;;;;;;;;;OAYG;IACH,MAAM,IAAI,GAAG,CACX,QAAkB,EAClB,IAAkC,EAClC,MAAc,EACd,KAA4E,EAC5C,EAAE,CAClC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;QACnC,IAAI,KAAK,GAAG,IAAI,CAAA;QAChB,IAAI,OAAO,GAAG,IAAI,CAAA;QAClB,IAAI,SAAS,GAAG,MAAM,CAAA;QACtB,OAAO,OAAO,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;gBAClC,KAAK;gBACL,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;gBACzC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC;aACrC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAA;YACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,CAAA;YAChF,KAAK,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC1C,KAAK,GAAG,KAAK,CAAC,GAAG,CAAA;gBACjB,SAAS,IAAI,CAAC,CAAA;gBACd,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;YAClC,CAAC;YACD,iEAAiE;YACjE,+DAA+D;YAC/D,iEAAiE;YACjE,kDAAkD;YAClD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAM;YACrC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QACxB,CAAC;IACH,CAAC,CAAC,CAAA;IAEJ;;;;;;;;;;OAUG;IACH,MAAM,MAAM,GAAG,CAAC,QAAkB,EAAE,KAAkB,EAAE,MAAc,EAAkC,EAAE,CACxG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,sEAAsE;QACtE,uEAAuE;QACvE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QACtC,IAAI,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAA;QAC3B,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE;YAC3D,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAA;YACpB,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,QAAQ,CAAC,MAAM,EAAE,IAAI,cAAc,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;YACnG,CAAC;QACH,CAAC,CAAC,CAAA;QACF,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAA;QACvB,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAA;IACzB,CAAC,CAAC,CAAA;IAEJ;;;;;;;;;;OAUG;IACH,MAAM,WAAW,GAAG,CAClB,QAAkB,EAClB,SAAoB,EACoC,EAAE,CAC1D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,IAAI,KAAmC,CAAA;QACvC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;YAC7E,IAAI,QAAQ,KAAK,SAAS;gBAAE,KAAK,GAAG,KAAK,CAAC,GAAG,CAAA;QAC/C,CAAC,CAAC,CAAA;QACF,OAAO,KAAK,CAAA;IACd,CAAC,CAAC,CAAA;IAEJ;;;;;;;;;;;OAWG;IACH,MAAM,OAAO,GAAG,CAAC,QAAkB,EAAE,KAAkB,EAAkC,EAAE,CACzF,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,IAAI,KAAK,CAAC,QAAQ;YAAE,OAAM;QAC1B,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,cAAc,CAAC,CAAA;QAC9C,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAA;IACvB,CAAC,CAAC,CAAA;IAEJ;;;;;;;;;;;;;;;OAeG;IACH,MAAM,QAAQ,GAAG,CACf,UAA6B,EAC7B,KAAkB,EAClB,KAA2B,EACe,EAAE,CAC5C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,kEAAkE;QAClE,8DAA8D;QAC9D,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAA;QACnE,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;QACtD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAA;QAClD,+DAA+D;QAC/D,qEAAqE;QACrE,mEAAmE;QACnE,gEAAgE;QAChE,kCAAkC;QAClC,MAAM,cAAc,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,IAAI,cAAc,CAAA;QAC5D,MAAM,GAAG,GAAG,cAAc;YACxB,CAAC,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,SAAS,CAAC;YAC/D,CAAC,CAAC,SAAS,CAAA;QACb,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAA;QACvE,OAAO,IAAI,cAAc,CAAC;YACxB,QAAQ,EAAE,UAAU,CAAC,QAAQ;YAC7B,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,MAAM,EAAE,WAAW;YACnB,GAAG;SACJ,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEJ,MAAM,YAAY,GAAG,CAAC,UAA6B,EAAE,YAAoB,EAAsB,EAAE,CAC/F,IAAI,YAAY,CAAC,KAAK,CAAC;QACrB,KAAK,EAAE,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC;QACvC,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,SAAS,CAAC;QAC/C,SAAS,EAAE,gBAAgB;QAC3B,SAAS,EAAE,YAAY;QACvB,OAAO,EAAE;YACP,QAAQ,EAAE,UAAU,CAAC,QAAQ;YAC7B,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,aAAa,EAAE,UAAU,CAAC,aAAa;YACvC,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,YAAY;SACb;QACD,IAAI,EAAE,IAAI;KACX,CAAC,CAAA;IAEJ,MAAM,KAAK,GAAG,CACZ,OAAsB,EACtB,YAAoB,EACpB,KAAkB,EACwB,EAAE,CAC5C,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAA;QACrC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;QAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;QACtD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,WAAW,CAAC,KAAK,CAAC,CAAA;QAClD,iEAAiE;QACjE,mEAAmE;QACnE,uCAAuC;QACvC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAChD,YAAY,CAAC,UAAU,EAAE,YAAY,CAAC,CACvC,CAAC,IAAI;QACJ,gEAAgE;QAChE,kEAAkE;QAClE,2DAA2D;QAC3D,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CACtB,IAAI,cAAc,CAAC;YACjB,QAAQ,EAAE,UAAU,CAAC,QAAQ;YAC7B,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,MAAM,EAAE,QAAQ,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU;YAChE,GAAG,EAAE,QAAQ,CAAC,GAAG;SAClB,CAAC,CACH,EACD,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CACrB,KAAK,CAAC,IAAI,KAAK,sBAAsB;YACnC,CAAC,CAAC,QAAQ,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC;YACpC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CACvC,CACF,CAAA;QACD,QAAQ,CACN,KAAK,CAAC,QAAQ,EACd,IAAI,cAAc,CAAC;YACjB,QAAQ,EAAE,UAAU,CAAC,QAAQ;YAC7B,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,MAAM,EAAE,UAAU;YAClB,GAAG,EAAE,OAAO,CAAC,GAAG;SACjB,CAAC,CACH,CAAA;QACD,OAAO,OAAO,CAAA;IAChB,CAAC,CAAC,CAAA;IAEJ,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,QAAQ,CAAC,EAAC,QAAuB;QACjF,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAA;QACnF,+EAA+E;QAC/E,MAAM,OAAO,GAAG,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,EAAE,CAAA;QACzF,KAAK,CAAC,CAAC,MAAM,CAAC,mBAAmB,CAAC;YAChC,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ;YACrC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,SAAS;YACvC,aAAa,EAAE,OAAO,CAAC,UAAU,CAAC,aAAa;SAChD,CAAC,CAAA;QACF,oEAAoE;QACpE,mEAAmE;QACnE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE;YACrD,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ;YACrC,MAAM,EAAE,OAAO;SAChB,CAAC,CAAA;QACF,MAAM,KAAK,GAAG,YAAY,CAAC,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;QAChE,IAAI,KAAK,GAAG,eAAe,EAAE,CAAC;YAC5B,mEAAmE;YACnE,yDAAyD;YACzD,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CACvB,IAAI,SAAS,CAAC;gBACZ,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,iCAAiC,KAAK,sBAAsB,eAAe,eAAe;aACpG,CAAC,CACH,CAAA;QACH,CAAC;QACD,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;QACnE,sEAAsE;QACtE,yEAAyE;QACzE,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,UAAU,GAAG,YAAY,CAAC,iBAAiB,CAAC;YAChD,GAAG,KAAK;YACR,GAAG,EAAE,MAAM,CAAC,gBAAgB,GAAG,CAAC;YAChC,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,gBAAgB,CAAC;YAChF,WAAW,EAAE,MAAM,CAAC,SAAS;SAC9B,CAAC,CAAA;QACF,IAAI,UAAU,GAAG,aAAa,EAAE,CAAC;YAC/B,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CACvB,IAAI,SAAS,CAAC;gBACZ,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EACL,gDAAgD,UAAU,yBAAyB,aAAa,qBAAqB;aACxH,CAAC,CACH,CAAA;QACH,CAAC;QACD,oEAAoE;QACpE,mEAAmE;QACnE,sEAAsE;QACtE,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CACjE,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,EAC9F,MAAM,CAAC,MAAM,CACd,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,OAAO,cAAc,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;AACtC,CAAC,CACF,CAAA;AAEH,MAAM,QAAQ,GAAG;IACf,eAAe,EAAE,sBAAsB;IACvC,aAAa,EAAE,YAAY,CAAC,oBAAoB;IAChD,cAAc,EAAE,qBAAqB;IACrC,cAAc,EAAE,qBAAqB;IACrC,YAAY,EAAE,mBAAmB;CAClC,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,UAAmB,EAAE,EACuE,EAAE,CAC9F,MAAM,CAAC,OAAO,CACZ,MAAM,CAAC,GAAG,CAAC;IACT,aAAa,EAAE,WAAW,CACxB,sCAAsC,EACtC,OAAO,CAAC,aAAa,EACrB,QAAQ,CAAC,aAAa,CACvB;IACD,eAAe,EAAE,WAAW,CAC1B,wCAAwC,EACxC,OAAO,CAAC,eAAe,EACvB,QAAQ,CAAC,eAAe,CACzB;IACD,cAAc,EAAE,WAAW,CACzB,uCAAuC,EACvC,OAAO,CAAC,cAAc,EACtB,QAAQ,CAAC,cAAc,CACxB;IACD,cAAc,EAAE,WAAW,CACzB,uCAAuC,EACvC,OAAO,CAAC,cAAc,EACtB,QAAQ,CAAC,cAAc,CACxB;IACD,YAAY,EAAE,WAAW,CACvB,qCAAqC,EACrC,OAAO,CAAC,YAAY,EACpB,QAAQ,CAAC,YAAY,CACtB;CACF,CAAC,EACF,QAAQ,CACT,CAAA;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GACnB,QAAQ,CAAC,QAAQ,CAAC,CAAA;AAEpB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAkF,KAAK,CAAC,MAAM,CAC9G,cAAc,EACd,QAAQ,CACT,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,OAAgB,EACmE,EAAE,CACrF,KAAK,CAAC,MAAM,CAAC,cAAc,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAO1B,EAA+C,EAAE;AAChD,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,2BAA2B;AAC3B,SAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE;IAClC,QAAQ,EAAE,MAAM,CAAC,QAAQ;IACzB,SAAS,EAAE,MAAM,CAAC,SAAS;IAC3B,aAAa,EAAE,MAAM,CAAC,aAAa;IACnC,IAAI,EAAE,MAAM,CAAC,IAAI;IACjB,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;CAC5B,EAAE,iBAAiB,CAAC,CAAA","sourcesContent":["/**\n * Idempotent admission of collaborative commands onto a branch document.\n *\n * Three different things look identical from the server's side — an optimistic\n * client re-sending after a timeout, a reconnecting client flushing its\n * outbox, and two people pressing the same button at once — and all three must\n * produce exactly one durable command. The client-minted `commandId` is the\n * idempotency key for all three.\n *\n * The exactly-once constraint is durable, not process-local: every append\n * carries the producer identity `(branch run, commandSourceId(commandId),\n * commandSourceSeq)`, which the journal enforces inside its own write\n * transaction. Two independently constructed servers that race the same\n * command therefore collide in the journal — one appends, the other receives\n * a duplicate receipt or an idempotency conflict and resolves the canonical\n * sequence by replaying the branch (audit finding F-14). The in-memory\n * ledger, permit, and replay cursor are a fast path only: they answer known\n * duplicates without a journal write and keep a restarted server from\n * re-executing history, but correctness never depends on them.\n *\n * @since 0.1.0\n */\n\nimport { Journal } from \"@smthrs/journal\"\nimport * as JournalEvent from \"@smthrs/journal/JournalEvent\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as RcMap from \"effect/RcMap\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Semaphore from \"effect/Semaphore\"\nimport {\n type BranchId,\n branchRunId,\n CommandEvent,\n type CommandId,\n CommandReceipt,\n commandSourceId,\n commandSourceSeq,\n CommandSubmission,\n SubmitRequest\n} from \"./BranchProtocol.ts\"\nimport { maximumBranchTtlMs } from \"./BranchRpcs.ts\"\nimport * as BranchShare from \"./BranchShare.ts\"\nimport * as Admission from \"./internal/Admission.ts\"\nimport { causeCode } from \"./internal/CauseText.ts\"\nimport { journalErrorCode } from \"./internal/JournalErrorCode.ts\"\nimport { positiveInt } from \"./internal/PolicyOptions.ts\"\nimport { SyncError } from \"./SyncError.ts\"\nimport * as SyncProtocol from \"./SyncProtocol.ts\"\n\n/**\n * Branch command admission operations.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Service {\n readonly submit: (request: SubmitRequest) => Effect.Effect<CommandReceipt, SyncError>\n}\n\n/**\n * The branch command ledger.\n *\n * @category services\n * @since 0.1.0\n */\nexport class BranchCommands extends Context.Service<BranchCommands, Service>()(\"@smthrs/sync/BranchCommands\") {}\n\n/**\n * Constructs a command ledger that admits nothing.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service =>\n BranchCommands.of({\n submit: () => Effect.fail(new SyncError({ code: \"closed\", message: \"Branch commands are unavailable\" })),\n ...overrides\n })\n\n/**\n * Provides a command ledger that admits nothing.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop: Layer.Layer<BranchCommands> = Layer.succeed(BranchCommands, makeNoop())\n\n/**\n * Projects a journal write failure onto the sync boundary.\n *\n * The public `message` is a constant and the `cause` names the failure's type\n * only. A branch writer may hold nothing but a share link, and the journal's\n * own message is the SQLite driver's, which carries SQL text, table and column\n * names, and constraint identifiers. The CODE is a different question from the\n * message: a journal code this boundary also declares crosses as that code, so\n * a writer whose commit was lost to a closing journal is told that rather than\n * being told nothing.\n */\nconst journalFailure = (cause: unknown): SyncError =>\n new SyncError({\n code: journalErrorCode(cause),\n message: \"Branch journal write failed\",\n cause: causeCode(cause)\n })\n\n/** How many entries one rehydration page reads. */\nconst pageSize = 256\n\n/**\n * Receipts one branch keeps in memory before the oldest are evicted.\n *\n * The ledger is a fast path: it answers a known duplicate without a journal\n * write, and losing an entry costs a round trip, never correctness, because\n * the journal's own producer identity is the durable exactly-once constraint\n * and returns a `Duplicate` receipt for a command already admitted. Retaining\n * one receipt per command forever meant a process serving a hundred branches\n * of a hundred thousand commands held ten million of them, so the fast path\n * is bounded and the durable path is what makes it safe to bound.\n *\n * @category constants\n * @since 1.0.0-rc.0\n */\nexport const defaultLedgerCapacity = 4096\n\n/**\n * Inclusive UTF-8 JSON submission ceiling. The separate sync entry ceiling\n * leaves room for the durable envelope around every admitted command.\n *\n * @category constants\n * @since 1.0.0-rc.0\n */\nexport const defaultMaxCommandBytes = 1024 * 1024\n\n/**\n * Milliseconds a branch may have no active submissions before its in-memory state is\n * dropped.\n *\n * The receipt ledger is bounded per branch; this bounds the branches. A\n * dropped branch re-hydrates on its next submission exactly as it does after\n * a restart, so eviction costs a journal read, never correctness. The default\n * is the longest lifetime a branch capability may carry, so a branch is only\n * dropped once every capability minted at its last touch can have expired.\n *\n * @category constants\n * @since 1.0.0-rc.0\n */\nexport const defaultBranchIdleMs = maximumBranchTtlMs\n\n/**\n * Entries one branch's first-touch hydration reads before it stops.\n *\n * Hydration is the only full history read that ever sat on the WRITE path: the\n * first submission a process makes to a branch paged that branch's whole\n * durable log, 256 entries at a time, while holding the branch's admission\n * permit, before its own append was attempted. A hundred-thousand-entry branch\n * therefore charged its next writer four hundred journal reads for a fast path\n * that keeps four thousand receipts.\n *\n * The budget defaults to {@link defaultLedgerCapacity} because hydration\n * cannot seed more receipts than the ledger holds: everything a longer walk\n * records displaces something the same walk already recorded. What the extra\n * reads buy is a FRESHER selection — the tail of history rather than its head\n * — and the price is an unbounded wait on somebody's write. This trades that\n * back: the walk stops, the cursor stays where it stopped, and a later replay\n * continues forward from there.\n *\n * A branch longer than the budget starts with a partly cold fast path and\n * nothing else. Correctness never rested on the ledger: the journal's producer\n * identity is the durable exactly-once constraint, an exact retry receives a\n * `Duplicate` receipt carrying the canonical sequence, and an\n * `idempotency_conflict` is resolved by a read of the whole history that the\n * ledger's own bound already made necessary.\n *\n * @category constants\n * @since 1.0.0-rc.0\n */\nexport const defaultHydrationLimit = defaultLedgerCapacity\n\n/**\n * Admission policy.\n *\n * The default caps one encoded command submission at 1 MiB, one branch's\n * in-memory receipt ledger at {@link defaultLedgerCapacity} commands, and one\n * branch's first-touch hydration at {@link defaultHydrationLimit} entries.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Options {\n /**\n * Largest encoded journal entry followers accept. Defaults to\n * {@link SyncProtocol.defaultMaxFrameBytes}. Configure the server and client\n * with the same or a larger ceiling. Admission reserves the full durable\n * envelope, including generated sequence and timestamp fields, before writing.\n */\n readonly maxFrameBytes?: number | undefined\n /**\n * Largest encoded command submission admitted, in bytes. Defaults to\n * {@link defaultMaxCommandBytes}.\n */\n readonly maxCommandBytes?: number | undefined\n /**\n * Receipts one branch keeps in memory before the oldest are evicted.\n * Defaults to {@link defaultLedgerCapacity}.\n */\n readonly ledgerCapacity?: number | undefined\n /**\n * Entries one branch's first-touch hydration reads before it stops.\n * Defaults to {@link defaultHydrationLimit}.\n */\n readonly hydrationLimit?: number | undefined\n /**\n * Milliseconds a branch may have no active submissions before its in-memory state is\n * dropped. Defaults to {@link defaultBranchIdleMs}.\n */\n readonly branchIdleMs?: number | undefined\n}\n\n/** Everything one process holds for one branch. */\ninterface BranchState {\n // One permit PER BRANCH. A single process-wide permit serialized admission\n // across every branch the process served, so one branch's first-touch\n // history replay blocked every other branch's writes.\n readonly permit: Semaphore.Semaphore\n receipts: Map<CommandId, CommandReceipt>\n cursor: JournalEvent.Seq | undefined\n hydrated: boolean\n}\n\n/** The ledger over an already-validated policy. */\nconst makeWith = (\n resolved: {\n readonly maxCommandBytes: number\n readonly maxFrameBytes: number\n readonly ledgerCapacity: number\n readonly hydrationLimit: number\n readonly branchIdleMs: number\n }\n): Effect.Effect<Service, never, Journal.Journal | BranchShare.BranchShare | Scope.Scope> =>\n Effect.gen(\n function*() {\n const journal = yield* Journal.Journal\n const share = yield* BranchShare.BranchShare\n const { branchIdleMs, hydrationLimit, ledgerCapacity, maxCommandBytes, maxFrameBytes } = resolved\n // Active submissions share one state; its idle lifetime starts at release.\n const branches = yield* RcMap.make({\n idleTimeToLive: branchIdleMs,\n lookup: (_branchId: BranchId) =>\n Effect.sync((): BranchState => ({\n permit: Semaphore.makeUnsafe(1),\n receipts: new Map(),\n cursor: undefined,\n hydrated: false\n }))\n })\n\n /**\n * Records one receipt, evicting the branch's oldest once the branch is\n * at capacity. `Map` iterates in insertion order, so the first key is\n * the least recently recorded one; re-recording a known command\n * refreshes it in place rather than growing the branch.\n */\n const recordIn = (branch: Map<CommandId, CommandReceipt>, receipt: CommandReceipt): void => {\n branch.delete(receipt.commandId)\n branch.set(receipt.commandId, receipt)\n for (const oldest of branch.keys()) {\n if (branch.size <= ledgerCapacity) break\n branch.delete(oldest)\n }\n }\n\n const duplicateOf = (known: CommandReceipt): CommandReceipt =>\n new CommandReceipt({\n branchId: known.branchId,\n commandId: known.commandId,\n status: \"duplicate\",\n seq: known.seq\n })\n\n /**\n * Pages a branch's journal forward from `from`, handing each entry to\n * `visit` along with the command identity it carries, if any, and\n * stopping once it has visited `budget` entries. Admission already\n * decoded every command payload in full, so the identity is read off\n * that decoded command rather than decoded a second time.\n *\n * The budget is entries VISITED, not pages read, so a caller names a\n * bound in the unit its own memory is measured in.\n * `Number.POSITIVE_INFINITY` is the unbounded walk the recovery reads\n * take: they answer a question about the whole durable history, and a\n * bounded answer to that question is a wrong one.\n */\n const walk = (\n branchId: BranchId,\n from: JournalEvent.Seq | undefined,\n budget: number,\n visit: (entry: JournalEvent.Entry, commandId: CommandId | undefined) => void\n ): Effect.Effect<void, SyncError> =>\n Effect.gen(function*() {\n const runId = branchRunId(branchId)\n let after = from\n let hasMore = true\n let remaining = budget\n while (hasMore && remaining > 0) {\n const page = yield* journal.entries({\n runId,\n ...(after === undefined ? {} : { after }),\n limit: Math.min(pageSize, remaining)\n }).pipe(Effect.mapError(journalFailure))\n const admitted = yield* Admission.withCommands(page.entries, runId, after ?? -1)\n for (const { command, entry } of admitted) {\n after = entry.seq\n remaining -= 1\n visit(entry, command?.commandId)\n }\n // An empty page ends the walk whatever the page claims, the same\n // guard `SyncServer.tail` carries: `after` cannot move, so the\n // next read is byte-identical and the loop would never terminate\n // — while holding this branch's admission permit.\n if (page.entries.length === 0) return\n hasMore = page.hasMore\n }\n })\n\n /**\n * Replays the branch journal forward from the last replayed sequence into\n * the ledger. Used once on first touch, so a process that restarts\n * mid-collaboration still recognises every command it already admitted,\n * and again after an admission conflict, to read the command another\n * writer admitted after this process last looked.\n *\n * The staged cursor tracks each visited entry. A successful bounded walk\n * commits exactly where it stopped, so the next replay resumes there.\n * Failure or interruption discards the staged cursor and receipts together.\n */\n const replay = (branchId: BranchId, state: BranchState, budget: number): Effect.Effect<void, SyncError> =>\n Effect.gen(function*() {\n // Stage at most ledgerCapacity receipts. A malformed later page or an\n // interruption must leave both the ledger and replay cursor untouched.\n const staged = new Map(state.receipts)\n let position = state.cursor\n yield* walk(branchId, position, budget, (entry, commandId) => {\n position = entry.seq\n if (commandId !== undefined) {\n recordIn(staged, new CommandReceipt({ branchId, commandId, status: \"admitted\", seq: entry.seq }))\n }\n })\n state.cursor = position\n state.receipts = staged\n })\n\n /**\n * The sequence one command was admitted at, read from the branch's whole\n * durable history rather than from the ledger.\n *\n * The ledger is BOUNDED, so the receipt for a command admitted far\n * enough back has been evicted, and a forward replay from the ledger's\n * own cursor can never bring it back — the entry is below that cursor.\n * The durable log still holds it, and this is the read that says so.\n * Neither the cursor nor the ledger moves: this answers one question\n * about history and leaves the fast path exactly as it found it.\n */\n const admittedSeq = (\n branchId: BranchId,\n commandId: CommandId\n ): Effect.Effect<JournalEvent.Seq | undefined, SyncError> =>\n Effect.gen(function*() {\n let found: JournalEvent.Seq | undefined\n yield* walk(branchId, undefined, Number.POSITIVE_INFINITY, (entry, admitted) => {\n if (admitted === commandId) found = entry.seq\n })\n return found\n })\n\n /**\n * Seeds the branch's fast path ONCE, from its durable history, within\n * the configured budget (see {@link defaultHydrationLimit}).\n *\n * It runs on the write path, inside the branch's admission permit, so\n * the budget is what keeps the length of a branch's history out of the\n * latency of the next submission to it. The branch is marked hydrated\n * whether or not the walk reached the tail, because the point of the\n * budget is that no submission pays for the rest: what the walk did not\n * reach is answered by the journal, and by the unbounded replay\n * {@link lostRace} runs when the journal reports a conflict.\n */\n const hydrate = (branchId: BranchId, state: BranchState): Effect.Effect<void, SyncError> =>\n Effect.gen(function*() {\n if (state.hydrated) return\n yield* replay(branchId, state, hydrationLimit)\n state.hydrated = true\n })\n\n /**\n * The losing side of a cross-server admission race: the journal refused\n * this append because another writer already holds the command's producer\n * identity with different content — same `commandId`, different\n * participant or arguments. The original admission is durable, so\n * replaying the branch forward must surface it; a replay that does not is\n * a journal whose conflict report and entries disagree, and that failure\n * is reported honestly instead of being masked as a duplicate.\n *\n * Two reads, because the ledger is bounded and the answer must not be.\n * The forward replay picks up an admission another writer landed after\n * this process last looked. A command this process admitted itself and\n * has since evicted is BELOW that cursor, so only a read of the whole\n * history finds it — and without one, bounding the ledger turned an\n * ordinary duplicate into a report that the journal contradicts itself.\n */\n const lostRace = (\n submission: CommandSubmission,\n state: BranchState,\n cause: Journal.JournalError\n ): Effect.Effect<CommandReceipt, SyncError> =>\n Effect.gen(function*() {\n // Unbounded, unlike hydration: this is the recovery read, and the\n // sequence it is looking for may sit anywhere in the history.\n yield* replay(submission.branchId, state, Number.POSITIVE_INFINITY)\n const known = state.receipts.get(submission.commandId)\n if (known !== undefined) return duplicateOf(known)\n // A branch still under the ledger's capacity has never evicted\n // anything, and `replay` has just read this branch to its tail, so a\n // command missing from the ledger is missing from the journal too.\n // Reading the whole history to confirm that would let one small\n // request cost one full log scan.\n const mayHaveEvicted = state.receipts.size >= ledgerCapacity\n const seq = mayHaveEvicted\n ? yield* admittedSeq(submission.branchId, submission.commandId)\n : undefined\n if (seq === undefined) return yield* Effect.fail(journalFailure(cause))\n return new CommandReceipt({\n branchId: submission.branchId,\n commandId: submission.commandId,\n status: \"duplicate\",\n seq\n })\n })\n\n const journalInput = (submission: CommandSubmission, capabilityId: string): JournalEvent.Input =>\n new JournalEvent.Input({\n runId: branchRunId(submission.branchId),\n sourceId: commandSourceId(submission.commandId),\n sourceSeq: commandSourceSeq,\n eventType: CommandEvent,\n payload: {\n branchId: submission.branchId,\n commandId: submission.commandId,\n participantId: submission.participantId,\n name: submission.name,\n args: submission.args,\n target: submission.target,\n capabilityId\n },\n meta: null\n })\n\n const admit = (\n request: SubmitRequest,\n capabilityId: string,\n state: BranchState\n ): Effect.Effect<CommandReceipt, SyncError> =>\n Effect.gen(function*() {\n const submission = request.submission\n yield* hydrate(submission.branchId, state)\n const known = state.receipts.get(submission.commandId)\n if (known !== undefined) return duplicateOf(known)\n // Unfenced: the sync command journal is a multi-writer admission\n // log — participants own no branch run, and command admissions are\n // first-writer-wins on the command id.\n const receipt = yield* journal.emitDurableUnfenced(\n journalInput(submission, capabilityId)\n ).pipe(\n // A `Duplicate` receipt is another writer landing the identical\n // submission first: the journal deduplicated durably and returned\n // the canonical sequence the original append committed at.\n Effect.map((accepted) =>\n new CommandReceipt({\n branchId: submission.branchId,\n commandId: submission.commandId,\n status: accepted._tag === \"Duplicate\" ? \"duplicate\" : \"admitted\",\n seq: accepted.seq\n })\n ),\n Effect.catch((cause) =>\n cause.code === \"idempotency_conflict\"\n ? lostRace(submission, state, cause)\n : Effect.fail(journalFailure(cause))\n )\n )\n recordIn(\n state.receipts,\n new CommandReceipt({\n branchId: submission.branchId,\n commandId: submission.commandId,\n status: \"admitted\",\n seq: receipt.seq\n })\n )\n return receipt\n })\n\n const submit = Effect.fn(\"BranchCommands.submit\")(function*(supplied: SubmitRequest) {\n const decoded = yield* Admission.decode(SubmitRequest, supplied, \"invalid_request\")\n // Freeze the authorized submission across asynchronous signature verification.\n const request = { capability: decoded.capability, submission: { ...decoded.submission } }\n yield* Effect.annotateCurrentSpan({\n branchId: request.submission.branchId,\n commandId: request.submission.commandId,\n participantId: request.submission.participantId\n })\n // The verified capability, not the client's `participantId`, is who\n // submitted: it is stamped on the durable payload for attribution.\n const claims = yield* share.verify(request.capability, {\n branchId: request.submission.branchId,\n access: \"write\"\n })\n const bytes = SyncProtocol.encodedByteLength(request.submission)\n if (bytes > maxCommandBytes) {\n // Refused BEFORE the append: an oversized command must never reach\n // the journal, and therefore never reaches any follower.\n return yield* Effect.fail(\n new SyncError({\n code: \"frame_too_large\",\n message: `Encoded command submission of ${bytes} bytes exceeds the ${maxCommandBytes}-byte ceiling`\n })\n )\n }\n const input = journalInput(request.submission, claims.capabilityId)\n // The journal owns these fields. Reserve their largest encoded forms,\n // so acceptance does not depend on wall clock or current history length.\n // Event identity repeats run/source IDs; their escaping and UTF-8 cost\n // must be measured, not replaced by a fixed envelope allowance.\n const entryBytes = SyncProtocol.encodedByteLength({\n ...input,\n seq: Number.MAX_SAFE_INTEGER - 1,\n eventId: JournalEvent.makeEventId(input.runId, input.sourceId, commandSourceSeq),\n emittedAtMs: Number.MAX_VALUE\n })\n if (entryBytes > maxFrameBytes) {\n return yield* Effect.fail(\n new SyncError({\n code: \"frame_too_large\",\n message:\n `Encoded command journal entry requires up to ${entryBytes} bytes, exceeding the ${maxFrameBytes}-byte frame ceiling`\n })\n )\n }\n // The permit is taken AFTER authorization so an unauthorized caller\n // cannot serialize (and therefore stall) legitimate collaborators,\n // and it is this BRANCH's permit so a slow branch stalls only itself.\n return yield* RcMap.get(branches, request.submission.branchId).pipe(\n Effect.flatMap((state) => state.permit.withPermit(admit(request, claims.capabilityId, state))),\n Effect.scoped\n )\n })\n\n return BranchCommands.of({ submit })\n }\n )\n\nconst defaults = {\n maxCommandBytes: defaultMaxCommandBytes,\n maxFrameBytes: SyncProtocol.defaultMaxFrameBytes,\n ledgerCapacity: defaultLedgerCapacity,\n hydrationLimit: defaultHydrationLimit,\n branchIdleMs: defaultBranchIdleMs\n}\n\n/**\n * Constructs the journal-backed branch command ledger under an explicit\n * policy. The caller scope owns cached states and their idle-expiration timers.\n *\n * A submission whose encoded form exceeds the command ceiling is refused with\n * `frame_too_large` before anything is appended, so one oversized `args`\n * cannot enter the branch journal and poison every follower that replays it.\n *\n * First-touch hydration reads at most `hydrationLimit` entries, so the length\n * of a branch's history is not charged to the next writer's latency; see\n * {@link defaultHydrationLimit} for why the fast path may be seeded partially\n * without any effect on what the ledger admits.\n *\n * Every option is validated as a positive safe integer at construction: the\n * TypeScript type says `number`, and `NaN` silently disabled the ceiling it\n * was compared against.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeLiveWith = (\n options: Options = {}\n): Effect.Effect<Service, SyncError, Journal.Journal | BranchShare.BranchShare | Scope.Scope> =>\n Effect.flatMap(\n Effect.all({\n maxFrameBytes: positiveInt(\n \"BranchCommands.Options.maxFrameBytes\",\n options.maxFrameBytes,\n defaults.maxFrameBytes\n ),\n maxCommandBytes: positiveInt(\n \"BranchCommands.Options.maxCommandBytes\",\n options.maxCommandBytes,\n defaults.maxCommandBytes\n ),\n ledgerCapacity: positiveInt(\n \"BranchCommands.Options.ledgerCapacity\",\n options.ledgerCapacity,\n defaults.ledgerCapacity\n ),\n hydrationLimit: positiveInt(\n \"BranchCommands.Options.hydrationLimit\",\n options.hydrationLimit,\n defaults.hydrationLimit\n ),\n branchIdleMs: positiveInt(\n \"BranchCommands.Options.branchIdleMs\",\n options.branchIdleMs,\n defaults.branchIdleMs\n )\n }),\n makeWith\n )\n\n/**\n * Constructs the journal-backed branch command ledger with default policy.\n * The caller scope owns cached states and their idle-expiration timers.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeLive: Effect.Effect<Service, never, Journal.Journal | BranchShare.BranchShare | Scope.Scope> =\n makeWith(defaults)\n\n/**\n * Provides the journal-backed branch command ledger.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer: Layer.Layer<BranchCommands, never, Journal.Journal | BranchShare.BranchShare> = Layer.effect(\n BranchCommands,\n makeLive\n)\n\n/**\n * Provides the journal-backed branch command ledger under an explicit policy.\n * Fails with `invalid_request` when an option is not a positive safe integer.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerWith = (\n options: Options\n): Layer.Layer<BranchCommands, SyncError, Journal.Journal | BranchShare.BranchShare> =>\n Layer.effect(BranchCommands, makeLiveWith(options))\n\n/**\n * Builds a submission, filling in the fields a plain command never sets, and\n * refusing a field the schema forbids with a `SyncError` rather than a defect.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const submission = (fields: {\n readonly branchId: BranchId\n readonly commandId: CommandId\n readonly participantId: CommandSubmission[\"participantId\"]\n readonly name: string\n readonly args?: string\n readonly target?: string\n}): Effect.Effect<CommandSubmission, SyncError> =>\n // Decoded, not constructed: `name` is a `NonEmptyString` the parameter type\n // admits `\"\"` for, and `new CommandSubmission` throws on it. A builder that\n // dies on a caller's argument is the same defect `submit` already refuses\n // typed, one call earlier.\n Admission.decode(CommandSubmission, {\n branchId: fields.branchId,\n commandId: fields.commandId,\n participantId: fields.participantId,\n name: fields.name,\n args: fields.args ?? \"\",\n target: fields.target ?? \"\"\n }, \"invalid_request\")\n"]}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The source of freshly minted branch and capability identifiers.
3
+ *
4
+ * Minting an id is nondeterminism against the host, so it enters through a
5
+ * port like every other host effect: `BranchServer.layerHandlers` asks
6
+ * this service for the ids it puts on the wire instead of calling a global
7
+ * generator, which is what lets a deterministic composition — a test, a
8
+ * replayed transcript, a deployment that derives ids from its own namespace —
9
+ * supply its own without touching the handlers.
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+ import * as Context from "effect/Context";
14
+ import * as Effect from "effect/Effect";
15
+ import * as Layer from "effect/Layer";
16
+ /**
17
+ * Identifier minting operations.
18
+ *
19
+ * @category models
20
+ * @since 0.1.0
21
+ */
22
+ export interface Service {
23
+ /** A fresh, unguessable identifier. Never returns the same value twice. */
24
+ readonly fresh: Effect.Effect<string>;
25
+ }
26
+ declare const BranchIds_base: Context.ServiceClass<BranchIds, "@smthrs/sync/BranchIds", Service>;
27
+ /**
28
+ * The branch identifier source.
29
+ *
30
+ * @category services
31
+ * @since 0.1.0
32
+ */
33
+ export declare class BranchIds extends BranchIds_base {
34
+ }
35
+ /**
36
+ * Constructs the default source: Web Crypto UUIDs.
37
+ *
38
+ * Web Crypto is used directly, as it is for the share signature in
39
+ * `BranchShare`, so the same module runs in the browser and on node.
40
+ * A branch id is not itself an authorization — every operation still
41
+ * authorizes through a signed capability — but an unguessable id keeps a
42
+ * branch from being enumerated, which is why the default stays with a
43
+ * cryptographic generator rather than `Random`.
44
+ *
45
+ * @category constructors
46
+ * @since 0.1.0
47
+ */
48
+ export declare const makeWebCrypto: () => Service;
49
+ /**
50
+ * Provides {@link makeWebCrypto}.
51
+ *
52
+ * @category layers
53
+ * @since 0.1.0
54
+ */
55
+ export declare const layer: Layer.Layer<BranchIds>;
56
+ /**
57
+ * Provides a deterministic counter, `<prefix>-1`, `<prefix>-2`, and so on.
58
+ * For tests and reproducible transcripts only — the ids are guessable.
59
+ *
60
+ * @category layers
61
+ * @since 0.1.0
62
+ */
63
+ export declare const layerSequential: (prefix: string) => Layer.Layer<BranchIds>;
64
+ export {};
65
+ //# sourceMappingURL=BranchIds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BranchIds.d.ts","sourceRoot":"","sources":["../../src/BranchIds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAA;AACzC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CACtC;;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,cAA+D;CAAG;AAEjG;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,QAAO,OAA0E,CAAA;AAE3G;;;;;GAKG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,CAAwC,CAAA;AAEjF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CASlE,CAAA"}