@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.
- package/CHANGELOG.md +208 -0
- package/LICENSE +21 -0
- package/README.md +208 -2
- package/dist/cjs/BranchCommands.d.ts +228 -0
- package/dist/cjs/BranchCommands.d.ts.map +1 -0
- package/dist/cjs/BranchCommands.js +316 -0
- package/dist/cjs/BranchCommands.js.map +7 -0
- package/dist/cjs/BranchIds.d.ts +65 -0
- package/dist/cjs/BranchIds.d.ts.map +1 -0
- package/dist/cjs/BranchIds.js +53 -0
- package/dist/cjs/BranchIds.js.map +7 -0
- package/dist/cjs/BranchPresence.d.ts +195 -0
- package/dist/cjs/BranchPresence.d.ts.map +1 -0
- package/dist/cjs/BranchPresence.js +276 -0
- package/dist/cjs/BranchPresence.js.map +7 -0
- package/dist/cjs/BranchProjection.d.ts +179 -0
- package/dist/cjs/BranchProjection.d.ts.map +1 -0
- package/dist/cjs/BranchProjection.js +197 -0
- package/dist/cjs/BranchProjection.js.map +7 -0
- package/dist/cjs/BranchProtocol.d.ts +415 -0
- package/dist/cjs/BranchProtocol.d.ts.map +1 -0
- package/dist/cjs/BranchProtocol.js +172 -0
- package/dist/cjs/BranchProtocol.js.map +7 -0
- package/dist/cjs/BranchRpcs.d.ts +177 -0
- package/dist/cjs/BranchRpcs.d.ts.map +1 -0
- package/dist/cjs/BranchRpcs.js +85 -0
- package/dist/cjs/BranchRpcs.js.map +7 -0
- package/dist/cjs/BranchServer.d.ts +55 -0
- package/dist/cjs/BranchServer.d.ts.map +1 -0
- package/dist/cjs/BranchServer.js +147 -0
- package/dist/cjs/BranchServer.js.map +7 -0
- package/dist/cjs/BranchShare.d.ts +159 -0
- package/dist/cjs/BranchShare.d.ts.map +1 -0
- package/dist/cjs/BranchShare.js +147 -0
- package/dist/cjs/BranchShare.js.map +7 -0
- package/dist/cjs/RunCatalog.d.ts +207 -0
- package/dist/cjs/RunCatalog.d.ts.map +1 -0
- package/dist/cjs/RunCatalog.js +122 -0
- package/dist/cjs/RunCatalog.js.map +7 -0
- package/dist/cjs/SyncAuth.d.ts +70 -0
- package/dist/cjs/SyncAuth.d.ts.map +1 -0
- package/dist/cjs/SyncAuth.js +109 -0
- package/dist/cjs/SyncAuth.js.map +7 -0
- package/dist/cjs/SyncClient.d.ts +274 -0
- package/dist/cjs/SyncClient.d.ts.map +1 -0
- package/dist/cjs/SyncClient.js +474 -0
- package/dist/cjs/SyncClient.js.map +7 -0
- package/dist/cjs/SyncError.d.ts +107 -0
- package/dist/cjs/SyncError.d.ts.map +1 -0
- package/dist/cjs/SyncError.js +110 -0
- package/dist/cjs/SyncError.js.map +7 -0
- package/dist/cjs/SyncPrincipal.d.ts +100 -0
- package/dist/cjs/SyncPrincipal.d.ts.map +1 -0
- package/dist/cjs/SyncPrincipal.js +48 -0
- package/dist/cjs/SyncPrincipal.js.map +7 -0
- package/dist/cjs/SyncProtocol.d.ts +592 -0
- package/dist/cjs/SyncProtocol.d.ts.map +1 -0
- package/dist/cjs/SyncProtocol.js +165 -0
- package/dist/cjs/SyncProtocol.js.map +7 -0
- package/dist/cjs/SyncRpcs.d.ts +92 -0
- package/dist/cjs/SyncRpcs.d.ts.map +1 -0
- package/dist/cjs/SyncRpcs.js +35 -0
- package/dist/cjs/SyncRpcs.js.map +7 -0
- package/dist/cjs/SyncServer.d.ts +184 -0
- package/dist/cjs/SyncServer.d.ts.map +1 -0
- package/dist/cjs/SyncServer.js +513 -0
- package/dist/cjs/SyncServer.js.map +7 -0
- package/dist/cjs/WorkspaceShare.d.ts +174 -0
- package/dist/cjs/WorkspaceShare.d.ts.map +1 -0
- package/dist/cjs/WorkspaceShare.js +149 -0
- package/dist/cjs/WorkspaceShare.js.map +7 -0
- package/dist/cjs/index.d.ts +91 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +67 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/Admission.d.ts +43 -0
- package/dist/cjs/internal/Admission.d.ts.map +1 -0
- package/dist/cjs/internal/Admission.js +90 -0
- package/dist/cjs/internal/Admission.js.map +7 -0
- package/dist/cjs/internal/CauseText.d.ts +55 -0
- package/dist/cjs/internal/CauseText.d.ts.map +1 -0
- package/dist/cjs/internal/CauseText.js +38 -0
- package/dist/cjs/internal/CauseText.js.map +7 -0
- package/dist/cjs/internal/JournalErrorCode.d.ts +23 -0
- package/dist/cjs/internal/JournalErrorCode.d.ts.map +1 -0
- package/dist/cjs/internal/JournalErrorCode.js +34 -0
- package/dist/cjs/internal/JournalErrorCode.js.map +7 -0
- package/dist/cjs/internal/PolicyOptions.d.ts +57 -0
- package/dist/cjs/internal/PolicyOptions.d.ts.map +1 -0
- package/dist/cjs/internal/PolicyOptions.js +62 -0
- package/dist/cjs/internal/PolicyOptions.js.map +7 -0
- package/dist/cjs/internal/ProtocolVersion.d.ts +17 -0
- package/dist/cjs/internal/ProtocolVersion.d.ts.map +1 -0
- package/dist/cjs/internal/ProtocolVersion.js +44 -0
- package/dist/cjs/internal/ProtocolVersion.js.map +7 -0
- package/dist/cjs/internal/ShareSigner.d.ts +173 -0
- package/dist/cjs/internal/ShareSigner.d.ts.map +1 -0
- package/dist/cjs/internal/ShareSigner.js +150 -0
- package/dist/cjs/internal/ShareSigner.js.map +7 -0
- package/dist/cjs/internal/SnapshotBoundary.d.ts +17 -0
- package/dist/cjs/internal/SnapshotBoundary.d.ts.map +1 -0
- package/dist/cjs/internal/SnapshotBoundary.js +97 -0
- package/dist/cjs/internal/SnapshotBoundary.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/TestSocket.d.ts +55 -0
- package/dist/cjs/test/TestSocket.d.ts.map +1 -0
- package/dist/cjs/test/TestSocket.js +145 -0
- package/dist/cjs/test/TestSocket.js.map +7 -0
- package/dist/cjs/test/TestSync.d.ts +54 -0
- package/dist/cjs/test/TestSync.d.ts.map +1 -0
- package/dist/cjs/test/TestSync.js +96 -0
- package/dist/cjs/test/TestSync.js.map +7 -0
- package/dist/esm/BranchCommands.d.ts +228 -0
- package/dist/esm/BranchCommands.d.ts.map +1 -0
- package/dist/esm/BranchCommands.js +496 -0
- package/dist/esm/BranchCommands.js.map +1 -0
- package/dist/esm/BranchIds.d.ts +65 -0
- package/dist/esm/BranchIds.d.ts.map +1 -0
- package/dist/esm/BranchIds.js +61 -0
- package/dist/esm/BranchIds.js.map +1 -0
- package/dist/esm/BranchPresence.d.ts +195 -0
- package/dist/esm/BranchPresence.d.ts.map +1 -0
- package/dist/esm/BranchPresence.js +361 -0
- package/dist/esm/BranchPresence.js.map +1 -0
- package/dist/esm/BranchProjection.d.ts +179 -0
- package/dist/esm/BranchProjection.d.ts.map +1 -0
- package/dist/esm/BranchProjection.js +269 -0
- package/dist/esm/BranchProjection.js.map +1 -0
- package/dist/esm/BranchProtocol.d.ts +415 -0
- package/dist/esm/BranchProtocol.d.ts.map +1 -0
- package/dist/esm/BranchProtocol.js +323 -0
- package/dist/esm/BranchProtocol.js.map +1 -0
- package/dist/esm/BranchRpcs.d.ts +177 -0
- package/dist/esm/BranchRpcs.d.ts.map +1 -0
- package/dist/esm/BranchRpcs.js +103 -0
- package/dist/esm/BranchRpcs.js.map +1 -0
- package/dist/esm/BranchServer.d.ts +55 -0
- package/dist/esm/BranchServer.d.ts.map +1 -0
- package/dist/esm/BranchServer.js +168 -0
- package/dist/esm/BranchServer.js.map +1 -0
- package/dist/esm/BranchShare.d.ts +159 -0
- package/dist/esm/BranchShare.d.ts.map +1 -0
- package/dist/esm/BranchShare.js +213 -0
- package/dist/esm/BranchShare.js.map +1 -0
- package/dist/esm/RunCatalog.d.ts +207 -0
- package/dist/esm/RunCatalog.d.ts.map +1 -0
- package/dist/esm/RunCatalog.js +196 -0
- package/dist/esm/RunCatalog.js.map +1 -0
- package/dist/esm/SyncAuth.d.ts +70 -0
- package/dist/esm/SyncAuth.d.ts.map +1 -0
- package/dist/esm/SyncAuth.js +119 -0
- package/dist/esm/SyncAuth.js.map +1 -0
- package/dist/esm/SyncClient.d.ts +274 -0
- package/dist/esm/SyncClient.d.ts.map +1 -0
- package/dist/esm/SyncClient.js +586 -0
- package/dist/esm/SyncClient.js.map +1 -0
- package/dist/esm/SyncError.d.ts +107 -0
- package/dist/esm/SyncError.d.ts.map +1 -0
- package/dist/esm/SyncError.js +133 -0
- package/dist/esm/SyncError.js.map +1 -0
- package/dist/esm/SyncPrincipal.d.ts +100 -0
- package/dist/esm/SyncPrincipal.d.ts.map +1 -0
- package/dist/esm/SyncPrincipal.js +58 -0
- package/dist/esm/SyncPrincipal.js.map +1 -0
- package/dist/esm/SyncProtocol.d.ts +592 -0
- package/dist/esm/SyncProtocol.d.ts.map +1 -0
- package/dist/esm/SyncProtocol.js +434 -0
- package/dist/esm/SyncProtocol.js.map +1 -0
- package/dist/esm/SyncRpcs.d.ts +92 -0
- package/dist/esm/SyncRpcs.d.ts.map +1 -0
- package/dist/esm/SyncRpcs.js +35 -0
- package/dist/esm/SyncRpcs.js.map +1 -0
- package/dist/esm/SyncServer.d.ts +184 -0
- package/dist/esm/SyncServer.d.ts.map +1 -0
- package/dist/esm/SyncServer.js +897 -0
- package/dist/esm/SyncServer.js.map +1 -0
- package/dist/esm/WorkspaceShare.d.ts +174 -0
- package/dist/esm/WorkspaceShare.d.ts.map +1 -0
- package/dist/esm/WorkspaceShare.js +208 -0
- package/dist/esm/WorkspaceShare.js.map +1 -0
- package/dist/esm/index.d.ts +91 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +91 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/Admission.d.ts +43 -0
- package/dist/esm/internal/Admission.d.ts.map +1 -0
- package/dist/esm/internal/Admission.js +80 -0
- package/dist/esm/internal/Admission.js.map +1 -0
- package/dist/esm/internal/CauseText.d.ts +55 -0
- package/dist/esm/internal/CauseText.d.ts.map +1 -0
- package/dist/esm/internal/CauseText.js +67 -0
- package/dist/esm/internal/CauseText.js.map +1 -0
- package/dist/esm/internal/JournalErrorCode.d.ts +23 -0
- package/dist/esm/internal/JournalErrorCode.d.ts.map +1 -0
- package/dist/esm/internal/JournalErrorCode.js +44 -0
- package/dist/esm/internal/JournalErrorCode.js.map +1 -0
- package/dist/esm/internal/PolicyOptions.d.ts +57 -0
- package/dist/esm/internal/PolicyOptions.d.ts.map +1 -0
- package/dist/esm/internal/PolicyOptions.js +70 -0
- package/dist/esm/internal/PolicyOptions.js.map +1 -0
- package/dist/esm/internal/ProtocolVersion.d.ts +17 -0
- package/dist/esm/internal/ProtocolVersion.d.ts.map +1 -0
- package/dist/esm/internal/ProtocolVersion.js +24 -0
- package/dist/esm/internal/ProtocolVersion.js.map +1 -0
- package/dist/esm/internal/ShareSigner.d.ts +173 -0
- package/dist/esm/internal/ShareSigner.d.ts.map +1 -0
- package/dist/esm/internal/ShareSigner.js +219 -0
- package/dist/esm/internal/ShareSigner.js.map +1 -0
- package/dist/esm/internal/SnapshotBoundary.d.ts +17 -0
- package/dist/esm/internal/SnapshotBoundary.d.ts.map +1 -0
- package/dist/esm/internal/SnapshotBoundary.js +68 -0
- package/dist/esm/internal/SnapshotBoundary.js.map +1 -0
- package/dist/esm/test/TestSocket.d.ts +55 -0
- package/dist/esm/test/TestSocket.d.ts.map +1 -0
- package/dist/esm/test/TestSocket.js +128 -0
- package/dist/esm/test/TestSocket.js.map +1 -0
- package/dist/esm/test/TestSync.d.ts +54 -0
- package/dist/esm/test/TestSync.d.ts.map +1 -0
- package/dist/esm/test/TestSync.js +76 -0
- package/dist/esm/test/TestSync.js.map +1 -0
- package/docs/README.md +156 -0
- package/docs/api.md +328 -0
- package/docs/concepts/authorization.md +117 -0
- package/docs/concepts/branches.md +195 -0
- package/docs/concepts/compaction.md +144 -0
- package/docs/concepts/replay-then-follow.md +129 -0
- package/docs/concepts/scopes-and-cursors.md +135 -0
- package/docs/guides/authorize-a-connection.md +174 -0
- package/docs/guides/follow-a-run.md +174 -0
- package/docs/guides/handle-a-compacted-run.md +158 -0
- package/docs/guides/list-workspace-runs.md +120 -0
- package/docs/guides/run-a-long-soak.md +80 -0
- package/docs/guides/serve-the-read-path.md +146 -0
- package/docs/guides/test-a-follower.md +273 -0
- package/docs/installation.md +92 -0
- package/docs/protocol.md +183 -0
- package/docs/quickstart.md +159 -0
- package/docs/troubleshooting.md +234 -0
- package/package.json +304 -3
- package/src/BranchCommands.ts +684 -0
- package/src/BranchIds.ts +76 -0
- package/src/BranchPresence.ts +512 -0
- package/src/BranchProjection.ts +308 -0
- package/src/BranchProtocol.ts +411 -0
- package/src/BranchRpcs.ts +163 -0
- package/src/BranchServer.ts +202 -0
- package/src/BranchShare.ts +297 -0
- package/src/RunCatalog.ts +308 -0
- package/src/SyncAuth.ts +146 -0
- package/src/SyncClient.ts +902 -0
- package/src/SyncError.ts +147 -0
- package/src/SyncPrincipal.ts +115 -0
- package/src/SyncProtocol.ts +594 -0
- package/src/SyncRpcs.ts +40 -0
- package/src/SyncServer.ts +1184 -0
- package/src/WorkspaceShare.ts +292 -0
- package/src/index.ts +107 -0
- package/src/internal/Admission.ts +119 -0
- package/src/internal/CauseText.ts +74 -0
- package/src/internal/JournalErrorCode.ts +46 -0
- package/src/internal/PolicyOptions.ts +97 -0
- package/src/internal/ProtocolVersion.ts +28 -0
- package/src/internal/ShareSigner.ts +300 -0
- package/src/internal/SnapshotBoundary.ts +91 -0
- package/src/test/TestSocket.ts +192 -0
- 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.
|