@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# @smthrs/sync
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- **Breaking.** `BranchShare` and `WorkspaceShare` keyrings refuse a signing
|
|
8
|
+
secret shorter than 32 bytes of UTF-8 with `invalid_request`. One observed
|
|
9
|
+
capability let an attacker search a short secret offline and forge
|
|
10
|
+
capabilities.
|
|
11
|
+
- `BranchPresence` binds a live participant to the `capabilityId` that
|
|
12
|
+
announced it. Another capability's announce or leave for that participant is
|
|
13
|
+
refused with `unauthorized` until the lease lapses.
|
|
14
|
+
- `BranchCommands.submit` stamps the verified `capabilityId` on the durable
|
|
15
|
+
command payload, and `BranchProjection` carries it on every message, command,
|
|
16
|
+
and field, so a command is attributed to the capability that sent it rather
|
|
17
|
+
than to the `participantId` the client claimed.
|
|
18
|
+
- **Breaking.** `TestSync.layerWorkspaceAuth` is renamed
|
|
19
|
+
`TestSync.layerTrustAllAsOwner` so a composition that trusts every
|
|
20
|
+
connection as the owner says so where it is wired.
|
|
21
|
+
|
|
22
|
+
## [1.0.0-rc.0] - 2026-09-01
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Added workspace authentication: the `SyncAuth` RPC middleware and its
|
|
27
|
+
header-verifying implementation, the `WorkspaceShare` capability authority
|
|
28
|
+
over a `Redacted` keyring with `kid` rotation, and the per-request
|
|
29
|
+
`SyncPrincipal` whose default is anonymous.
|
|
30
|
+
- Added compaction recovery: `SyncProtocol.Resync`, the `compacted` error code
|
|
31
|
+
and `SyncError.resync`, and the client-side resync that moves a run's cursor
|
|
32
|
+
to the checkpoint and restarts instead of dying.
|
|
33
|
+
- Added `SubscribeOptions.onResync`, the seam a consumer restores checkpoint
|
|
34
|
+
state through. It runs before the cursor moves and must succeed, so a
|
|
35
|
+
follower that cannot fill the hole fails rather than skipping it silently.
|
|
36
|
+
- Added `SubscribeOptions.apply`, which advances a cursor only after the
|
|
37
|
+
consumer has applied the entry, so `RunCursor` can mean what its schema says.
|
|
38
|
+
- Added the bounded workspace fan-out: `SyncServer.Options.concurrency` and
|
|
39
|
+
`tailIntervalMs`, plus `RunCatalog.makePolling` for the durable run set a
|
|
40
|
+
workspace follower learns from.
|
|
41
|
+
- Added request limits `SyncProtocol.maxReadLimit` and `maxSubscribeCredit`,
|
|
42
|
+
the branch ledger bound `BranchCommands.defaultLedgerCapacity`, and the
|
|
43
|
+
roster bound `BranchPresence.defaultMaxParticipants`.
|
|
44
|
+
- Added golden wire vectors: the encoded form of every frame variant, a read
|
|
45
|
+
response, a command receipt, the capability header, and both authorities'
|
|
46
|
+
signatures are frozen as literals, so a renamed field or a changed signing
|
|
47
|
+
encoding fails a test rather than a deployed follower.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- **Breaking.** `SyncError.cause` is a bounded string rather than
|
|
52
|
+
`Schema.Unknown`. It is the declared error schema of every RPC, so it now
|
|
53
|
+
carries a rendering instead of the host object that failed; a journal
|
|
54
|
+
failure crosses as its stable journal code and never as the driver's own
|
|
55
|
+
message.
|
|
56
|
+
- **Breaking.** `BranchShare.makeHmac` and `layerHmac` take a `Redacted`
|
|
57
|
+
keyring rather than a single secret, `ShareClaims` carries the signing `kid`,
|
|
58
|
+
and the branch scheme signs a scheme label, now `v2`, so a branch signature
|
|
59
|
+
can no longer be replayed as a workspace signature under a shared secret and
|
|
60
|
+
the branch secret rotates the way the workspace secret already did: the
|
|
61
|
+
retired key stays in the keyring until its links expire. `BranchShare`
|
|
62
|
+
gained `layerConfig`, which reads `SMITHERS_SYNC_BRANCH_SECRET` and
|
|
63
|
+
`SMITHERS_SYNC_BRANCH_KEY_ID`. Outstanding branch capabilities do not verify
|
|
64
|
+
under this release.
|
|
65
|
+
- **Breaking.** The client's service tag is `SyncClient.SyncClient`, keyed
|
|
66
|
+
`@smthrs/sync/SyncClient`, matching every other service module.
|
|
67
|
+
`SyncClient.Sync` stays as a deprecated alias of the same tag for one
|
|
68
|
+
release.
|
|
69
|
+
- **Breaking.** `SubscribeOptions.apply` and `onResync` fail with the
|
|
70
|
+
consumer's own error types, and `subscribe` returns
|
|
71
|
+
`Stream<Entry, SyncError | SyncGapError | EApply | EResync>`. A consumer's
|
|
72
|
+
transaction failure no longer has to borrow a wire code such as `unknown` or
|
|
73
|
+
`compacted`, so it can no longer be mistaken for a transport or protocol
|
|
74
|
+
failure or pass `SyncError.is`.
|
|
75
|
+
- **Breaking.** `SyncProtocol.Progress` is
|
|
76
|
+
`{ delivered: WorkspaceCursor, applied: WorkspaceCursor }`. The
|
|
77
|
+
`DeliveredProgress` and `AppliedProgress` tagged wrappers are removed: they
|
|
78
|
+
never sat in a union, so their tags discriminated nothing. `Service.cursors`
|
|
79
|
+
is removed; read `progress.delivered` instead.
|
|
80
|
+
- **Breaking.** `BranchCommands.submission` returns
|
|
81
|
+
`Effect<CommandSubmission, SyncError>`. It decodes the fields it fills in, so
|
|
82
|
+
a command name the schema forbids is the same typed refusal `submit` returns
|
|
83
|
+
one call later rather than a throw at the caller.
|
|
84
|
+
- `BranchShare.mint` and `WorkspaceShare.mint` decode their request before
|
|
85
|
+
signing it. An empty `branchId` or `capabilityId` and a `ttlMs` of `0` or
|
|
86
|
+
`NaN` are refused with `invalid_request`; `ttlMs: 0` used to mint an
|
|
87
|
+
already-expired capability, and the others died where the declared type
|
|
88
|
+
promises a `SyncError`. `Branch.CreateBranch` decodes the id its `BranchIds`
|
|
89
|
+
port yields instead of branding it, so a host id source returning `""`
|
|
90
|
+
refuses rather than crashing the handler.
|
|
91
|
+
- Both authorities verify through one pipeline, `shareSigner.verifyClaims`, so
|
|
92
|
+
the signature check, the expiry boundary, and the read-only refusal cannot
|
|
93
|
+
drift apart between them. `ShareHardening` asserts each against both.
|
|
94
|
+
- **Breaking.** A subscription's `credit` must be between 1 and
|
|
95
|
+
`maxSubscribeCredit`, and a read's `limit` at most `maxReadLimit`. A cursor
|
|
96
|
+
set that names one run twice is refused with `invalid_request`.
|
|
97
|
+
- **Breaking.** `BranchRpcs`'s submit, announce, leave, and roster payloads are
|
|
98
|
+
the service schemas rather than copies of them, so `displayName: ""` is a
|
|
99
|
+
typed wire refusal instead of a defect raised inside the presence service.
|
|
100
|
+
- **Breaking.** The branch request schemas `SubmitRequest`, `Announcement`,
|
|
101
|
+
`RosterRequest`, and `LeaveRequest` live in `BranchProtocol` beside every
|
|
102
|
+
other branch wire shape, so `BranchRpcs` no longer imports the command ledger
|
|
103
|
+
or the presence registry. `BranchCommands.SubmitRequest`, the three
|
|
104
|
+
`BranchPresence` request schemas, and the `BranchRpcs` aliases
|
|
105
|
+
`SubmitPayload`, `AnnouncePayload`, `LeavePayload`, and `RosterPayload` are
|
|
106
|
+
removed. `BranchProtocol.CommandIdentity` is removed: the ledger reads a
|
|
107
|
+
command's id from the payload admission already decoded.
|
|
108
|
+
- **Breaking.** `makeLiveWith`, `layerWith`, and `BranchPresence.layer` fail
|
|
109
|
+
with `invalid_request` when an option is not a positive safe integer.
|
|
110
|
+
`BranchPresence.Service` gained `leaseMs`.
|
|
111
|
+
- A workspace subscription woken by an entry committed in this process reads
|
|
112
|
+
only the runs that entry named, rather than re-listing the catalog and
|
|
113
|
+
reading one page per covered run. The catalog-wide round still runs at open,
|
|
114
|
+
on an announcement, and once per `tailIntervalMs`, so removals, generation
|
|
115
|
+
changes, and other processes' writes are reconciled as before. The bounds
|
|
116
|
+
documentation now separates what a follower holds open, which the bounds
|
|
117
|
+
cap, from its per-run positions and per-round work, which scale with the
|
|
118
|
+
runs it covers.
|
|
119
|
+
- A branch admitted after a workspace subscription opened ends the stream at
|
|
120
|
+
that branch's own expiry: the expiry interrupt re-arms when reconciliation
|
|
121
|
+
lowers the deadline, and a page checks the deadline before it emits, so a
|
|
122
|
+
journal read that spans the expiry serves nothing past it and a stalled read
|
|
123
|
+
cannot hold the subscription open.
|
|
124
|
+
- An open subscription ends with `unauthorized` when the capability that
|
|
125
|
+
authorized it expires. Authorization was one-shot at open, so a share-link
|
|
126
|
+
holder could read past its own expiry by staying connected. That now covers a
|
|
127
|
+
branch a workspace subscription discovers AFTER it opened: reconciliation
|
|
128
|
+
carries the admitting capability's expiry into the subscription's deadline
|
|
129
|
+
rather than reducing the admission to a yes.
|
|
130
|
+
- Catch-up paging defers its cursor snapshot, so every page past the first asks
|
|
131
|
+
from where the previous one ended instead of repeating the previous request.
|
|
132
|
+
- Catch-up pages are validated the way live frames already were: scope,
|
|
133
|
+
per-run ordering, progress past the requested cursor, and cursor uniqueness,
|
|
134
|
+
refused as `protocol_violation` before any cursor moves.
|
|
135
|
+
- A workspace subscription reconciles its covered run set against
|
|
136
|
+
`RunCatalog.list` every round, so a run the catalog gains becomes visible
|
|
137
|
+
without an announcement and a run it stops naming is no longer queried.
|
|
138
|
+
- A workspace tail reads one bounded page per run per round, so a permanently
|
|
139
|
+
busy run wakes the next round instead of holding its fan-out slot.
|
|
140
|
+
- A read shares its `limit` across the runs it covers before any run takes a
|
|
141
|
+
second helping. Filling in run order let a producer that stayed one page
|
|
142
|
+
ahead take every slot of every page, so `done` never became true and a
|
|
143
|
+
bootstrapping follower never reached the runs behind it.
|
|
144
|
+
- The server deduplicates `RunCatalog.list` at the seam. A host catalog that
|
|
145
|
+
named one run twice had it read twice from the same served position, so a
|
|
146
|
+
read page carried its entries twice and a subscription opened two concurrent
|
|
147
|
+
tails of it emitting identical frames.
|
|
148
|
+
- `BranchCommands` keys its ledger and permit per branch, and `BranchPresence`
|
|
149
|
+
keys its roster per branch, so ids sharing a delimiter no longer collide and
|
|
150
|
+
one branch's replay no longer stalls another's writes.
|
|
151
|
+
- `BranchCommands.replay` ends its walk on an empty page, matching the guard
|
|
152
|
+
the workspace tail already carried.
|
|
153
|
+
- An admission conflict on a command whose receipt the bounded ledger has
|
|
154
|
+
already evicted reads the branch's durable history for it, so it is reported
|
|
155
|
+
as the duplicate it is instead of as a journal contradicting its own conflict
|
|
156
|
+
report.
|
|
157
|
+
- `Branch.MintShare` refuses a parent capability that expired mid-request
|
|
158
|
+
rather than minting a link that can never authorize.
|
|
159
|
+
- `BranchShare` and `WorkspaceShare` verify from a snapshot taken at entry, so
|
|
160
|
+
claims cannot widen while Web Crypto is in flight.
|
|
161
|
+
- Share claims are refused with `invalid_request` when they do not survive
|
|
162
|
+
UTF-8, and length prefixes count UTF-8 bytes rather than UTF-16 units.
|
|
163
|
+
- `encodedByteLength` is total: a value with no JSON text costs the bytes
|
|
164
|
+
`null` occupies, and a value JSON refuses measures as infinite so it trips
|
|
165
|
+
every ceiling rather than throwing.
|
|
166
|
+
- `branchOfRunId` returns `null` for the bare branch prefix instead of
|
|
167
|
+
branding an empty string as a `BranchId`.
|
|
168
|
+
- `RunCatalog.layerStatic` answers `list` with a fresh array, matching the
|
|
169
|
+
other implementations.
|
|
170
|
+
- `BranchShare.makeNoop` and `WorkspaceShare.makeNoop` fail `mint` instead of
|
|
171
|
+
dying, matching their declared type and the rest of the package.
|
|
172
|
+
- `SyncError.is` validates the code, the message, and the `resync` invariant
|
|
173
|
+
rather than trusting a bare tag, and answers rather than raising when a
|
|
174
|
+
field it reads throws.
|
|
175
|
+
- `SubscribeOptions.credit` and `SyncClient.make`'s `bootstrapLimit` are
|
|
176
|
+
refused locally with `invalid_request` when they exceed the wire maxima. A
|
|
177
|
+
value only the wire schema rejected surfaced as a transport failure and the
|
|
178
|
+
follow retried it under backoff forever.
|
|
179
|
+
- `SyncError.message` is bounded like `cause`, and a journal failure's rendered
|
|
180
|
+
code is bounded too, so no host string can be the largest thing an error
|
|
181
|
+
carries.
|
|
182
|
+
- A journal failure whose code this boundary also declares crosses as that
|
|
183
|
+
code: `journal_closed` as `closed`, `queue_overflow` as `backpressure`, and
|
|
184
|
+
`decode_failed` as itself, on the read path and the branch write path alike.
|
|
185
|
+
Collapsing every code but `compacted` into `unknown` threw away a
|
|
186
|
+
classification the wire already carried, so a follower could not tell a
|
|
187
|
+
shut-down journal from an unexplained fault. A code with no counterpart here
|
|
188
|
+
stays `unknown`, and the journal's own message stays refused.
|
|
189
|
+
- `apply` and `onResync` are handed values the committed position is
|
|
190
|
+
snapshotted from, so a consumer callback can no longer choose the cursor it
|
|
191
|
+
is being handed an entry for.
|
|
192
|
+
- `Branch.WatchRoster` compares rosters by an injective rendering and
|
|
193
|
+
deduplicates its initial emission, so a change the old delimiter-joined key
|
|
194
|
+
hid is emitted and an unchanged roster is not sent twice.
|
|
195
|
+
- `HeartbeatFrame` is documented as reserved: no server emits one, so a client
|
|
196
|
+
must not wait for it.
|
|
197
|
+
- Documentation is package-owned and generated: `docs/pages/api/sync.md` and
|
|
198
|
+
the protocol section of `docs/pages/concepts/sync.md` come from this
|
|
199
|
+
package's JSDoc and `docs/`.
|
|
200
|
+
|
|
201
|
+
## [0.1.0] - 2026-08-05
|
|
202
|
+
|
|
203
|
+
### Added
|
|
204
|
+
|
|
205
|
+
- Added the workspace read-path sync protocol with its server, client, and
|
|
206
|
+
run catalog.
|
|
207
|
+
- Added the shared branch protocol, presence, projection, commands, share, and
|
|
208
|
+
RPCs, with authorization enforced on the server.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Cory and the Smithers Flows contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,209 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @smthrs/sync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Release candidate scope, host requirements and compatibility review are defined in the [library support policy](https://github.com/smithersai/smithers/blob/main/RELEASE_SUPPORT.md).
|
|
4
|
+
|
|
5
|
+
This package declares `effect` as an exact
|
|
6
|
+
`4.0.0-rc.115` peer dependency. Keep the application on that version so
|
|
7
|
+
all Smithers packages share one Effect runtime.
|
|
8
|
+
|
|
9
|
+
**Documentation:** https://smithers-sync.smithers.sh
|
|
10
|
+
|
|
11
|
+
Browser-safe, read-only replication of canonical
|
|
12
|
+
[`@smthrs/journal`](https://journal.smithers.sh) entries. It defines the wire
|
|
13
|
+
protocol, the RPC group, the server, and the replay-then-follow client.
|
|
14
|
+
|
|
15
|
+
The read path, `SyncRpcs` served by `SyncServer` and followed by `SyncClient`,
|
|
16
|
+
never writes to a journal, so a follower cannot corrupt what it reads.
|
|
17
|
+
`BranchCommands` is the separate write surface: it appends admitted branch
|
|
18
|
+
commands to a branch's journal run after verifying a write-scoped
|
|
19
|
+
`BranchShare` capability.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
|
|
24
|
+
|
|
25
|
+
`effect` is a peer dependency at exactly `4.0.0-rc.115`. Two copies of `effect`
|
|
26
|
+
in one program are two sets of service tags, so a client built against one copy
|
|
27
|
+
cannot be provided to a program holding the other.
|
|
28
|
+
|
|
29
|
+
Node.js 26.4.0 or later, or a browser with Web Crypto. The package ships as
|
|
30
|
+
both ESM and CommonJS with TypeScript declarations.
|
|
31
|
+
|
|
32
|
+
## Follow a run
|
|
33
|
+
|
|
34
|
+
One `subscribe` call replays a run's durable history and then follows it live,
|
|
35
|
+
through a single stream:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import type { JournalEvent } from "@smthrs/journal"
|
|
39
|
+
import * as SyncClient from "@smthrs/sync/SyncClient"
|
|
40
|
+
import * as Effect from "effect/Effect"
|
|
41
|
+
import * as Layer from "effect/Layer"
|
|
42
|
+
import * as Stream from "effect/Stream"
|
|
43
|
+
import * as RpcClient from "effect/unstable/rpc/RpcClient"
|
|
44
|
+
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
|
|
45
|
+
|
|
46
|
+
const runId = "build-42" as JournalEvent.RunId
|
|
47
|
+
|
|
48
|
+
export const follow = Effect.gen(function*() {
|
|
49
|
+
const sync = yield* SyncClient.SyncClient
|
|
50
|
+
yield* sync.subscribe({ scope: { _tag: "Run", runId }, cursors: [] }).pipe(
|
|
51
|
+
Stream.runForEach((entry) => Effect.logInfo(`${entry.seq} ${entry.eventType}`))
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const clientLayer = SyncClient.layer.pipe(
|
|
56
|
+
Layer.provide(RpcClient.layerProtocolSocket()),
|
|
57
|
+
Layer.provide(RpcSerialization.layerJson)
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The socket that protocol runs over comes from your platform: a WebSocket in a
|
|
62
|
+
browser, `@effect/platform-node`'s socket layer in Node. Pass
|
|
63
|
+
`{ _tag: "Workspace" }` instead of a run scope to follow every run the server
|
|
64
|
+
lists and your credential covers.
|
|
65
|
+
|
|
66
|
+
The serving side is `SyncServer` over a journal and a run catalog.
|
|
67
|
+
[`@smthrs/gateway`](https://gateway.smithers.sh) already mounts it on
|
|
68
|
+
`POST /sync` and `/sync/ws`, so a follower against a Smithers gateway writes
|
|
69
|
+
only the code above.
|
|
70
|
+
|
|
71
|
+
`Read` pages durable entries, then the client subscribes from exclusive per-run
|
|
72
|
+
cursors. A non-contiguous journal sequence is valid; `SyncGapError` means the
|
|
73
|
+
server skipped beyond the interval covered by the client's cursor.
|
|
74
|
+
|
|
75
|
+
`sync.progress` reports two cursor sets: `delivered` holds delivery bookmarks,
|
|
76
|
+
and `applied` holds what successful `apply` or `onResync` callbacks
|
|
77
|
+
acknowledged. Those callbacks fail with your own error types, which the
|
|
78
|
+
subscription's error channel carries unchanged. Persist state and its cursor
|
|
79
|
+
together inside those callbacks. An applying subscriber never skips entries
|
|
80
|
+
merely because an earlier subscriber received them.
|
|
81
|
+
|
|
82
|
+
## Authorization
|
|
83
|
+
|
|
84
|
+
Authorization is fail-closed along two boundaries, and both are consulted on
|
|
85
|
+
every request. Branch runs authorize through the branch share capability
|
|
86
|
+
carried in each request (`BranchShare`); a share link grants exactly its
|
|
87
|
+
branch's run. Non-branch runs and workspace listings authorize through the
|
|
88
|
+
authenticated workspace principal (`SyncPrincipal`, default anonymous), which
|
|
89
|
+
`SyncAuth.layer` establishes over RPC by verifying the `WorkspaceShare`
|
|
90
|
+
capability presented in the `flows-sync-workspace` request header. A connection
|
|
91
|
+
with no valid credential is refused every non-branch read.
|
|
92
|
+
|
|
93
|
+
Both authorities take `Redacted` keyrings, lead their signed encoding with a
|
|
94
|
+
scheme label, and carry a rotation-ready `kid` inside the signed claims. An
|
|
95
|
+
open subscription ends with `unauthorized` when the credential that opened it
|
|
96
|
+
expires, because a stream authorized once at open is otherwise the one thing a
|
|
97
|
+
signed expiry cannot revoke. See
|
|
98
|
+
[Authorization](https://smithers-sync.smithers.sh/concepts/authorization/).
|
|
99
|
+
|
|
100
|
+
## Branch collaboration
|
|
101
|
+
|
|
102
|
+
A branch is one shared live document whose durable state is exactly one journal
|
|
103
|
+
run (`BranchProtocol.branchRunId`), so multiplayer reuses the canonical `seq`,
|
|
104
|
+
cursors, gap detection, and resumable follow rather than introducing a second
|
|
105
|
+
source of truth. Presence is a lease and is never journalled; commands are
|
|
106
|
+
admitted through a client-minted idempotency key; every branch operation after
|
|
107
|
+
`Branch.CreateBranch` authorizes through a signed, expiring, branch-scoped
|
|
108
|
+
share capability.
|
|
109
|
+
|
|
110
|
+
The native gateway mounts the existing branch presence RPCs at `POST /branch`
|
|
111
|
+
when its authenticated runtime bridge is configured. The Go install resolves
|
|
112
|
+
membership, branch and socket session before forwarding; this host adapter uses
|
|
113
|
+
the same `BranchPresence` lease roster. Branch commands and share-link bootstrap
|
|
114
|
+
remain unserved on that mount. See
|
|
115
|
+
[Branch collaboration](https://smithers-sync.smithers.sh/concepts/branches/).
|
|
116
|
+
|
|
117
|
+
## Bounds
|
|
118
|
+
|
|
119
|
+
Every in-flight surface is bounded: entries per page, frames per subscription,
|
|
120
|
+
journal reads open per workspace subscription, bytes per frame. What one
|
|
121
|
+
follower holds open is a function of the configured bound, not of how far
|
|
122
|
+
behind it has fallen. Bookkeeping and periodic work still scale with the
|
|
123
|
+
workspace: a workspace subscription keeps a position for every run it has
|
|
124
|
+
covered, and its catalog-wide round, once per `tailIntervalMs` or on an
|
|
125
|
+
announcement, reads one page per listed run under the concurrency bound. A
|
|
126
|
+
wake for a local append reads only the run it named. Every numeric option is
|
|
127
|
+
validated where it enters: a value that is not a positive safe integer fails
|
|
128
|
+
the constructor with `invalid_request` rather than quietly disabling the
|
|
129
|
+
comparison it configures.
|
|
130
|
+
The table of bounds and defaults is in the
|
|
131
|
+
[API reference](https://smithers-sync.smithers.sh/reference/api/#bounds).
|
|
132
|
+
|
|
133
|
+
Both change feeds slide rather than block, and neither is a source of truth.
|
|
134
|
+
`RunCatalog.list` and `BranchPresence.list` are the authoritative state, and
|
|
135
|
+
every reader re-lists on a cadence of its own, so a dropped notification costs
|
|
136
|
+
latency and never state.
|
|
137
|
+
|
|
138
|
+
## Rewinds and cursor generations
|
|
139
|
+
|
|
140
|
+
`RunCursor` and `EntriesFrame` carry a `generation`. Server responses always
|
|
141
|
+
include it, zero included, and a client refuses one that omits it; a persisted
|
|
142
|
+
request cursor may omit it for generation zero. SQL journals persist it
|
|
143
|
+
independently of sequence numbers. Rewind increments it atomically with
|
|
144
|
+
truncation, so a follower at 100 cannot silently discard a replacement entry at
|
|
145
|
+
51 after rewinding to 50.
|
|
146
|
+
|
|
147
|
+
Both reads and subscriptions fail with `SyncError.code = "lineage_changed"`
|
|
148
|
+
when generations differ. Live run and workspace subscriptions also check while
|
|
149
|
+
idle, within `tailIntervalMs`. This failure is terminal: rebuild the projection
|
|
150
|
+
from the current retained history through the archive boundary, create a fresh
|
|
151
|
+
sync client, and resume from the server error's
|
|
152
|
+
`rewind: { runId, generation, afterSeq }`. For `afterSeq: -1`, omit the cursor
|
|
153
|
+
to replay the entire current history. Keep the generation with every persisted
|
|
154
|
+
cursor. Compaction recovery remains separate.
|
|
155
|
+
|
|
156
|
+
Append-only journal adapters may omit `Journal.Service.generation`. Any adapter
|
|
157
|
+
that truncates or replaces history must implement it and advance the generation
|
|
158
|
+
in its truncation transaction. Upgrade sync clients and servers together;
|
|
159
|
+
older clients cannot detect generation changes. See
|
|
160
|
+
[Rewind generations](https://smithers-sync.smithers.sh/reference/api/#rewind-generations).
|
|
161
|
+
|
|
162
|
+
## Public API
|
|
163
|
+
|
|
164
|
+
The root exports these namespaces, also available from matching
|
|
165
|
+
`@smthrs/sync/*` subpaths. The
|
|
166
|
+
[API reference](https://smithers-sync.smithers.sh/reference/api/) documents
|
|
167
|
+
each one.
|
|
168
|
+
|
|
169
|
+
| Namespace | What it owns |
|
|
170
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
171
|
+
| `SyncError` | `ErrorCode`, `SyncError` with the structural guard `SyncError.is`, and terminal `SyncGapError`. |
|
|
172
|
+
| `SyncProtocol` | Scopes, cursors, `Resync`, request and response schemas, frames, and the size and request limits. |
|
|
173
|
+
| `SyncRpcs` | The read-path RPC group (`Sync.Read`, `Sync.Subscribe`, `Sync.Snapshot`) and the `SyncAuth` middleware service. |
|
|
174
|
+
| `SyncAuth` | Implementations of that middleware: `layer`, `layerClient`, and the header codec. |
|
|
175
|
+
| `SyncPrincipal` | The per-request identity reference, its constructors, and `layerWorkspace` for in-process owners. |
|
|
176
|
+
| `WorkspaceShare` | The workspace capability authority over a `Redacted` keyring with `kid` rotation. |
|
|
177
|
+
| `RunCatalog` | The workspace run set a subscription reconciles against, in-memory, polling, and static forms. |
|
|
178
|
+
| `SyncServer` | The read-path implementation, its policy, and `layerHandlers`. |
|
|
179
|
+
| `SyncClient` | The browser-safe replay-then-follow client and its subscription options. |
|
|
180
|
+
| `BranchProtocol` | The branch vocabulary: ids, claims, capabilities, submissions, receipts, and the run-id mapping. |
|
|
181
|
+
| `BranchShare` | The branch capability authority over a `Redacted` keyring with `kid` rotation. |
|
|
182
|
+
| `BranchIds` | The port branch and capability ids are minted through. |
|
|
183
|
+
| `BranchCommands` | Idempotent command admission onto a branch's journal run. |
|
|
184
|
+
| `BranchPresence` | The ephemeral, lease-expiring roster. |
|
|
185
|
+
| `BranchProjection` | The fold from branch commands to a document view. |
|
|
186
|
+
| `BranchRpcs` | The branch collaboration wire group. |
|
|
187
|
+
| `BranchServer` | The handler layer that projects the branch services onto that group. |
|
|
188
|
+
|
|
189
|
+
Public test subpaths are `@smthrs/sync/test/TestSocket` (`FrameFilter`,
|
|
190
|
+
`TestFaults`, `Pair`, `makePair`) and `@smthrs/sync/test/TestSync`
|
|
191
|
+
(`layerTest`, `layerTrustAllAsOwner`, `layerNoop`, `connect`).
|
|
192
|
+
|
|
193
|
+
## Documentation
|
|
194
|
+
|
|
195
|
+
- [Installation](https://smithers-sync.smithers.sh/installation/)
|
|
196
|
+
- [Quickstart](https://smithers-sync.smithers.sh/quickstart/)
|
|
197
|
+
- [Scopes and cursors](https://smithers-sync.smithers.sh/concepts/scopes-and-cursors/)
|
|
198
|
+
- [Replay then follow](https://smithers-sync.smithers.sh/concepts/replay-then-follow/)
|
|
199
|
+
- [Authorization](https://smithers-sync.smithers.sh/concepts/authorization/)
|
|
200
|
+
- [Follow a run](https://smithers-sync.smithers.sh/guides/follow-a-run/)
|
|
201
|
+
- [Serve the read path](https://smithers-sync.smithers.sh/guides/serve-the-read-path/)
|
|
202
|
+
- [Wire protocol](https://smithers-sync.smithers.sh/protocol/)
|
|
203
|
+
- [API reference](https://smithers-sync.smithers.sh/reference/api/)
|
|
204
|
+
- [Troubleshooting](https://smithers-sync.smithers.sh/troubleshooting/)
|
|
205
|
+
- [Optional long soak](docs/guides/run-a-long-soak.md)
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent admission of collaborative commands onto a branch document.
|
|
3
|
+
*
|
|
4
|
+
* Three different things look identical from the server's side — an optimistic
|
|
5
|
+
* client re-sending after a timeout, a reconnecting client flushing its
|
|
6
|
+
* outbox, and two people pressing the same button at once — and all three must
|
|
7
|
+
* produce exactly one durable command. The client-minted `commandId` is the
|
|
8
|
+
* idempotency key for all three.
|
|
9
|
+
*
|
|
10
|
+
* The exactly-once constraint is durable, not process-local: every append
|
|
11
|
+
* carries the producer identity `(branch run, commandSourceId(commandId),
|
|
12
|
+
* commandSourceSeq)`, which the journal enforces inside its own write
|
|
13
|
+
* transaction. Two independently constructed servers that race the same
|
|
14
|
+
* command therefore collide in the journal — one appends, the other receives
|
|
15
|
+
* a duplicate receipt or an idempotency conflict and resolves the canonical
|
|
16
|
+
* sequence by replaying the branch (audit finding F-14). The in-memory
|
|
17
|
+
* ledger, permit, and replay cursor are a fast path only: they answer known
|
|
18
|
+
* duplicates without a journal write and keep a restarted server from
|
|
19
|
+
* re-executing history, but correctness never depends on them.
|
|
20
|
+
*
|
|
21
|
+
* @since 0.1.0
|
|
22
|
+
*/
|
|
23
|
+
import { Journal } from "@smthrs/journal";
|
|
24
|
+
import * as Context from "effect/Context";
|
|
25
|
+
import * as Effect from "effect/Effect";
|
|
26
|
+
import * as Layer from "effect/Layer";
|
|
27
|
+
import type * as Scope from "effect/Scope";
|
|
28
|
+
import { type BranchId, type CommandId, CommandReceipt, CommandSubmission, SubmitRequest } from "./BranchProtocol.ts";
|
|
29
|
+
import * as BranchShare from "./BranchShare.ts";
|
|
30
|
+
import { SyncError } from "./SyncError.ts";
|
|
31
|
+
/**
|
|
32
|
+
* Branch command admission operations.
|
|
33
|
+
*
|
|
34
|
+
* @category models
|
|
35
|
+
* @since 0.1.0
|
|
36
|
+
*/
|
|
37
|
+
export interface Service {
|
|
38
|
+
readonly submit: (request: SubmitRequest) => Effect.Effect<CommandReceipt, SyncError>;
|
|
39
|
+
}
|
|
40
|
+
declare const BranchCommands_base: Context.ServiceClass<BranchCommands, "@smthrs/sync/BranchCommands", Service>;
|
|
41
|
+
/**
|
|
42
|
+
* The branch command ledger.
|
|
43
|
+
*
|
|
44
|
+
* @category services
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
export declare class BranchCommands extends BranchCommands_base {
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Constructs a command ledger that admits nothing.
|
|
51
|
+
*
|
|
52
|
+
* @category constructors
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
*/
|
|
55
|
+
export declare const makeNoop: (overrides?: Partial<Service>) => Service;
|
|
56
|
+
/**
|
|
57
|
+
* Provides a command ledger that admits nothing.
|
|
58
|
+
*
|
|
59
|
+
* @category layers
|
|
60
|
+
* @since 0.1.0
|
|
61
|
+
*/
|
|
62
|
+
export declare const layerNoop: Layer.Layer<BranchCommands>;
|
|
63
|
+
/**
|
|
64
|
+
* Receipts one branch keeps in memory before the oldest are evicted.
|
|
65
|
+
*
|
|
66
|
+
* The ledger is a fast path: it answers a known duplicate without a journal
|
|
67
|
+
* write, and losing an entry costs a round trip, never correctness, because
|
|
68
|
+
* the journal's own producer identity is the durable exactly-once constraint
|
|
69
|
+
* and returns a `Duplicate` receipt for a command already admitted. Retaining
|
|
70
|
+
* one receipt per command forever meant a process serving a hundred branches
|
|
71
|
+
* of a hundred thousand commands held ten million of them, so the fast path
|
|
72
|
+
* is bounded and the durable path is what makes it safe to bound.
|
|
73
|
+
*
|
|
74
|
+
* @category constants
|
|
75
|
+
* @since 1.0.0-rc.0
|
|
76
|
+
*/
|
|
77
|
+
export declare const defaultLedgerCapacity = 4096;
|
|
78
|
+
/**
|
|
79
|
+
* Inclusive UTF-8 JSON submission ceiling. The separate sync entry ceiling
|
|
80
|
+
* leaves room for the durable envelope around every admitted command.
|
|
81
|
+
*
|
|
82
|
+
* @category constants
|
|
83
|
+
* @since 1.0.0-rc.0
|
|
84
|
+
*/
|
|
85
|
+
export declare const defaultMaxCommandBytes: number;
|
|
86
|
+
/**
|
|
87
|
+
* Milliseconds a branch may have no active submissions before its in-memory state is
|
|
88
|
+
* dropped.
|
|
89
|
+
*
|
|
90
|
+
* The receipt ledger is bounded per branch; this bounds the branches. A
|
|
91
|
+
* dropped branch re-hydrates on its next submission exactly as it does after
|
|
92
|
+
* a restart, so eviction costs a journal read, never correctness. The default
|
|
93
|
+
* is the longest lifetime a branch capability may carry, so a branch is only
|
|
94
|
+
* dropped once every capability minted at its last touch can have expired.
|
|
95
|
+
*
|
|
96
|
+
* @category constants
|
|
97
|
+
* @since 1.0.0-rc.0
|
|
98
|
+
*/
|
|
99
|
+
export declare const defaultBranchIdleMs: number;
|
|
100
|
+
/**
|
|
101
|
+
* Entries one branch's first-touch hydration reads before it stops.
|
|
102
|
+
*
|
|
103
|
+
* Hydration is the only full history read that ever sat on the WRITE path: the
|
|
104
|
+
* first submission a process makes to a branch paged that branch's whole
|
|
105
|
+
* durable log, 256 entries at a time, while holding the branch's admission
|
|
106
|
+
* permit, before its own append was attempted. A hundred-thousand-entry branch
|
|
107
|
+
* therefore charged its next writer four hundred journal reads for a fast path
|
|
108
|
+
* that keeps four thousand receipts.
|
|
109
|
+
*
|
|
110
|
+
* The budget defaults to {@link defaultLedgerCapacity} because hydration
|
|
111
|
+
* cannot seed more receipts than the ledger holds: everything a longer walk
|
|
112
|
+
* records displaces something the same walk already recorded. What the extra
|
|
113
|
+
* reads buy is a FRESHER selection — the tail of history rather than its head
|
|
114
|
+
* — and the price is an unbounded wait on somebody's write. This trades that
|
|
115
|
+
* back: the walk stops, the cursor stays where it stopped, and a later replay
|
|
116
|
+
* continues forward from there.
|
|
117
|
+
*
|
|
118
|
+
* A branch longer than the budget starts with a partly cold fast path and
|
|
119
|
+
* nothing else. Correctness never rested on the ledger: the journal's producer
|
|
120
|
+
* identity is the durable exactly-once constraint, an exact retry receives a
|
|
121
|
+
* `Duplicate` receipt carrying the canonical sequence, and an
|
|
122
|
+
* `idempotency_conflict` is resolved by a read of the whole history that the
|
|
123
|
+
* ledger's own bound already made necessary.
|
|
124
|
+
*
|
|
125
|
+
* @category constants
|
|
126
|
+
* @since 1.0.0-rc.0
|
|
127
|
+
*/
|
|
128
|
+
export declare const defaultHydrationLimit = 4096;
|
|
129
|
+
/**
|
|
130
|
+
* Admission policy.
|
|
131
|
+
*
|
|
132
|
+
* The default caps one encoded command submission at 1 MiB, one branch's
|
|
133
|
+
* in-memory receipt ledger at {@link defaultLedgerCapacity} commands, and one
|
|
134
|
+
* branch's first-touch hydration at {@link defaultHydrationLimit} entries.
|
|
135
|
+
*
|
|
136
|
+
* @category models
|
|
137
|
+
* @since 0.1.0
|
|
138
|
+
*/
|
|
139
|
+
export interface Options {
|
|
140
|
+
/**
|
|
141
|
+
* Largest encoded journal entry followers accept. Defaults to
|
|
142
|
+
* {@link SyncProtocol.defaultMaxFrameBytes}. Configure the server and client
|
|
143
|
+
* with the same or a larger ceiling. Admission reserves the full durable
|
|
144
|
+
* envelope, including generated sequence and timestamp fields, before writing.
|
|
145
|
+
*/
|
|
146
|
+
readonly maxFrameBytes?: number | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Largest encoded command submission admitted, in bytes. Defaults to
|
|
149
|
+
* {@link defaultMaxCommandBytes}.
|
|
150
|
+
*/
|
|
151
|
+
readonly maxCommandBytes?: number | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Receipts one branch keeps in memory before the oldest are evicted.
|
|
154
|
+
* Defaults to {@link defaultLedgerCapacity}.
|
|
155
|
+
*/
|
|
156
|
+
readonly ledgerCapacity?: number | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* Entries one branch's first-touch hydration reads before it stops.
|
|
159
|
+
* Defaults to {@link defaultHydrationLimit}.
|
|
160
|
+
*/
|
|
161
|
+
readonly hydrationLimit?: number | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* Milliseconds a branch may have no active submissions before its in-memory state is
|
|
164
|
+
* dropped. Defaults to {@link defaultBranchIdleMs}.
|
|
165
|
+
*/
|
|
166
|
+
readonly branchIdleMs?: number | undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Constructs the journal-backed branch command ledger under an explicit
|
|
170
|
+
* policy. The caller scope owns cached states and their idle-expiration timers.
|
|
171
|
+
*
|
|
172
|
+
* A submission whose encoded form exceeds the command ceiling is refused with
|
|
173
|
+
* `frame_too_large` before anything is appended, so one oversized `args`
|
|
174
|
+
* cannot enter the branch journal and poison every follower that replays it.
|
|
175
|
+
*
|
|
176
|
+
* First-touch hydration reads at most `hydrationLimit` entries, so the length
|
|
177
|
+
* of a branch's history is not charged to the next writer's latency; see
|
|
178
|
+
* {@link defaultHydrationLimit} for why the fast path may be seeded partially
|
|
179
|
+
* without any effect on what the ledger admits.
|
|
180
|
+
*
|
|
181
|
+
* Every option is validated as a positive safe integer at construction: the
|
|
182
|
+
* TypeScript type says `number`, and `NaN` silently disabled the ceiling it
|
|
183
|
+
* was compared against.
|
|
184
|
+
*
|
|
185
|
+
* @category constructors
|
|
186
|
+
* @since 0.1.0
|
|
187
|
+
*/
|
|
188
|
+
export declare const makeLiveWith: (options?: Options) => Effect.Effect<Service, SyncError, Journal.Journal | BranchShare.BranchShare | Scope.Scope>;
|
|
189
|
+
/**
|
|
190
|
+
* Constructs the journal-backed branch command ledger with default policy.
|
|
191
|
+
* The caller scope owns cached states and their idle-expiration timers.
|
|
192
|
+
*
|
|
193
|
+
* @category constructors
|
|
194
|
+
* @since 0.1.0
|
|
195
|
+
*/
|
|
196
|
+
export declare const makeLive: Effect.Effect<Service, never, Journal.Journal | BranchShare.BranchShare | Scope.Scope>;
|
|
197
|
+
/**
|
|
198
|
+
* Provides the journal-backed branch command ledger.
|
|
199
|
+
*
|
|
200
|
+
* @category layers
|
|
201
|
+
* @since 0.1.0
|
|
202
|
+
*/
|
|
203
|
+
export declare const layer: Layer.Layer<BranchCommands, never, Journal.Journal | BranchShare.BranchShare>;
|
|
204
|
+
/**
|
|
205
|
+
* Provides the journal-backed branch command ledger under an explicit policy.
|
|
206
|
+
* Fails with `invalid_request` when an option is not a positive safe integer.
|
|
207
|
+
*
|
|
208
|
+
* @category layers
|
|
209
|
+
* @since 0.1.0
|
|
210
|
+
*/
|
|
211
|
+
export declare const layerWith: (options: Options) => Layer.Layer<BranchCommands, SyncError, Journal.Journal | BranchShare.BranchShare>;
|
|
212
|
+
/**
|
|
213
|
+
* Builds a submission, filling in the fields a plain command never sets, and
|
|
214
|
+
* refusing a field the schema forbids with a `SyncError` rather than a defect.
|
|
215
|
+
*
|
|
216
|
+
* @category constructors
|
|
217
|
+
* @since 0.1.0
|
|
218
|
+
*/
|
|
219
|
+
export declare const submission: (fields: {
|
|
220
|
+
readonly branchId: BranchId;
|
|
221
|
+
readonly commandId: CommandId;
|
|
222
|
+
readonly participantId: CommandSubmission["participantId"];
|
|
223
|
+
readonly name: string;
|
|
224
|
+
readonly args?: string;
|
|
225
|
+
readonly target?: string;
|
|
226
|
+
}) => Effect.Effect<CommandSubmission, SyncError>;
|
|
227
|
+
export {};
|
|
228
|
+
//# sourceMappingURL=BranchCommands.d.ts.map
|