@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,159 @@
1
+ ---
2
+ title: "Quickstart"
3
+ description: "Follow a run end to end: write two entries, open one subscription over a real server and client, and watch a third entry arrive live."
4
+ sidebar:
5
+ order: 2
6
+ ---
7
+
8
+ This quickstart runs a real `SyncServer` over a real journal, connected to a
9
+ real `SyncClient`, and needs no database of your own and no network. The only
10
+ piece that is not production is the transport: an in-memory socket pair stands
11
+ in for a WebSocket. By the end you will have one subscription that delivers two
12
+ entries of history and then a third entry that commits while you are watching.
13
+
14
+ A runnable copy lives in the Smithers repository, as
15
+ [`examples/src/07-sync-follower.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/07-sync-follower.ts).
16
+
17
+ ## Prerequisites
18
+
19
+ - Node.js 26.4.0 or later.
20
+ - A package with the dependencies installed. Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
21
+
22
+ ## Name the run
23
+
24
+ Create `quickstart.ts`. `RunId`, `SourceId`, and `SourceSeq` are branded, so a
25
+ plain string needs a cast at the boundary where you introduce it:
26
+
27
+ ```ts
28
+ import { JournalEvent } from "@smthrs/journal"
29
+
30
+ const runId = "sync-demo-1" as JournalEvent.RunId
31
+ const sourceId = "quickstart" as JournalEvent.SourceId
32
+
33
+ const entry = (sourceSeq: number, eventType: string) =>
34
+ new JournalEvent.Input({
35
+ runId,
36
+ sourceId,
37
+ sourceSeq: sourceSeq as JournalEvent.SourceSeq,
38
+ eventType,
39
+ payload: { sourceSeq },
40
+ meta: null
41
+ })
42
+ ```
43
+
44
+ `sourceId` and `sourceSeq` are the journal's producer identity. They are what
45
+ makes a re-emitted entry a duplicate rather than a second row, and the sync
46
+ read path never sees them as anything but fields on the entry it replicates.
47
+
48
+ ## Write history the follower has never seen
49
+
50
+ `emitDurableUnfenced` commits before it returns, so these two entries are on
51
+ disk before anything subscribes:
52
+
53
+ ```ts
54
+ import { Journal } from "@smthrs/journal"
55
+ import * as Effect from "effect/Effect"
56
+
57
+ const writeHistory = Effect.gen(function*() {
58
+ const journal = yield* Journal.Journal
59
+ yield* journal.emitDurableUnfenced(entry(0, "run.started"))
60
+ yield* journal.emitDurableUnfenced(entry(1, "step.recorded"))
61
+ })
62
+ ```
63
+
64
+ ## Connect a follower
65
+
66
+ `TestSocket.makePair` is a scoped, bidirectional in-memory socket.
67
+ `TestSync.connect` starts an RPC server over its server end and returns a
68
+ `SyncClient.Service` over its client end:
69
+
70
+ ```ts
71
+ import * as TestSocket from "@smthrs/sync/test/TestSocket"
72
+ import * as TestSync from "@smthrs/sync/test/TestSync"
73
+
74
+ const connect = Effect.gen(function*() {
75
+ const pair = yield* TestSocket.makePair()
76
+ return yield* TestSync.connect(pair)
77
+ })
78
+ ```
79
+
80
+ Swap the socket pair for a network transport and the follower code below does
81
+ not change. That is the point of the seam: the client talks to an RPC protocol,
82
+ not to a socket.
83
+
84
+ ## Subscribe, then commit one more entry
85
+
86
+ The subscription replays the durable history first and then stays open for live
87
+ entries, so a `take(3)` collects both halves through one stream:
88
+
89
+ ```ts
90
+ import * as Fiber from "effect/Fiber"
91
+ import * as Stream from "effect/Stream"
92
+
93
+ export interface Summary {
94
+ readonly caughtUp: ReadonlyArray<string>
95
+ readonly followed: ReadonlyArray<string>
96
+ }
97
+
98
+ export const main: Effect.Effect<Summary> = Effect.gen(function*() {
99
+ yield* writeHistory
100
+ const follower = yield* connect
101
+
102
+ const collected = yield* Stream.runCollect(
103
+ follower.subscribe({ scope: { _tag: "Run", runId }, cursors: [] }).pipe(Stream.take(3))
104
+ ).pipe(Effect.forkChild({ startImmediately: true }))
105
+
106
+ const journal = yield* Journal.Journal
107
+ yield* journal.emitDurableUnfenced(entry(2, "run.completed"))
108
+
109
+ const entries = Array.from(yield* Fiber.join(collected))
110
+ return {
111
+ caughtUp: entries.slice(0, 2).map((committed) => committed.eventType),
112
+ followed: entries.slice(2).map((committed) => committed.eventType)
113
+ }
114
+ }).pipe(Effect.provide(TestSync.layerTest), Effect.scoped, Effect.orDie)
115
+ ```
116
+
117
+ The subscription is forked with `startImmediately: true` so it is really
118
+ attached before the third entry commits. Without that the third entry could
119
+ land while nothing was listening, and the follower would read it out of the
120
+ durable history instead of following it, which proves less than the test
121
+ intends.
122
+
123
+ ## Run it
124
+
125
+ ```ts
126
+ console.log(await Effect.runPromise(main))
127
+ ```
128
+
129
+ The output separates what was replayed from what was followed:
130
+
131
+ ```text
132
+ {
133
+ caughtUp: [ 'run.started', 'step.recorded' ],
134
+ followed: [ 'run.completed' ]
135
+ }
136
+ ```
137
+
138
+ ## What just happened
139
+
140
+ `TestSync.layerTest` provided three things: the in-memory SQLite journal, an
141
+ empty mutable run catalog, and an authentication middleware that trusts the
142
+ connection as the workspace owner. The server and the client are the production
143
+ ones.
144
+
145
+ One `subscribe` call ran both phases of the protocol. The client issued
146
+ `Sync.Read` pages until the server reported it had reached the durable tail,
147
+ then opened `Sync.Subscribe` from its delivered bookmarks. The third
148
+ entry arrived on the second phase. Nothing was delivered twice, because the
149
+ cursor the follow resumed from was exactly the last sequence the replay handed
150
+ over.
151
+
152
+ ## Next steps
153
+
154
+ - [Follow a run](./guides/follow-a-run.md): cursors you persist, applying an
155
+ entry before its cursor moves, and the failures a follower must handle.
156
+ - [Serve the read path](./guides/serve-the-read-path.md): the same server over
157
+ a real journal and a real transport.
158
+ - [Test a follower](./guides/test-a-follower.md): the fault injection this
159
+ socket pair adds, for reconnects, stalls, and dropped frames.
@@ -0,0 +1,234 @@
1
+ ---
2
+ title: "Troubleshooting"
3
+ description: "Every SyncError code @smthrs/sync raises, the symptom each one produces, and the change that fixes it, plus the silent failures that are not errors at all."
4
+ ---
5
+
6
+ A sync operation fails with one of two errors. `SyncError` carries a stable
7
+ `code`, a `message`, an optional bounded `cause` string, a `resync` that is set
8
+ only on `compacted`, and a `rewind` that is set only on `lineage_changed`. `SyncGapError` reports that a server frame started
9
+ beyond the cursor the client covered, and it is terminal.
10
+
11
+ Match on the code, not on the message. Messages name run ids and byte counts
12
+ and are not a contract; codes are.
13
+
14
+ ```ts
15
+ import { SyncError } from "@smthrs/sync/SyncError"
16
+
17
+ const isExpired = (failure: unknown) => SyncError.is(failure) && failure.code === "unauthorized"
18
+ ```
19
+
20
+ `SyncError.is` is a structural guard, not a class check, because every value
21
+ that reaches it has crossed a boundary that rebuilds it: the RPC client's
22
+ schema-decoded error channel, or a browser `postMessage` that keeps the fields
23
+ and drops the prototype. It is also total, so a throwing getter on the value
24
+ answers `false` rather than raising.
25
+
26
+ ## Error codes
27
+
28
+ | Code | Where it comes from | What to do |
29
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | `unauthorized` | No workspace principal, a refused workspace capability, a missing or foreign branch capability, or a credential that expired while a subscription was open. | Present a credential, or mint a new one and resubscribe. |
31
+ | `unsupported` | Presence transport or required host wiring is unavailable. | Keep presence unknown; enable only after authenticated sources are ready. |
32
+ | `invalid_request` | A cursor set naming one run twice, a numeric option that is not a positive safe integer, an option above the ceiling the wire allows, a keyring with a duplicate or dangling `kid`, or claims that do not survive UTF-8. | Fix the request or the option. The message names the field. |
33
+ | `compacted` | The request's cursor for one run sits below that run's compaction floor. `resync` names the run and the checkpoint sequence. | Restore through an explicit `onResync` handler or retain the refusal. See [Handle a compacted run](./guides/handle-a-compacted-run.md). |
34
+ | `not_found` | `Sync.Snapshot` ran against a server that provides no `SyncServer.SnapshotSource`, a provider that failed, or `SyncServer.makeNoop`. | Provide a `SnapshotSource`, or omit `onResync` so the client never asks for a snapshot. |
35
+ | `lineage_changed` | A read or subscription named a generation the run has moved past, because the run was rewound. `rewind` names the current generation and the archive boundary. | Rebuild the projection from retained history and create a fresh client. The client never retries this refusal. |
36
+ | `frame_too_large` | A single entry whose own encoded size exceeds the frame ceiling, or a page or frame whose entries sum past the ceiling the client enforces. | Raise `maxFrameBytes` on both sides, or stop writing entries that large. |
37
+ | `protocol_violation` | A server page or frame that contradicted itself: another run's entry, a repeated or reordered sequence, an entry at or below the requested cursor, or an incomplete page with no entries. | Report it. No cursor moved, so nothing was skipped. A rewriting proxy is the usual cause. |
38
+ | `transport_failed` | The RPC transport failed. | The client retries this under backoff. It reaches you only if the stream ends for another reason. |
39
+ | `closed` | The server sent a `Closed` frame, the journal reported `journal_closed`, or the operation ran against `SyncClient.makeNoop`, `SyncServer.layerNoop`, or `BranchCommands.layerNoop`. | Check that the composition provides real services, then resubscribe. |
40
+ | `backpressure` | The journal's admission queue overflowed, or a branch already holds `maxParticipants` participants. | Retry. Raise the bound only after confirming the producer is not the problem. |
41
+ | `decode_failed` | The journal could not decode a stored payload. | Inspect the run's storage. The sync boundary is reporting, not causing, this. |
42
+ | `unknown` | A journal failure this boundary has no counterpart for, or Web Crypto refusing to import or use the signing key. | Read `cause`, which carries the underlying error's name and its own enumerated code. |
43
+
44
+ The `cause` field is a bounded string and never the host object that failed.
45
+ `SyncError` is the declared error schema of every procedure in both RPC groups,
46
+ so whatever it carries reaches a follower that may hold nothing but a share
47
+ link. A journal failure crosses as its stable journal code, never as the
48
+ driver's own sentence.
49
+
50
+ ## Every read fails with unauthorized
51
+
52
+ **Symptom.** A follower gets `unauthorized` on `Sync.Read` and
53
+ `Sync.Subscribe` for every non-branch run, with the message "Reading workspace
54
+ runs requires an authenticated workspace principal".
55
+
56
+ **Cause.** The request ran as the anonymous principal. Either the connection
57
+ sent no `flows-sync-workspace` header, or the serving composition never
58
+ provided `SyncAuth.layer` and nothing else established a principal.
59
+
60
+ **Fix.** On the client, provide `SyncAuth.layerClient(capability)` so every
61
+ outgoing request carries the header. On the server, provide `SyncAuth.layer`
62
+ over a `WorkspaceShare` authority. An in-process caller that owns the workspace
63
+ provides `SyncPrincipal.layerWorkspace(id)` instead and presents no credential
64
+ at all. See [Authorize a connection](./guides/authorize-a-connection.md).
65
+
66
+ There is no trusted pass-through middleware. A transport cannot be told to skip
67
+ the check; only an in-process caller providing `SyncPrincipal` bypasses it.
68
+
69
+ ## A working subscription ends with unauthorized
70
+
71
+ **Symptom.** A subscription delivers entries for a while, then fails with
72
+ "The capability authorizing this subscription has expired".
73
+
74
+ **Cause.** A subscription is authorized once, when it opens, so the signed
75
+ expiry travels with the identity. When that moment arrives the stream ends,
76
+ because a stream authorized only at open is otherwise the one thing a signed
77
+ expiry cannot revoke.
78
+
79
+ **Fix.** Mint a fresh capability and resubscribe from your persisted cursors.
80
+ Treat this as routine rather than as an error path: choose a `ttlMs` longer
81
+ than the reconnect you are willing to pay for. An in-process owner that
82
+ provides `SyncPrincipal` itself has no deadline.
83
+
84
+ ## A branch read is refused with a capability in hand
85
+
86
+ **Symptom.** `unauthorized` with "Reading a shared branch requires a valid
87
+ share capability", even though the request carried one.
88
+
89
+ **Cause.** One of three things: the capability is for a different branch, it
90
+ has expired, or the serving composition provides no `BranchShare` authority.
91
+ Without an authority in scope, every branch run is closed.
92
+
93
+ **Fix.** Check that the capability's branch id matches the branch behind the
94
+ run id, and that the server provides `BranchShare.layerHmac` or
95
+ `BranchShare.layerConfig`. A workspace
96
+ listing does not fail here: it silently excludes the branch runs the caller's
97
+ capability does not cover, so a run missing from a listing is the same problem
98
+ seen from the other side.
99
+
100
+ ## invalid_request naming a cursor or an option
101
+
102
+ **Symptom.** "Cursors name run `X` more than once", or a message of the form
103
+ "`SyncClient.SubscribeOptions.credit` must be a positive safe integer, not
104
+ NaN".
105
+
106
+ **Cause.** A cursor set that names one run twice has no correct reading: the
107
+ read position would come from the first occurrence and the echoed response
108
+ state from the last, so a follower persisting the returned cursors would skip
109
+ entries the page never carried. A numeric option that is `NaN`, zero, or
110
+ negative satisfies the TypeScript type `number` and disables the comparison it
111
+ configures rather than tightening it, so each one is checked where it enters.
112
+
113
+ **Fix.** Deduplicate the cursor set, or pass a positive safe integer.
114
+ `boundedInt` also refuses a value above the wire's ceiling, so
115
+ `credit` above `SyncProtocol.maxSubscribeCredit` and `bootstrapLimit` above
116
+ `SyncProtocol.maxReadLimit` are refused before a request is built rather than
117
+ retried as a transport failure.
118
+
119
+ ## invalid_request from the keyring
120
+
121
+ **Symptom.** `WorkspaceShare.makeHmac` or `BranchShare.makeHmac` fails with
122
+ "The workspace keyring names kid `K` twice", "The branch keyring's active kid
123
+ names no key in the ring", or "The HMAC signing secret must be at least 32 bytes
124
+ of UTF-8".
125
+
126
+ **Cause.** The keyring is malformed. A duplicate `kid` makes verification
127
+ ambiguous, an `activeKid` with no matching key leaves nothing to mint with, and
128
+ a short secret can be searched offline from one observed capability.
129
+
130
+ **Fix.** Correct the keyring before the layer is built. Generate each secret
131
+ with `openssl rand -base64 32`. Rotation adds the new
132
+ key to `keys` and points `activeKid` at it, keeping the retired key in the ring
133
+ so capabilities minted under it still verify.
134
+
135
+ ## SyncGapError
136
+
137
+ **Symptom.** The stream fails with `SyncGapError`, naming a run, the sequence
138
+ the client expected to start from, and the sequence the server's frame started
139
+ at.
140
+
141
+ **Cause.** The server's declared covered interval began beyond the cursor the
142
+ client had. History between the two was never delivered.
143
+
144
+ **Fix.** Rebuild, do not retry. Resubscribing from the same cursors reproduces
145
+ it. This is not the same as a hole in the sequence: a journal sequence is
146
+ legitimately non-contiguous because dropped admissions leave gaps, and the
147
+ client compares `fromSeq` against the cursor for exactly that reason. A gap
148
+ error means the interval itself skipped forward, which a correct server does
149
+ not do.
150
+
151
+ ## A compacted failure that is not recovered
152
+
153
+ **Symptom.** `compacted` reaches your consumer instead of being absorbed.
154
+
155
+ **Cause.** No recovery handler was supplied, application failed, the error
156
+ carries no `resync`, or the checkpoint it names is
157
+ at or below what the subscription already covers. A resync to a position the
158
+ client has passed cannot move it forward, so retrying would re-read the same
159
+ refusal forever.
160
+
161
+ **Fix.** Supply `onResync` only when you can restore the missing state. Return
162
+ the exact `{ runId, afterSeq }` actually applied, covering the reported floor.
163
+ If you supplied a handler, check whether it failed: it runs before the cursor moves and
164
+ must return a valid receipt, so a failure deliberately leaves the cursor where it was rather
165
+ than skipping the range silently. See
166
+ [Handle a compacted run](./guides/handle-a-compacted-run.md).
167
+
168
+ ## A workspace subscription delivers nothing
169
+
170
+ **Symptom.** `{ _tag: "Workspace" }` produces an empty stream that never fails.
171
+
172
+ **Cause.** The run catalog lists no runs. `RunCatalog.layerNoop` is
173
+ `layerStatic([])`, and `RunCatalog.makeMemory` starts empty, so a composition
174
+ that never registers a run has nothing to cover.
175
+
176
+ **Fix.** Provide a catalog that sees the runs. `RunCatalog.layerPolling` over
177
+ `RunCatalogRead` from [`@smthrs/engine-store`](/api/engine-store) is the
178
+ durable form, and it is what lets a follower learn of runs another engine
179
+ created. See [List a workspace's runs](./guides/list-workspace-runs.md).
180
+
181
+ A subscription with no covered runs is also what a credential that covers
182
+ nothing looks like, so confirm the principal before changing the catalog.
183
+
184
+ ## A subscription skips history you asked for
185
+
186
+ **Symptom.** You pass an older cursor, and the subscription starts later than
187
+ that cursor.
188
+
189
+ **Cause.** The effective start position is the later of your cursor and the
190
+ matching shared progress map: applied for a subscription with `apply`, delivered
191
+ otherwise. One client belongs to one materialization. A delivered bookmark is
192
+ not proof that a projection applied the corresponding history.
193
+
194
+ **Fix.** Build a fresh client. Its progress maps are empty, so your cursor is
195
+ the only one there is. Rebuilding a projection from an earlier position is a
196
+ different job from following, and it gets its own client.
197
+
198
+ ## Every operation fails with closed
199
+
200
+ **Symptom.** Every subscription fails immediately with "Sync client is closed",
201
+ "Branch commands are unavailable", or "Branch presence is unavailable".
202
+
203
+ **Cause.** The composition provided a noop stub. `SyncClient.layerNoop`,
204
+ `SyncServer.layerNoop`, `BranchCommands.layerNoop`, and
205
+ `TestSync.layerNoop` exist so a consumer that only needs the ports to resolve
206
+ can compile, and every operation on them refuses.
207
+
208
+ **Fix.** Provide the real layers. In a test, `TestSync.layerTest` is the
209
+ fixture that binds a real server over a real journal. See
210
+ [Test a follower](./guides/test-a-follower.md).
211
+
212
+ ## An import fails to resolve
213
+
214
+ **Symptom.** A bundler or Node reports that `@smthrs/sync/internal/thing` has
215
+ no export, or a browser build fails on `@smthrs/sync/test/TestSync`.
216
+
217
+ **Cause.** `./internal/*` is mapped to `null` in the package's exports, so
218
+ nothing under it resolves. `@smthrs/sync/test/TestSync` binds the Node SQLite
219
+ test journal and is Node only.
220
+
221
+ **Fix.** Import from the root or from a public module path. In a browser test,
222
+ `@smthrs/sync/test/TestSocket` is the half that runs anywhere.
223
+
224
+ ## Branch procedures have no server
225
+
226
+ **Symptom.** A branch client finds no endpoint to talk to.
227
+
228
+ **Cause.** Nothing outside this package's tests mounts `BranchRpcs`.
229
+ [`@smthrs/gateway`](/api/gateway) mounts `SyncRpcs` on `POST /sync` and
230
+ `/sync/ws` and stops there.
231
+
232
+ **Fix.** Mount `BranchServer.layerHandlers` on a transport of your own, and
233
+ treat the branch modules as a library surface pending a host. See
234
+ [Branch collaboration](./concepts/branches.md).