@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,68 @@
1
+ /** Snapshot validation shared by the hosted and browser-side boundaries.
2
+ * @since 1.0.0-rc.0
3
+ */
4
+ import * as Effect from "effect/Effect";
5
+ import * as Schema from "effect/Schema";
6
+ import { SyncError } from "../SyncError.js";
7
+ import * as Protocol from "../SyncProtocol.js";
8
+ import { causeCode } from "./CauseText.js";
9
+ import { requireVersion } from "./ProtocolVersion.js";
10
+ /** Decode a detached request before callbacks can mutate its expected identity.
11
+ * @category validation
12
+ * @since 1.0.0-rc.0
13
+ */
14
+ export const request = (value) => Effect.try({
15
+ try: () => Schema.decodeUnknownSync(Protocol.SnapshotRequest)(value, { onExcessProperty: "error" }),
16
+ catch: (cause) => new SyncError({ code: "invalid_request", message: "Invalid public snapshot request", cause: causeCode(cause) })
17
+ }).pipe(
18
+ // The schema admits any integer version so this refusal, not a decode
19
+ // failure, is what a version mismatch produces here as on every other RPC.
20
+ Effect.tap((decoded) => requireVersion(decoded.protocolVersion)), Effect.map((decoded) => ({ ...decoded })));
21
+ /** Copy JSON bytes once, bound them, then check the exact requested projection.
22
+ * @category validation
23
+ * @since 1.0.0-rc.0
24
+ */
25
+ export const response = (expected, value, maxBytes) => Effect.gen(function* () {
26
+ const text = yield* Effect.try({
27
+ try: () => JSON.stringify(value),
28
+ catch: (cause) => new SyncError({
29
+ code: "decode_failed",
30
+ message: "Public snapshot is not JSON encodable",
31
+ cause: causeCode(cause)
32
+ })
33
+ });
34
+ if (text === undefined) {
35
+ return yield* Effect.fail(new SyncError({ code: "decode_failed", message: "Public snapshot is missing", cause: "undefined" }));
36
+ }
37
+ if (new TextEncoder().encode(text).length > maxBytes) {
38
+ return yield* Effect.fail(new SyncError({ code: "frame_too_large", message: "Public snapshot exceeds the response byte limit" }));
39
+ }
40
+ const snapshot = yield* Effect.try({
41
+ try: () => {
42
+ // JSON.stringify alone silently drops undefined/functions and turns
43
+ // non-finite numbers into null. Reject such provider values rather than
44
+ // returning a different state under the same checkpoint sequence.
45
+ Schema.decodeUnknownSync(Protocol.Snapshot)(value, { onExcessProperty: "error" });
46
+ return Schema.decodeUnknownSync(Protocol.Snapshot)(JSON.parse(text), { onExcessProperty: "error" });
47
+ },
48
+ catch: (cause) => new SyncError({ code: "decode_failed", message: "Invalid public snapshot response", cause: causeCode(cause) })
49
+ });
50
+ yield* requireVersion(snapshot.protocolVersion);
51
+ if (snapshot.runId !== expected.runId || snapshot.lineageId !== expected.lineageId ||
52
+ snapshot.projection !== expected.projection || snapshot.projectionVersion !== expected.projectionVersion) {
53
+ return yield* Effect.fail(new SyncError({
54
+ code: "protocol_violation",
55
+ message: "Public snapshot identity does not match the request",
56
+ cause: "snapshot_identity_mismatch"
57
+ }));
58
+ }
59
+ if (snapshot.seq < expected.atLeastSeq) {
60
+ return yield* Effect.fail(new SyncError({
61
+ code: "compacted",
62
+ message: "Public snapshot does not cover the required history",
63
+ resync: { runId: expected.runId, checkpointSeq: expected.atLeastSeq }
64
+ }));
65
+ }
66
+ return snapshot;
67
+ });
68
+ //# sourceMappingURL=SnapshotBoundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SnapshotBoundary.js","sourceRoot":"","sources":["../../../src/internal/SnapshotBoundary.ts"],"names":[],"mappings":"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","sourcesContent":["/** Snapshot validation shared by the hosted and browser-side boundaries.\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport * as Schema from \"effect/Schema\"\nimport { SyncError } from \"../SyncError.ts\"\nimport * as Protocol from \"../SyncProtocol.ts\"\nimport { causeCode } from \"./CauseText.ts\"\nimport { requireVersion } from \"./ProtocolVersion.ts\"\n\n/** Decode a detached request before callbacks can mutate its expected identity.\n * @category validation\n * @since 1.0.0-rc.0\n */\nexport const request = (value: unknown): Effect.Effect<Protocol.SnapshotRequest, SyncError> =>\n Effect.try({\n try: () => Schema.decodeUnknownSync(Protocol.SnapshotRequest)(value, { onExcessProperty: \"error\" }),\n catch: (cause) =>\n new SyncError({ code: \"invalid_request\", message: \"Invalid public snapshot request\", cause: causeCode(cause) })\n }).pipe(\n // The schema admits any integer version so this refusal, not a decode\n // failure, is what a version mismatch produces here as on every other RPC.\n Effect.tap((decoded) => requireVersion(decoded.protocolVersion)),\n Effect.map((decoded) => ({ ...decoded }))\n )\n\n/** Copy JSON bytes once, bound them, then check the exact requested projection.\n * @category validation\n * @since 1.0.0-rc.0\n */\nexport const response = (\n expected: Protocol.SnapshotRequest,\n value: unknown,\n maxBytes: number\n): Effect.Effect<Protocol.Snapshot, SyncError> =>\n Effect.gen(function*() {\n const text = yield* Effect.try({\n try: () => JSON.stringify(value),\n catch: (cause) =>\n new SyncError({\n code: \"decode_failed\",\n message: \"Public snapshot is not JSON encodable\",\n cause: causeCode(cause)\n })\n })\n if (text === undefined) {\n return yield* Effect.fail(\n new SyncError({ code: \"decode_failed\", message: \"Public snapshot is missing\", cause: \"undefined\" })\n )\n }\n if (new TextEncoder().encode(text).length > maxBytes) {\n return yield* Effect.fail(\n new SyncError({ code: \"frame_too_large\", message: \"Public snapshot exceeds the response byte limit\" })\n )\n }\n const snapshot = yield* Effect.try({\n try: () => {\n // JSON.stringify alone silently drops undefined/functions and turns\n // non-finite numbers into null. Reject such provider values rather than\n // returning a different state under the same checkpoint sequence.\n Schema.decodeUnknownSync(Protocol.Snapshot)(value, { onExcessProperty: \"error\" })\n return Schema.decodeUnknownSync(Protocol.Snapshot)(JSON.parse(text), { onExcessProperty: \"error\" })\n },\n catch: (cause) =>\n new SyncError({ code: \"decode_failed\", message: \"Invalid public snapshot response\", cause: causeCode(cause) })\n })\n yield* requireVersion(snapshot.protocolVersion)\n if (\n snapshot.runId !== expected.runId || snapshot.lineageId !== expected.lineageId ||\n snapshot.projection !== expected.projection || snapshot.projectionVersion !== expected.projectionVersion\n ) {\n return yield* Effect.fail(\n new SyncError({\n code: \"protocol_violation\",\n message: \"Public snapshot identity does not match the request\",\n cause: \"snapshot_identity_mismatch\"\n })\n )\n }\n if (snapshot.seq < expected.atLeastSeq) {\n return yield* Effect.fail(\n new SyncError({\n code: \"compacted\",\n message: \"Public snapshot does not cover the required history\",\n resync: { runId: expected.runId, checkpointSeq: expected.atLeastSeq }\n })\n )\n }\n return snapshot\n })\n"]}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Deterministic in-memory socket transport for sync tests.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as Scope from "effect/Scope";
8
+ import * as Socket from "effect/unstable/socket/Socket";
9
+ /**
10
+ * A predicate or byte transformation installed between the two socket ends.
11
+ * Returning `false` drops the frame.
12
+ *
13
+ * @category models
14
+ * @since 0.1.0
15
+ */
16
+ export type FrameFilter = (bytes: Uint8Array) => boolean | Uint8Array;
17
+ /**
18
+ * Deterministic transport failures that can be injected into a test socket pair.
19
+ *
20
+ * `dropRange` recognizes the JSON representation of an `Entries` frame without
21
+ * importing the sync protocol. Tests using another wire representation can use
22
+ * `installFilter` directly.
23
+ *
24
+ * @category models
25
+ * @since 0.1.0
26
+ */
27
+ export interface TestFaults {
28
+ readonly installFilter: (filter: FrameFilter) => void;
29
+ readonly dropRange: (runId: string, from: number, to: number) => void;
30
+ readonly stall: () => void;
31
+ readonly resume: () => void;
32
+ readonly disconnect: () => Effect.Effect<void>;
33
+ }
34
+ /**
35
+ * The two endpoints and controls of an in-memory socket connection.
36
+ *
37
+ * @category models
38
+ * @since 0.1.0
39
+ */
40
+ export interface Pair {
41
+ readonly client: Socket.Socket;
42
+ readonly server: Socket.Socket;
43
+ readonly faults: TestFaults;
44
+ }
45
+ /**
46
+ * Creates a scoped, bounded, bidirectional in-memory socket connection.
47
+ *
48
+ * Socket runs wait on queue reads and are therefore interrupted with their
49
+ * enclosing scope; no host abort signal or timer is involved.
50
+ *
51
+ * @category constructors
52
+ * @since 0.1.0
53
+ */
54
+ export declare const makePair: () => Effect.Effect<Pair, never, Scope.Scope>;
55
+ //# sourceMappingURL=TestSocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestSocket.d.ts","sourceRoot":"","sources":["../../../src/test/TestSocket.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,+BAA+B,CAAA;AAEvD;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,GAAG,UAAU,CAAA;AAErE;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAA;IACrD,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAC/C;AAED;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAC5B;AAkDD;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,QAAO,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAkF9D,CAAA"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Deterministic in-memory socket transport for sync tests.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Deferred from "effect/Deferred";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Latch from "effect/Latch";
9
+ import * as Queue from "effect/Queue";
10
+ import * as Scope from "effect/Scope";
11
+ import * as Socket from "effect/unstable/socket/Socket";
12
+ const queueCapacity = 16;
13
+ const transportError = () => new Socket.SocketError({
14
+ reason: new Socket.SocketReadError({ cause: new Error("test socket disconnected") })
15
+ });
16
+ const intersects = (leftFrom, leftTo, rightFrom, rightTo) => leftFrom <= rightTo && rightFrom <= leftTo;
17
+ const entriesRange = (bytes) => {
18
+ try {
19
+ const value = JSON.parse(new TextDecoder().decode(bytes));
20
+ return findEntriesRange(value);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ };
26
+ const findEntriesRange = (value) => {
27
+ if (typeof value !== "object" || value === null)
28
+ return undefined;
29
+ const record = value;
30
+ if (record["_tag"] === "Entries" &&
31
+ typeof record["runId"] === "string" &&
32
+ typeof record["fromSeq"] === "number" &&
33
+ typeof record["toSeq"] === "number") {
34
+ return { runId: record["runId"], from: record["fromSeq"], to: record["toSeq"] };
35
+ }
36
+ for (const child of Object.values(record)) {
37
+ if (Array.isArray(child)) {
38
+ for (const element of child) {
39
+ const result = findEntriesRange(element);
40
+ if (result !== undefined)
41
+ return result;
42
+ }
43
+ }
44
+ else {
45
+ const result = findEntriesRange(child);
46
+ if (result !== undefined)
47
+ return result;
48
+ }
49
+ }
50
+ return undefined;
51
+ };
52
+ /**
53
+ * Creates a scoped, bounded, bidirectional in-memory socket connection.
54
+ *
55
+ * Socket runs wait on queue reads and are therefore interrupted with their
56
+ * enclosing scope; no host abort signal or timer is involved.
57
+ *
58
+ * @category constructors
59
+ * @since 0.1.0
60
+ */
61
+ export const makePair = () => Effect.gen(function* () {
62
+ const scope = yield* Effect.scope;
63
+ const clientIncoming = yield* Queue.bounded(queueCapacity);
64
+ const serverIncoming = yield* Queue.bounded(queueCapacity);
65
+ yield* Scope.addFinalizer(scope, Effect.all([Queue.shutdown(clientIncoming), Queue.shutdown(serverIncoming)], { discard: true }));
66
+ const delivery = Latch.makeUnsafe(true);
67
+ const filters = [];
68
+ const offer = (queue, bytes) => Effect.suspend(() => {
69
+ let current = bytes;
70
+ for (const filter of filters) {
71
+ const result = filter(current);
72
+ if (typeof result === "boolean") {
73
+ if (result === false)
74
+ return Effect.void;
75
+ }
76
+ else {
77
+ current = result;
78
+ }
79
+ }
80
+ return Queue.offer(queue, current).pipe(Effect.asVoid);
81
+ });
82
+ const socket = (incoming, outgoing) => {
83
+ const write = (chunk) => {
84
+ if (Socket.isCloseEvent(chunk))
85
+ return Effect.void;
86
+ const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
87
+ return offer(outgoing, bytes);
88
+ };
89
+ return Socket.make({
90
+ reader: Effect.gen(function* () {
91
+ const closed = yield* Deferred.make();
92
+ yield* Effect.addFinalizer(() => Deferred.fail(closed, transportError()));
93
+ return {
94
+ pull: Effect.raceFirst(Effect.map(delivery.whenOpen(Queue.take(incoming)), (bytes) => [bytes]), Deferred.await(closed)),
95
+ upgrade: Socket.SocketUpgradeError.unsupported
96
+ };
97
+ }),
98
+ writer: Effect.succeed({
99
+ write,
100
+ writeAll: (chunks) => Effect.forEach(chunks, write, { discard: true })
101
+ })
102
+ });
103
+ };
104
+ const faults = {
105
+ installFilter: (filter) => {
106
+ filters.push(filter);
107
+ },
108
+ dropRange: (runId, from, to) => {
109
+ filters.push((bytes) => {
110
+ const range = entriesRange(bytes);
111
+ return range === undefined || range.runId !== runId || !intersects(range.from, range.to, from, to);
112
+ });
113
+ },
114
+ stall: () => {
115
+ delivery.closeUnsafe();
116
+ },
117
+ resume: () => {
118
+ delivery.openUnsafe();
119
+ },
120
+ disconnect: () => Effect.andThen(Queue.fail(clientIncoming, transportError()), Queue.fail(serverIncoming, transportError())).pipe(Effect.asVoid)
121
+ };
122
+ return {
123
+ client: socket(clientIncoming, serverIncoming),
124
+ server: socket(serverIncoming, clientIncoming),
125
+ faults
126
+ };
127
+ });
128
+ //# sourceMappingURL=TestSocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestSocket.js","sourceRoot":"","sources":["../../../src/test/TestSocket.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Deterministic in-memory socket transport for sync tests.\n *\n * @since 0.1.0\n */\n\nimport * as Deferred from \"effect/Deferred\"\nimport * as Effect from \"effect/Effect\"\nimport * as Latch from \"effect/Latch\"\nimport * as Queue from \"effect/Queue\"\nimport * as Scope from \"effect/Scope\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\n\n/**\n * A predicate or byte transformation installed between the two socket ends.\n * Returning `false` drops the frame.\n *\n * @category models\n * @since 0.1.0\n */\nexport type FrameFilter = (bytes: Uint8Array) => boolean | Uint8Array\n\n/**\n * Deterministic transport failures that can be injected into a test socket pair.\n *\n * `dropRange` recognizes the JSON representation of an `Entries` frame without\n * importing the sync protocol. Tests using another wire representation can use\n * `installFilter` directly.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface TestFaults {\n readonly installFilter: (filter: FrameFilter) => void\n readonly dropRange: (runId: string, from: number, to: number) => void\n readonly stall: () => void\n readonly resume: () => void\n readonly disconnect: () => Effect.Effect<void>\n}\n\n/**\n * The two endpoints and controls of an in-memory socket connection.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Pair {\n readonly client: Socket.Socket\n readonly server: Socket.Socket\n readonly faults: TestFaults\n}\n\nconst queueCapacity = 16\n\nconst transportError = () =>\n new Socket.SocketError({\n reason: new Socket.SocketReadError({ cause: new Error(\"test socket disconnected\") })\n })\n\nconst intersects = (leftFrom: number, leftTo: number, rightFrom: number, rightTo: number) =>\n leftFrom <= rightTo && rightFrom <= leftTo\n\nconst entriesRange = (\n bytes: Uint8Array\n): { readonly runId: string; readonly from: number; readonly to: number } | undefined => {\n try {\n const value: unknown = JSON.parse(new TextDecoder().decode(bytes))\n return findEntriesRange(value)\n } catch {\n return undefined\n }\n}\n\nconst findEntriesRange = (\n value: unknown\n): { readonly runId: string; readonly from: number; readonly to: number } | undefined => {\n if (typeof value !== \"object\" || value === null) return undefined\n const record = value as Readonly<Record<string, unknown>>\n if (\n record[\"_tag\"] === \"Entries\" &&\n typeof record[\"runId\"] === \"string\" &&\n typeof record[\"fromSeq\"] === \"number\" &&\n typeof record[\"toSeq\"] === \"number\"\n ) {\n return { runId: record[\"runId\"], from: record[\"fromSeq\"], to: record[\"toSeq\"] }\n }\n for (const child of Object.values(record)) {\n if (Array.isArray(child)) {\n for (const element of child) {\n const result = findEntriesRange(element)\n if (result !== undefined) return result\n }\n } else {\n const result = findEntriesRange(child)\n if (result !== undefined) return result\n }\n }\n return undefined\n}\n\n/**\n * Creates a scoped, bounded, bidirectional in-memory socket connection.\n *\n * Socket runs wait on queue reads and are therefore interrupted with their\n * enclosing scope; no host abort signal or timer is involved.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makePair = (): Effect.Effect<Pair, never, Scope.Scope> =>\n Effect.gen(function*() {\n const scope = yield* Effect.scope\n const clientIncoming = yield* Queue.bounded<Uint8Array, Socket.SocketError>(queueCapacity)\n const serverIncoming = yield* Queue.bounded<Uint8Array, Socket.SocketError>(queueCapacity)\n yield* Scope.addFinalizer(\n scope,\n Effect.all([Queue.shutdown(clientIncoming), Queue.shutdown(serverIncoming)], { discard: true })\n )\n const delivery = Latch.makeUnsafe(true)\n const filters: Array<FrameFilter> = []\n\n const offer = (queue: Queue.Queue<Uint8Array, Socket.SocketError>, bytes: Uint8Array) =>\n Effect.suspend(() => {\n let current = bytes\n for (const filter of filters) {\n const result = filter(current)\n if (typeof result === \"boolean\") {\n if (result === false) return Effect.void\n } else {\n current = result\n }\n }\n return Queue.offer(queue, current).pipe(Effect.asVoid)\n })\n\n const socket = (\n incoming: Queue.Queue<Uint8Array, Socket.SocketError>,\n outgoing: Queue.Queue<Uint8Array, Socket.SocketError>\n ) => {\n const write: Socket.Writer[\"write\"] = (chunk) => {\n if (Socket.isCloseEvent(chunk)) return Effect.void\n const bytes = typeof chunk === \"string\" ? new TextEncoder().encode(chunk) : chunk\n return offer(outgoing, bytes)\n }\n return Socket.make({\n reader: Effect.gen(function*() {\n const closed = yield* Deferred.make<never, Socket.SocketError>()\n yield* Effect.addFinalizer(() => Deferred.fail(closed, transportError()))\n return {\n pull: Effect.raceFirst(\n Effect.map(delivery.whenOpen(Queue.take(incoming)), (bytes) => [bytes] as const),\n Deferred.await(closed)\n ),\n upgrade: Socket.SocketUpgradeError.unsupported\n }\n }),\n writer: Effect.succeed({\n write,\n writeAll: (chunks) => Effect.forEach(chunks, write, { discard: true })\n })\n })\n }\n\n const faults: TestFaults = {\n installFilter: (filter) => {\n filters.push(filter)\n },\n dropRange: (runId, from, to) => {\n filters.push((bytes) => {\n const range = entriesRange(bytes)\n return range === undefined || range.runId !== runId || !intersects(range.from, range.to, from, to)\n })\n },\n stall: () => {\n delivery.closeUnsafe()\n },\n resume: () => {\n delivery.openUnsafe()\n },\n disconnect: () =>\n Effect.andThen(\n Queue.fail(clientIncoming, transportError()),\n Queue.fail(serverIncoming, transportError())\n ).pipe(Effect.asVoid)\n }\n\n return {\n client: socket(clientIncoming, serverIncoming),\n server: socket(serverIncoming, clientIncoming),\n faults\n }\n })\n"]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * In-process sync service and RPC transport wiring for tests.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as Layer from "effect/Layer";
8
+ import * as RunCatalog from "../RunCatalog.ts";
9
+ import * as SyncClient from "../SyncClient.ts";
10
+ import * as SyncRpcs from "../SyncRpcs.ts";
11
+ import * as SyncServer from "../SyncServer.ts";
12
+ import type { Pair } from "./TestSocket.ts";
13
+ /**
14
+ * An authentication middleware that trusts every connection as the workspace
15
+ * owner. Test-only by construction: replication-mechanics suites use it so
16
+ * they exercise paging and streaming without provisioning capabilities,
17
+ * while the authentication suites use the production `SyncAuth.layer`.
18
+ *
19
+ * Never compose it into a served transport: every network client would read
20
+ * every workspace run as the owner. The name says so at the call site.
21
+ *
22
+ * @category layers
23
+ * @since 1.0.0-rc.1
24
+ */
25
+ export declare const layerTrustAllAsOwner: Layer.Layer<SyncRpcs.SyncAuth, never, never>;
26
+ /**
27
+ * Provides the durable in-memory journal, a mutable empty run catalog, the
28
+ * owner-trusting {@link layerTrustAllAsOwner} middleware, and the production
29
+ * sync server.
30
+ *
31
+ * Tests that need runs in the workspace may register them through their own
32
+ * `RunCatalog.makeMemory` fixture before providing a more specific layer.
33
+ *
34
+ * @category layers
35
+ * @since 0.1.0
36
+ */
37
+ export declare const layerTest: Layer.Layer<import("@smthrs/journal/Journal").Journal | SyncRpcs.SyncAuth | RunCatalog.RunCatalog | SyncServer.SyncServer, import("effect/unstable/sql/SqlError").SqlError | import("@smthrs/journal/Journal").JournalError | import("effect/unstable/sql/Migrator").MigrationError | import("../SyncError.ts").SyncError, never>;
38
+ /**
39
+ * Provides no-op sync dependencies for consumers that only require the ports.
40
+ *
41
+ * @category layers
42
+ * @since 0.1.0
43
+ */
44
+ export declare const layerNoop: Layer.Layer<SyncRpcs.SyncAuth | RunCatalog.RunCatalog | SyncClient.SyncClient | SyncServer.SyncServer, never, never>;
45
+ /**
46
+ * Starts an RPC server over the pair's server endpoint and returns a sync
47
+ * client over the client endpoint. JSON serialization is deliberately used so
48
+ * frame faults can inspect test traffic without coupling to sync schemas.
49
+ *
50
+ * @category constructors
51
+ * @since 0.1.0
52
+ */
53
+ export declare const connect: (pair: Pair) => Effect.Effect<SyncClient.Service, never, import("effect/Scope").Scope | SyncRpcs.SyncAuth | SyncServer.SyncServer>;
54
+ //# sourceMappingURL=TestSync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestSync.d.ts","sourceRoot":"","sources":["../../../src/test/TestSync.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAOrC,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAE9C,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAC1C,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAI3C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,8CAEhC,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,mUAQrB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,sHAKrB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,IAAI,uHA8B9B,CAAA"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * In-process sync service and RPC transport wiring for tests.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as TestJournal from "@smthrs/journal/test/TestJournal";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Layer from "effect/Layer";
9
+ import * as NetAddress from "effect/unstable/net/NetAddress";
10
+ import * as RpcClient from "effect/unstable/rpc/RpcClient";
11
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
12
+ import * as RpcServer from "effect/unstable/rpc/RpcServer";
13
+ import * as Socket from "effect/unstable/socket/Socket";
14
+ import * as SocketServer from "effect/unstable/socket/SocketServer";
15
+ import * as RunCatalog from "../RunCatalog.js";
16
+ import * as SyncClient from "../SyncClient.js";
17
+ import * as SyncPrincipal from "../SyncPrincipal.js";
18
+ import * as SyncRpcs from "../SyncRpcs.js";
19
+ import * as SyncServer from "../SyncServer.js";
20
+ const layerNoopAuth = Layer.succeed(SyncRpcs.SyncAuth)((effect) => effect);
21
+ /**
22
+ * An authentication middleware that trusts every connection as the workspace
23
+ * owner. Test-only by construction: replication-mechanics suites use it so
24
+ * they exercise paging and streaming without provisioning capabilities,
25
+ * while the authentication suites use the production `SyncAuth.layer`.
26
+ *
27
+ * Never compose it into a served transport: every network client would read
28
+ * every workspace run as the owner. The name says so at the call site.
29
+ *
30
+ * @category layers
31
+ * @since 1.0.0-rc.1
32
+ */
33
+ export const layerTrustAllAsOwner = Layer.succeed(SyncRpcs.SyncAuth)((effect) => Effect.provideService(effect, SyncPrincipal.SyncPrincipal, SyncPrincipal.workspace("test-workspace-owner")));
34
+ /**
35
+ * Provides the durable in-memory journal, a mutable empty run catalog, the
36
+ * owner-trusting {@link layerTrustAllAsOwner} middleware, and the production
37
+ * sync server.
38
+ *
39
+ * Tests that need runs in the workspace may register them through their own
40
+ * `RunCatalog.makeMemory` fixture before providing a more specific layer.
41
+ *
42
+ * @category layers
43
+ * @since 0.1.0
44
+ */
45
+ export const layerTest = SyncServer.layer.pipe(Layer.provideMerge(Layer.mergeAll(TestJournal.layer(), RunCatalog.layerMemory(), layerTrustAllAsOwner)));
46
+ /**
47
+ * Provides no-op sync dependencies for consumers that only require the ports.
48
+ *
49
+ * @category layers
50
+ * @since 0.1.0
51
+ */
52
+ export const layerNoop = Layer.mergeAll(SyncClient.layerNoop(), SyncServer.layerNoop, RunCatalog.layerNoop, layerNoopAuth);
53
+ /**
54
+ * Starts an RPC server over the pair's server endpoint and returns a sync
55
+ * client over the client endpoint. JSON serialization is deliberately used so
56
+ * frame faults can inspect test traffic without coupling to sync schemas.
57
+ *
58
+ * @category constructors
59
+ * @since 0.1.0
60
+ */
61
+ export const connect = (pair) => Effect.gen(function* () {
62
+ const sync = yield* SyncServer.SyncServer;
63
+ const handlers = SyncServer.layerHandlers.pipe(Layer.provide(Layer.succeed(SyncServer.SyncServer, sync)));
64
+ // Use the production schema-aware protocol. JSON.stringify on the raw
65
+ // makeNoSerialization responses happened to encode successes, but not
66
+ // Effect's Cause representation: typed RPC failures never reached clients.
67
+ const serverProtocol = yield* RpcServer.makeProtocolSocketServer.pipe(Effect.provideService(SocketServer.SocketServer, {
68
+ address: NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 0),
69
+ run: (handler) => handler(pair.server).pipe(Effect.orDie, Effect.andThen(Effect.never))
70
+ }), Effect.provide(RpcSerialization.layerJson));
71
+ yield* RpcServer.make(SyncRpcs.SyncRpcs, { disableFatalDefects: true }).pipe(Effect.provideService(RpcServer.Protocol, serverProtocol), Effect.provide(handlers), Effect.forkScoped);
72
+ const protocol = yield* RpcClient.makeProtocolSocket().pipe(Effect.provideService(Socket.Socket, pair.client), Effect.provide(RpcSerialization.layerJson));
73
+ const client = yield* RpcClient.make(SyncRpcs.SyncRpcs).pipe(Effect.provideService(RpcClient.Protocol, protocol));
74
+ return yield* SyncClient.make({ client });
75
+ });
76
+ //# sourceMappingURL=TestSync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestSync.js","sourceRoot":"","sources":["../../../src/test/TestSync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * In-process sync service and RPC transport wiring for tests.\n *\n * @since 0.1.0\n */\n\nimport * as TestJournal from \"@smthrs/journal/test/TestJournal\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as NetAddress from \"effect/unstable/net/NetAddress\"\nimport * as RpcClient from \"effect/unstable/rpc/RpcClient\"\nimport * as RpcSerialization from \"effect/unstable/rpc/RpcSerialization\"\nimport * as RpcServer from \"effect/unstable/rpc/RpcServer\"\nimport * as Socket from \"effect/unstable/socket/Socket\"\nimport * as SocketServer from \"effect/unstable/socket/SocketServer\"\nimport * as RunCatalog from \"../RunCatalog.ts\"\nimport * as SyncClient from \"../SyncClient.ts\"\nimport * as SyncPrincipal from \"../SyncPrincipal.ts\"\nimport * as SyncRpcs from \"../SyncRpcs.ts\"\nimport * as SyncServer from \"../SyncServer.ts\"\nimport type { Pair } from \"./TestSocket.ts\"\n\nconst layerNoopAuth = Layer.succeed(SyncRpcs.SyncAuth)((effect) => effect)\n\n/**\n * An authentication middleware that trusts every connection as the workspace\n * owner. Test-only by construction: replication-mechanics suites use it so\n * they exercise paging and streaming without provisioning capabilities,\n * while the authentication suites use the production `SyncAuth.layer`.\n *\n * Never compose it into a served transport: every network client would read\n * every workspace run as the owner. The name says so at the call site.\n *\n * @category layers\n * @since 1.0.0-rc.1\n */\nexport const layerTrustAllAsOwner = Layer.succeed(SyncRpcs.SyncAuth)((effect) =>\n Effect.provideService(effect, SyncPrincipal.SyncPrincipal, SyncPrincipal.workspace(\"test-workspace-owner\"))\n)\n\n/**\n * Provides the durable in-memory journal, a mutable empty run catalog, the\n * owner-trusting {@link layerTrustAllAsOwner} middleware, and the production\n * sync server.\n *\n * Tests that need runs in the workspace may register them through their own\n * `RunCatalog.makeMemory` fixture before providing a more specific layer.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerTest = SyncServer.layer.pipe(\n Layer.provideMerge(\n Layer.mergeAll(\n TestJournal.layer(),\n RunCatalog.layerMemory(),\n layerTrustAllAsOwner\n )\n )\n)\n\n/**\n * Provides no-op sync dependencies for consumers that only require the ports.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop = Layer.mergeAll(\n SyncClient.layerNoop(),\n SyncServer.layerNoop,\n RunCatalog.layerNoop,\n layerNoopAuth\n)\n\n/**\n * Starts an RPC server over the pair's server endpoint and returns a sync\n * client over the client endpoint. JSON serialization is deliberately used so\n * frame faults can inspect test traffic without coupling to sync schemas.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const connect = (pair: Pair) =>\n Effect.gen(function*() {\n const sync = yield* SyncServer.SyncServer\n const handlers = SyncServer.layerHandlers.pipe(\n Layer.provide(Layer.succeed(SyncServer.SyncServer, sync))\n )\n // Use the production schema-aware protocol. JSON.stringify on the raw\n // makeNoSerialization responses happened to encode successes, but not\n // Effect's Cause representation: typed RPC failures never reached clients.\n const serverProtocol = yield* RpcServer.makeProtocolSocketServer.pipe(\n Effect.provideService(SocketServer.SocketServer, {\n address: NetAddress.inetAddressFromIpStringUnsafe(\"127.0.0.1\", 0),\n run: (handler) => handler(pair.server).pipe(Effect.orDie, Effect.andThen(Effect.never))\n }),\n Effect.provide(RpcSerialization.layerJson)\n )\n yield* RpcServer.make(SyncRpcs.SyncRpcs, { disableFatalDefects: true }).pipe(\n Effect.provideService(RpcServer.Protocol, serverProtocol),\n Effect.provide(handlers),\n Effect.forkScoped\n )\n\n const protocol = yield* RpcClient.makeProtocolSocket().pipe(\n Effect.provideService(Socket.Socket, pair.client),\n Effect.provide(RpcSerialization.layerJson)\n )\n const client = yield* RpcClient.make(SyncRpcs.SyncRpcs).pipe(\n Effect.provideService(RpcClient.Protocol, protocol)\n )\n return yield* SyncClient.make({ client })\n })\n"]}
package/docs/README.md ADDED
@@ -0,0 +1,156 @@
1
+ ---
2
+ title: "@smthrs/sync"
3
+ description: "Read-only replication of a Smithers journal: a wire protocol, a server that pages and streams entries, and a browser-safe client that replays a run's history and then follows it live."
4
+ ---
5
+
6
+ `@smthrs/sync` copies the history of a run to readers that do not own it. It
7
+ carries three things: a wire protocol, a server that serves that protocol over
8
+ [Effect](https://effect.website) RPC, and a browser-safe client that replays a
9
+ run's durable history and then follows it live through one stream.
10
+
11
+ The history it replicates is a [`@smthrs/journal`](/api/journal) run. The read
12
+ path, `SyncRpcs` served by `SyncServer` and followed by `SyncClient`, never
13
+ writes to a journal, so a follower cannot corrupt what it reads.
14
+ `BranchCommands` is the separate write surface: it appends admitted branch
15
+ commands to a branch's journal run after verifying a write-scoped
16
+ `BranchShare` capability.
17
+
18
+ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
19
+
20
+ ## What it solves
21
+
22
+ A long-running job records what it did in a journal, on the machine that ran
23
+ it. Everyone else who cares about that job is somewhere else: a dashboard in a
24
+ browser, a terminal tailing a build another process started, a second engine
25
+ reconciling its own view. Each of them needs the same entries, in the same
26
+ order, without reaching into the writer's database.
27
+
28
+ Writing that by hand means solving the same problems every time, and getting
29
+ any one of them wrong loses history quietly. This package solves them once:
30
+
31
+ - **Catch up and then follow, through one call.** `subscribe` pages durable
32
+ history until the server reports it reached the tail, then switches to live
33
+ frames. The consumer sees one stream and never sees the switch.
34
+ - **Resume where you stopped.** A cursor is per run and exclusive, so a
35
+ follower that persists its cursors hands them back after a restart and reads
36
+ forward from there.
37
+ - **Acknowledge what you applied, not what you received.** By default a cursor
38
+ names what was delivered. Supply `apply` and the cursor advances only after
39
+ your own write succeeds, so a consumer that fails halfway re-receives the
40
+ entry instead of skipping it.
41
+ - **Hold open a bound, not a workspace.** Every in-flight surface has a
42
+ configured ceiling: page size, frame bytes, subscription credit, concurrent
43
+ journal reads. What one follower holds open is a function of those numbers
44
+ rather than of how far behind it has fallen. Its per-run positions and its
45
+ periodic catalog-wide round still scale with the runs it covers; a local
46
+ append wakes a read of that run alone.
47
+ - **Read nothing until you are authorized.** The default principal is
48
+ anonymous and reads nothing. A connection becomes a reader by presenting a
49
+ signed, expiring capability, and an open subscription ends when that
50
+ capability expires.
51
+ - **Distrust the server.** The client refuses a page or frame that carries
52
+ another run's entry, repeats or reorders a sequence, or serves an entry at or
53
+ below the cursor it asked from. It refuses before any cursor moves, so a
54
+ faulty server or a rewriting proxy costs an error rather than a hole.
55
+
56
+ ## Follow a run
57
+
58
+ A follower composes the client over a transport and subscribes. This program
59
+ prints every entry of one run, starting with the history that already exists:
60
+
61
+ ```ts
62
+ import type { JournalEvent } from "@smthrs/journal"
63
+ import * as SyncClient from "@smthrs/sync/SyncClient"
64
+ import * as Effect from "effect/Effect"
65
+ import * as Layer from "effect/Layer"
66
+ import * as Stream from "effect/Stream"
67
+ import * as RpcClient from "effect/unstable/rpc/RpcClient"
68
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
69
+
70
+ const runId = "build-42" as JournalEvent.RunId
71
+
72
+ export const follow = Effect.gen(function*() {
73
+ const sync = yield* SyncClient.SyncClient
74
+ yield* sync.subscribe({ scope: { _tag: "Run", runId }, cursors: [] }).pipe(
75
+ Stream.runForEach((entry) => Effect.logInfo(`${entry.seq} ${entry.eventType}`))
76
+ )
77
+ })
78
+
79
+ export const clientLayer = SyncClient.layer.pipe(
80
+ Layer.provide(RpcClient.layerProtocolSocket()),
81
+ Layer.provide(RpcSerialization.layerJson)
82
+ )
83
+ ```
84
+
85
+ The socket that protocol runs over comes from your platform: a WebSocket in a
86
+ browser, `@effect/platform-node`'s socket layer in Node. Pass
87
+ `{ _tag: "Workspace" }` instead of a run scope to follow every run the server
88
+ lists and your credential covers.
89
+
90
+ The serving side is `SyncServer` over a journal and a run catalog.
91
+ [`@smthrs/gateway`](/api/gateway) already mounts it on `POST /sync` and
92
+ `/sync/ws`, so a follower against a Smithers gateway writes only the code
93
+ above. [Serve the read path](./guides/serve-the-read-path.md) builds it into a
94
+ host of your own.
95
+
96
+ To watch both halves work with no network and no database, run the
97
+ [quickstart](./quickstart.md): it stands the real server and the real client up
98
+ over an in-memory socket pair and follows an entry that commits while you are
99
+ watching.
100
+
101
+ ## Branch collaboration
102
+
103
+ The package also carries a branch surface: several people editing one shared
104
+ live document, whose entire durable state is one journal run. Reusing a run
105
+ means multiplayer inherits the canonical sequence, the cursors, the gap
106
+ detection, and the resumable follow rather than introducing a second source of
107
+ truth.
108
+
109
+ Branch collaboration ships unserved at 1.0.0-rc.0. The gateway mounts the read
110
+ path and nothing mounts the branch procedures, so treat those modules as a
111
+ library surface waiting for a host. [Branch collaboration](./concepts/branches.md)
112
+ describes what they guarantee.
113
+
114
+ ## Where this sits
115
+
116
+ `@smthrs/sync` is one package of the Smithers durable flow engine, and it owns
117
+ exactly the read path out of that engine. [`@smthrs/flows`](/api/flows) is the
118
+ engine's barrel: it re-exports this package as a namespace next to the journal,
119
+ the stores, and the Node runtime that wires them over one SQLite file, so
120
+ `import { Sync } from "@smthrs/flows"` reaches the same code as
121
+ `import * as Sync from "@smthrs/sync"`. Depend on the barrel when you want a
122
+ working engine in one dependency. Depend on this package when replication is
123
+ the part you need, which is what a browser wants: the root here is browser
124
+ safe, and the barrel's Node runtime has no place in a bundle.
125
+
126
+ The two packages either side of it are worth knowing.
127
+ [`@smthrs/journal`](/api/journal) writes the entries this one replicates and
128
+ owns compaction, which is the one refusal a follower has to recover from.
129
+ [`@smthrs/gateway`](/api/gateway) is the host that puts the read path on a
130
+ port.
131
+
132
+ Above the whole engine sits the
133
+ [`smithers` command-line interface](/api/cli), which runs flows out of a
134
+ project directory without your composing any of this by hand.
135
+
136
+ ## Next steps
137
+
138
+ - [Installation](./installation.md): the import forms, and what a follower and
139
+ a server each add on top of this package.
140
+ - [Quickstart](./quickstart.md): a real server and a real client following one
141
+ run end to end, with no network.
142
+ - [Scopes and cursors](./concepts/scopes-and-cursors.md): what a request covers,
143
+ what a cursor claims, and why journal sequences have holes.
144
+ - [Replay then follow](./concepts/replay-then-follow.md): the two phases of a
145
+ subscription and the bounds on each.
146
+ - [Authorization](./concepts/authorization.md): the two fail-closed boundaries
147
+ and why a subscription carries its own expiry.
148
+ - [Follow a run](./guides/follow-a-run.md): the follower above, with cursors
149
+ you persist and the failures that reach you.
150
+ - [Test a follower](./guides/test-a-follower.md): drop, stall, and corrupt
151
+ frames against the production client.
152
+ - [Wire protocol](./protocol.md): the normative message shapes, for a client in
153
+ another language.
154
+ - [API reference](./api.md): every public export, its bounds, and its defaults.
155
+ - [Troubleshooting](./troubleshooting.md): every error code, the symptom it
156
+ produces, and the change that fixes it.