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