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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,594 @@
1
+ /**
2
+ * The wire contract shared by the sync server and the browser-safe client.
3
+ *
4
+ * Every message is schema-backed so the same definitions serve the RPC group,
5
+ * the HTTP transport, and replay tooling.
6
+ *
7
+ * @since 0.1.0
8
+ */
9
+
10
+ import * as JournalEvent from "@smthrs/journal/JournalEvent"
11
+ import * as Schema from "effect/Schema"
12
+ import { ShareCapability } from "./BranchProtocol.ts"
13
+
14
+ /**
15
+ * Wire revision requiring explicit generations in server positions.
16
+ * Missing request versions decode so the server can return a typed refusal.
17
+ *
18
+ * @category constants
19
+ * @since 1.0.0-rc.0
20
+ */
21
+ export const protocolVersion = 1
22
+
23
+ /**
24
+ * Selects every run in the workspace.
25
+ *
26
+ * @category models
27
+ * @since 0.1.0
28
+ */
29
+ export const WorkspaceScope = Schema.TaggedStruct("Workspace", {})
30
+
31
+ /**
32
+ * Selects a single run.
33
+ *
34
+ * @category models
35
+ * @since 0.1.0
36
+ */
37
+ export const RunScope = Schema.TaggedStruct("Run", { runId: JournalEvent.RunId })
38
+
39
+ /**
40
+ * The set of runs a request covers.
41
+ *
42
+ * @category models
43
+ * @since 0.1.0
44
+ */
45
+ export const Scope = Schema.Union([WorkspaceScope, RunScope])
46
+
47
+ /**
48
+ * The set of runs a request covers.
49
+ *
50
+ * @category models
51
+ * @since 0.1.0
52
+ */
53
+ export type Scope = typeof Scope.Type
54
+
55
+ /**
56
+ * The last sequence one run has been DELIVERED to a client through.
57
+ *
58
+ * "Delivered", not "applied": {@link SyncClient}'s default follow advances a
59
+ * cursor as it hands the entry to the consumer, so a consumer whose own apply
60
+ * step then fails must not treat the cursor as an acknowledgement of that
61
+ * apply. A consumer that needs the stronger meaning supplies
62
+ * `SubscribeOptions.apply`, which the client runs to success before the cursor
63
+ * moves. `Progress.applied` records that application claim; a bare wire
64
+ * cursor never acknowledges application to the server.
65
+ *
66
+ * `generation` identifies the run's current history after rewinds. Requests may
67
+ * omit it for persisted generation-zero cursors. Server responses must include
68
+ * it in exactly one cursor for every run represented in Read entries. Clients
69
+ * refuse missing cursor rows or generations before delivery; persist a returned
70
+ * generation with its sequence.
71
+ * Sequence ordering is meaningful only within one generation.
72
+ *
73
+ * @category models
74
+ * @since 0.1.0
75
+ */
76
+ export const RunCursor = Schema.Struct({
77
+ runId: JournalEvent.RunId,
78
+ afterSeq: JournalEvent.Seq,
79
+ // Omitted only for generation zero (persisted cursors from before rewind support).
80
+ generation: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
81
+ })
82
+
83
+ /**
84
+ * The last sequence one run has been delivered to a client through.
85
+ *
86
+ * @category models
87
+ * @since 0.1.0
88
+ */
89
+ export type RunCursor = typeof RunCursor.Type
90
+
91
+ /**
92
+ * A client's delivered position across the workspace.
93
+ *
94
+ * At most ONE cursor per run: a request carrying two cursors for the same run
95
+ * is ambiguous about where the read starts, so the server refuses it with
96
+ * `invalid_request` rather than picking one (see {@link duplicateCursorRunId}).
97
+ *
98
+ * @category models
99
+ * @since 0.1.0
100
+ */
101
+ export const WorkspaceCursor = Schema.Array(RunCursor)
102
+
103
+ /**
104
+ * A client's delivered position across the workspace.
105
+ *
106
+ * @category models
107
+ * @since 0.1.0
108
+ */
109
+ export type WorkspaceCursor = typeof WorkspaceCursor.Type
110
+
111
+ /**
112
+ * One run's delivered position as a SERVER states it.
113
+ *
114
+ * Identical to {@link RunCursor} except that `generation` is required: a
115
+ * request may omit it for a persisted generation-zero cursor, a response may
116
+ * not. Server positions are typed with this shape so an implementation that
117
+ * omits a generation fails to compile rather than being refused at run time by
118
+ * every client that reads it.
119
+ *
120
+ * The wire schemas stay lenient on purpose. {@link ReadResponse} and
121
+ * {@link Frame} decode an absent generation so {@link SyncClient} can answer a
122
+ * non-conforming server with a typed `protocol_violation` naming the missing
123
+ * field, rather than a bare decode failure.
124
+ *
125
+ * @category models
126
+ * @since 1.0.0-rc.0
127
+ */
128
+ export const ServerCursor = Schema.Struct({
129
+ ...RunCursor.fields,
130
+ generation: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
131
+ })
132
+
133
+ /**
134
+ * One run's delivered position as a server states it.
135
+ *
136
+ * @category models
137
+ * @since 1.0.0-rc.0
138
+ */
139
+ export type ServerCursor = typeof ServerCursor.Type
140
+
141
+ /**
142
+ * The scope and cursor set every read and subscribe request carries.
143
+ *
144
+ * Server read, server subscribe and client subscribe each admit a
145
+ * caller-built request against this one schema, so an envelope field is
146
+ * declared once and every entry point agrees on what an admitted request is.
147
+ *
148
+ * @category models
149
+ * @since 1.0.0-rc.0
150
+ */
151
+ export const RequestEnvelope = Schema.Struct({ scope: Scope, cursors: WorkspaceCursor })
152
+
153
+ /**
154
+ * The scope and cursor set every read and subscribe request carries.
155
+ *
156
+ * @category models
157
+ * @since 1.0.0-rc.0
158
+ */
159
+ export type RequestEnvelope = typeof RequestEnvelope.Type
160
+
161
+ /**
162
+ * A client's delivery and application positions, as two cursor sets.
163
+ *
164
+ * `delivered` is transport delivery and never an application acknowledgement.
165
+ * `applied` holds the positions committed after a successful `apply` or
166
+ * `onResync` callback; consumers persist state and these positions in the
167
+ * same transaction. The field names carry the distinction, so neither set is
168
+ * wrapped in a tag.
169
+ *
170
+ * @category models
171
+ * @since 1.0.0-rc.0
172
+ */
173
+ export const Progress = Schema.Struct({ delivered: WorkspaceCursor, applied: WorkspaceCursor })
174
+
175
+ /** The value form of progress.
176
+ * @category models
177
+ * @since 1.0.0-rc.0
178
+ */
179
+ export type Progress = typeof Progress.Type
180
+
181
+ /**
182
+ * The first run named twice in a cursor set, or `undefined` when every run is
183
+ * named at most once.
184
+ *
185
+ * The schema cannot express the uniqueness — `Schema.Array` has no such check
186
+ * — so the one rule is stated once here and enforced by both request paths.
187
+ *
188
+ * @category predicates
189
+ * @since 0.1.0
190
+ */
191
+ export const duplicateCursorRunId = (cursors: WorkspaceCursor): JournalEvent.RunId | undefined => {
192
+ const seen = new Set<JournalEvent.RunId>()
193
+ for (const cursor of cursors) {
194
+ if (seen.has(cursor.runId)) return cursor.runId
195
+ seen.add(cursor.runId)
196
+ }
197
+ return undefined
198
+ }
199
+
200
+ /**
201
+ * Where a follower must resume after the server refused a cursor that starts
202
+ * below a run's compaction floor.
203
+ *
204
+ * Compaction deletes the entries below a checkpoint, so a cursor under the
205
+ * floor names history that no longer exists. `checkpointSeq` is that floor:
206
+ * the checkpoint's state subsumes every entry at or below it, so a follower
207
+ * must restore a snapshot covering that sequence before reading forward.
208
+ * Its applied cursor names that snapshot, which may be newer than this floor.
209
+ *
210
+ * `generation` is the run's history the floor belongs to. A run rewound
211
+ * before it was compacted is past generation zero, and a fresh follower has
212
+ * no earlier cursor to learn that from, so a recovery receipt that omits its
213
+ * generation is bound to this one. The server's refusal of a read always
214
+ * carries it; a snapshot that falls short of a floor refuses without one.
215
+ *
216
+ * This field carries no replacement state. The client fails closed without
217
+ * an explicit recovery handler returning the cursor it actually restored.
218
+ * `SnapshotRequest` fetches a separately selected public projection. Raw journal
219
+ * checkpoints are unredacted and must not be exposed under history permission.
220
+ *
221
+ * @category models
222
+ * @since 0.1.0
223
+ */
224
+ export const Resync = Schema.Struct({
225
+ runId: JournalEvent.RunId,
226
+ checkpointSeq: JournalEvent.Seq,
227
+ generation: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)))
228
+ })
229
+
230
+ /**
231
+ * Where a follower must resume after a compaction refusal.
232
+ *
233
+ * @category models
234
+ * @since 0.1.0
235
+ */
236
+ export type Resync = typeof Resync.Type
237
+
238
+ // `protocolVersion` accepts any integer here for the same reason
239
+ // `ReadRequest` and `SubscribeRequest` do: a mismatch decodes so the boundary
240
+ // can answer it with a typed `protocol_violation` instead of a decode
241
+ // failure, and the accepted number lives only in `protocolVersion`.
242
+ const snapshotIdentity = {
243
+ protocolVersion: Schema.Int,
244
+ runId: JournalEvent.RunId,
245
+ lineageId: Schema.NonEmptyString.check(Schema.isMaxLength(512)),
246
+ projection: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
247
+ projectionVersion: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThan(Number.MAX_SAFE_INTEGER))
248
+ }
249
+
250
+ /**
251
+ * Requests an explicitly public projection covering the missing prefix.
252
+ * Identity and version must match exactly; this never requests raw engine state.
253
+ * @category models
254
+ * @since 1.0.0-rc.0
255
+ */
256
+ export const SnapshotRequest = Schema.Struct({
257
+ ...snapshotIdentity,
258
+ atLeastSeq: JournalEvent.Seq,
259
+ capability: Schema.optional(ShareCapability)
260
+ })
261
+
262
+ /** A decoded request for an explicitly public projection.
263
+ * @category models
264
+ * @since 1.0.0-rc.0
265
+ */
266
+ export type SnapshotRequest = typeof SnapshotRequest.Type
267
+
268
+ /**
269
+ * A complete public projection through `seq`, inclusive. Apply its state and
270
+ * durable cursor atomically, then read entries strictly after `seq`.
271
+ * @category models
272
+ * @since 1.0.0-rc.0
273
+ */
274
+ export const Snapshot = Schema.Struct({ ...snapshotIdentity, seq: JournalEvent.Seq, state: Schema.Json })
275
+
276
+ /** A complete, versioned public projection at one journal sequence.
277
+ * @category models
278
+ * @since 1.0.0-rc.0
279
+ */
280
+ export type Snapshot = typeof Snapshot.Type
281
+
282
+ /**
283
+ * Largest number of entries one {@link ReadRequest} may ask for.
284
+ *
285
+ * `limit` is request-controlled fan-out: the server passes it to the journal
286
+ * as the page size, once per covered run, so an unbounded limit made one
287
+ * workspace read materialize an unbounded number of rows per run. The frame
288
+ * ceiling does not cover it — that drops entries AFTER they have been read.
289
+ * A request above this bound is refused at the wire, and an in-process caller
290
+ * that bypasses the schema is clamped to it.
291
+ *
292
+ * @category limits
293
+ * @since 0.1.0
294
+ */
295
+ export const maxReadLimit = 1024
296
+
297
+ /**
298
+ * Largest number of frames one {@link SubscribeRequest} may hold open.
299
+ *
300
+ * Credit is a hard frame limit, so it is also how long one subscription may
301
+ * pin a server-side fan-out. The bound keeps that lifetime a function of the
302
+ * protocol rather than of what the caller asks for; a follower that wants
303
+ * more resubscribes from the cursors it has, which is what
304
+ * `SyncClient.defaultCredit` already does.
305
+ *
306
+ * @category limits
307
+ * @since 0.1.0
308
+ */
309
+ export const maxSubscribeCredit = 4096
310
+
311
+ /**
312
+ * A durable catch-up request.
313
+ *
314
+ * `capability` authorizes branch runs: a run whose id maps to a shared branch
315
+ * is only read when the capability verifies for that branch. Non-branch runs
316
+ * ignore it.
317
+ *
318
+ * `limit` is bounded on both sides: at least one entry, at most
319
+ * {@link maxReadLimit}.
320
+ *
321
+ * @category models
322
+ * @since 0.1.0
323
+ */
324
+ export const ReadRequest = Schema.Struct({
325
+ protocolVersion: Schema.optional(Schema.Int),
326
+ scope: Scope,
327
+ cursors: WorkspaceCursor,
328
+ limit: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(maxReadLimit)),
329
+ capability: Schema.optional(ShareCapability)
330
+ })
331
+
332
+ /**
333
+ * A durable catch-up request.
334
+ *
335
+ * @category models
336
+ * @since 0.1.0
337
+ */
338
+ export type ReadRequest = typeof ReadRequest.Type
339
+
340
+ /**
341
+ * One page of durable catch-up entries.
342
+ *
343
+ * `done` reports that the page reached the durable tail, which is the point at
344
+ * which a client may switch from paging to following.
345
+ *
346
+ * @category models
347
+ * @since 0.1.0
348
+ */
349
+ export const ReadResponse = Schema.Struct({
350
+ entries: Schema.Array(JournalEvent.Entry),
351
+ cursors: WorkspaceCursor,
352
+ done: Schema.Boolean
353
+ })
354
+
355
+ /**
356
+ * One page of durable catch-up entries.
357
+ *
358
+ * @category models
359
+ * @since 0.1.0
360
+ */
361
+ export type ReadResponse = typeof ReadResponse.Type
362
+
363
+ /**
364
+ * One page of durable catch-up entries as a SERVER states it.
365
+ *
366
+ * Every cursor carries its generation. {@link SyncServer.Service} answers with
367
+ * this shape; {@link ReadResponse} is the lenient shape a client decodes an
368
+ * untrusted server's page into before refusing a missing generation.
369
+ *
370
+ * @category models
371
+ * @since 1.0.0-rc.0
372
+ */
373
+ export const ServerReadResponse = Schema.Struct({
374
+ ...ReadResponse.fields,
375
+ cursors: Schema.Array(ServerCursor)
376
+ })
377
+
378
+ /**
379
+ * One page of durable catch-up entries as a server states it.
380
+ *
381
+ * @category models
382
+ * @since 1.0.0-rc.0
383
+ */
384
+ export type ServerReadResponse = typeof ServerReadResponse.Type
385
+
386
+ /**
387
+ * A request to follow committed entries.
388
+ *
389
+ * `capability` authorizes branch runs exactly as on {@link ReadRequest}.
390
+ *
391
+ * `credit` is bounded on both sides: at least one frame, at most
392
+ * {@link maxSubscribeCredit}. Zero is refused rather than served as an
393
+ * immediately-empty stream, because the two readings of it — "open a window
394
+ * of nothing" and "a caller computed its window wrong" — are
395
+ * indistinguishable on the wire, and the second one busy-loops a follow that
396
+ * replenishes by resubscribing.
397
+ *
398
+ * @category models
399
+ * @since 0.1.0
400
+ */
401
+ export const SubscribeRequest = Schema.Struct({
402
+ protocolVersion: Schema.optional(Schema.Int),
403
+ scope: Scope,
404
+ cursors: WorkspaceCursor,
405
+ credit: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(maxSubscribeCredit)),
406
+ capability: Schema.optional(ShareCapability)
407
+ })
408
+
409
+ /**
410
+ * A request to follow committed entries.
411
+ *
412
+ * @category models
413
+ * @since 0.1.0
414
+ */
415
+ export type SubscribeRequest = typeof SubscribeRequest.Type
416
+
417
+ /**
418
+ * A contiguous batch of committed entries for one run.
419
+ *
420
+ * `fromSeq` and `toSeq` describe the interval the server covered, not the
421
+ * sequences actually carried: dropped admissions leave legitimate holes.
422
+ * `generation` identifies the history covering the interval and is mandatory
423
+ * on server responses, including zero. Clients refuse its omission before
424
+ * deduplicating by sequence. The optional schema permits a typed refusal.
425
+ *
426
+ * @category models
427
+ * @since 0.1.0
428
+ */
429
+ export const EntriesFrame = Schema.TaggedStruct("Entries", {
430
+ runId: JournalEvent.RunId,
431
+ generation: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
432
+ fromSeq: JournalEvent.Seq,
433
+ toSeq: JournalEvent.Seq,
434
+ entries: Schema.Array(JournalEvent.Entry)
435
+ })
436
+
437
+ /**
438
+ * A contiguous batch of committed entries for one run.
439
+ *
440
+ * @category models
441
+ * @since 0.1.0
442
+ */
443
+ export type EntriesFrame = typeof EntriesFrame.Type
444
+
445
+ /**
446
+ * A liveness frame reserved for a future revision, which no rc.0 server emits.
447
+ *
448
+ * No server in this package constructs one at rc.0, and a client must not wait
449
+ * for one: {@link SyncClient} tolerates the variant and ignores it, which is
450
+ * the whole of the contract today. It stays in the {@link Frame} union so
451
+ * adding the frame later is not a wire break, and so a third-party client
452
+ * written against this schema already ignores it.
453
+ *
454
+ * Keepalive is a transport concern at rc.0. Emitting a heartbeat here would
455
+ * spend the subscription's `credit`, which is a hard frame limit, so an idle
456
+ * follow would resubscribe on a timer instead of staying quiet.
457
+ *
458
+ * @category models
459
+ * @since 0.1.0
460
+ */
461
+ export const HeartbeatFrame = Schema.TaggedStruct("Heartbeat", {})
462
+
463
+ /**
464
+ * A terminal frame reporting that the server ended the subscription.
465
+ *
466
+ * @category models
467
+ * @since 0.1.0
468
+ */
469
+ export const ClosedFrame = Schema.TaggedStruct("Closed", {
470
+ reason: Schema.optional(Schema.String)
471
+ })
472
+
473
+ /**
474
+ * Any frame a subscription may emit.
475
+ *
476
+ * @category models
477
+ * @since 0.1.0
478
+ */
479
+ export const Frame = Schema.Union([EntriesFrame, HeartbeatFrame, ClosedFrame])
480
+
481
+ /**
482
+ * Any frame a subscription may emit.
483
+ *
484
+ * @category models
485
+ * @since 0.1.0
486
+ */
487
+ export type Frame = typeof Frame.Type
488
+
489
+ /**
490
+ * A contiguous batch of committed entries as a SERVER states it.
491
+ *
492
+ * `generation` is required here: a server that covers an interval knows the
493
+ * history it read, so omitting it is a compile error rather than a run-time
494
+ * refusal. {@link EntriesFrame} is the lenient shape a client decodes an
495
+ * untrusted server's frame into.
496
+ *
497
+ * @category models
498
+ * @since 1.0.0-rc.0
499
+ */
500
+ export const ServerEntriesFrame = Schema.TaggedStruct("Entries", {
501
+ runId: JournalEvent.RunId,
502
+ generation: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
503
+ fromSeq: JournalEvent.Seq,
504
+ toSeq: JournalEvent.Seq,
505
+ entries: Schema.Array(JournalEvent.Entry)
506
+ })
507
+
508
+ /**
509
+ * A contiguous batch of committed entries as a server states it.
510
+ *
511
+ * @category models
512
+ * @since 1.0.0-rc.0
513
+ */
514
+ export type ServerEntriesFrame = typeof ServerEntriesFrame.Type
515
+
516
+ /**
517
+ * Any frame a subscription may emit, as a server states it.
518
+ *
519
+ * @category models
520
+ * @since 1.0.0-rc.0
521
+ */
522
+ export const ServerFrame = Schema.Union([ServerEntriesFrame, HeartbeatFrame, ClosedFrame])
523
+
524
+ /**
525
+ * Any frame a subscription may emit, as a server states it.
526
+ *
527
+ * @category models
528
+ * @since 1.0.0-rc.0
529
+ */
530
+ export type ServerFrame = typeof ServerFrame.Type
531
+
532
+ /**
533
+ * Whether a scope covers a run.
534
+ *
535
+ * @category predicates
536
+ * @since 0.1.0
537
+ */
538
+ export const covers = (scope: Scope, runId: JournalEvent.RunId): boolean =>
539
+ scope._tag === "Workspace" || scope.runId === runId
540
+
541
+ /**
542
+ * Default ceiling on the encoded entries one frame or page may
543
+ * carry, in bytes.
544
+ *
545
+ * Both ends enforce the sum of encoded journal entries, excluding the outer
546
+ * RPC/frame envelope. Transports must allow their own envelope overhead.
547
+ * Commands default to 1 MiB; the 2 MiB entry ceiling includes their durable
548
+ * journal envelope and keeps already-admitted maximum commands consumable.
549
+ *
550
+ * @category limits
551
+ * @since 0.1.0
552
+ */
553
+ export const defaultMaxFrameBytes = 2 * 1024 * 1024
554
+
555
+ const utf8 = new TextEncoder()
556
+
557
+ /** The four bytes `null` occupies, which is what a value with no JSON text
558
+ * costs once it is inside an encoded frame. */
559
+ const nullByteLength = 4
560
+
561
+ /**
562
+ * The wire size of one value: the UTF-8 byte length of its JSON text.
563
+ *
564
+ * Ceilings measure this encoded form rather than in-memory object size, so a
565
+ * limit tracks what a transport actually carries.
566
+ *
567
+ * The function is TOTAL, because every caller is a size guard whose whole job
568
+ * is to produce a typed `frame_too_large` refusal rather than a defect:
569
+ *
570
+ * - A value with no JSON text at all — `undefined`, a function, a symbol —
571
+ * measures {@link nullByteLength}, the bytes it occupies once an enclosing
572
+ * array or object encodes it as `null`. It used to measure zero, which let
573
+ * an unencodable value pass every ceiling.
574
+ * - A value `JSON.stringify` refuses — a cycle, a `bigint`, a throwing
575
+ * `toJSON` — measures `Infinity`, so it trips every ceiling instead of
576
+ * throwing a `TypeError` out of code that only ever fails typed.
577
+ *
578
+ * It measures the JSON text, so a `toJSON` method, a getter, or a proxy trap
579
+ * on the value runs while the size is taken; the read path measures each
580
+ * entry once per end. Entries that arrive over the wire are schema-decoded
581
+ * data and have none of those.
582
+ *
583
+ * @category limits
584
+ * @since 0.1.0
585
+ */
586
+ export const encodedByteLength = (value: unknown): number => {
587
+ let text: string | undefined
588
+ try {
589
+ text = JSON.stringify(value)
590
+ } catch {
591
+ return Number.POSITIVE_INFINITY
592
+ }
593
+ return text === undefined ? nullByteLength : utf8.encode(text).length
594
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Schema-backed RPC projection of the sync read path.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+
7
+ import { Rpc, RpcGroup, RpcMiddleware } from "effect/unstable/rpc"
8
+ import { SyncError } from "./SyncError.ts"
9
+ import { Frame, ReadRequest, ReadResponse, Snapshot, SnapshotRequest, SubscribeRequest } from "./SyncProtocol.ts"
10
+
11
+ /**
12
+ * Authentication boundary for the sync RPC group.
13
+ *
14
+ * The middleware authenticates each request to a `SyncPrincipal` before the
15
+ * handler runs. The production implementation is `SyncAuth.layer`, which
16
+ * verifies the workspace capability presented in the request headers and
17
+ * installs the resulting principal; a request without a credential runs as
18
+ * the anonymous principal, which the server refuses for every non-branch
19
+ * read.
20
+ *
21
+ * @category middleware
22
+ * @since 0.1.0
23
+ */
24
+ export class SyncAuth extends RpcMiddleware.Service<SyncAuth>()("@smthrs/sync/SyncAuth", { error: SyncError }) {}
25
+
26
+ /**
27
+ * The remote procedures of the sync read path.
28
+ *
29
+ * A subscription's `credit` is a hard frame limit. Clients reconnect with
30
+ * their materialized cursor to replenish it; there is no separate
31
+ * acknowledgement channel.
32
+ *
33
+ * @category groups
34
+ * @since 0.1.0
35
+ */
36
+ export const SyncRpcs = RpcGroup.make(
37
+ Rpc.make("Sync.Snapshot", { payload: SnapshotRequest, success: Snapshot, error: SyncError }),
38
+ Rpc.make("Sync.Read", { payload: ReadRequest, success: ReadResponse, error: SyncError }),
39
+ Rpc.make("Sync.Subscribe", { payload: SubscribeRequest, success: Frame, error: SyncError, stream: true })
40
+ ).middleware(SyncAuth)