@ultimat3/realtime 7.0.0 → 9.0.0
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/CLAUDE.md +31 -0
- package/README.md +37 -15
- package/package.json +8 -4
- package/src/index.ts +6 -227
- package/src/offline-queue.ts +20 -0
- package/src/pgoutput.ts +2 -1
- package/src/server.ts +234 -0
package/CLAUDE.md
CHANGED
|
@@ -13,6 +13,35 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
13
13
|
|
|
14
14
|
## Rules
|
|
15
15
|
|
|
16
|
+
- **Two entries, and a name lives in exactly ONE of them (2026-08-22, BREAKING).** `.` is the
|
|
17
|
+
client half — `hooks`, `client*`, `identity-map`, `live-rows`, `apply-patches`, `offline-queue`,
|
|
18
|
+
`rebase`, `local-store`, `sync-protocol`, `json`, `cursor`, `errors`, and the client's half of
|
|
19
|
+
`thundering-herd`. `./server` (`src/server.ts`) is everything that touches `nats`, Postgres, the
|
|
20
|
+
sync node, the channel hub, the live-query registry or the fanout. The reason is measured, not
|
|
21
|
+
aesthetic: `nats` `require()`s `stream/web`, so one barrel carrying `openNatsClient` beside
|
|
22
|
+
`useLive` failed `bun build --target=browser` with
|
|
23
|
+
`Browser build cannot require() Node.js builtin: "stream/web"` — the island `wiki/Realtime.md`
|
|
24
|
+
promises could not be built at all. Two build errors hold it: `packages/cli/src/realtime-browser-barrel.test.ts`
|
|
25
|
+
bundles a `useLive`-only entry for the browser, and `barrel-split.test.ts` refuses a name
|
|
26
|
+
exported from both (values through the namespace objects, types off the source text, because a
|
|
27
|
+
type-only export leaves no runtime entry). `errors.ts` is deliberately whole on `.`: every code
|
|
28
|
+
reaches the wire through `toWireError`, so a client must be able to name any of them, and the
|
|
29
|
+
module is already in the client graph via `sync-protocol`.
|
|
30
|
+
- **`sideEffects` is the ARRAY `["./src/errors.ts"]`, never `false` and never absent.** Absent was
|
|
31
|
+
what made the failure above unrecoverable — with no field a bundler must assume every module has
|
|
32
|
+
effects, so nothing was tree-shaken and `nats` came along with `useLive`. Measured, not guessed:
|
|
33
|
+
`bun run side-effects --explain --json` prints what the tree actually does at import time.
|
|
34
|
+
`false` would drop `registerErrorCodes()` and every `REALTIME_ERROR_TITLES` entry with it.
|
|
35
|
+
**The array alone would have fixed the build** and is not why the split exists: tree-shaking is
|
|
36
|
+
a bundler's discretion, `export * from` or a namespace import defeats it, and "the client entry
|
|
37
|
+
cannot reach the bus" is a contract rather than an optimisation.
|
|
38
|
+
- **`@ultimat3/realtime/server` needs its own `paths` entry in `tsconfig.base.json`**, beside
|
|
39
|
+
`@ultimat3/admin/dev`'s. `@ultimat3/*` maps `realtime/server` to `packages/realtime/server/src`,
|
|
40
|
+
which does not exist, and the root program has no `node_modules/@ultimat3` symlink to fall back
|
|
41
|
+
through — so `scripts/**` (which the root `tsc -b` compiles) reports `TS2307` without it. The
|
|
42
|
+
workspace packages resolve through their own `node_modules` and never needed it, which is what
|
|
43
|
+
makes the failure look local to `scripts/`.
|
|
44
|
+
|
|
16
45
|
- Policy is evaluated **once per subscriber**, never once per query. `live-query.test.ts` proves it
|
|
17
46
|
for a hand-written definition and `live-definition.test.ts` proves it for a real declared
|
|
18
47
|
`query({ live: true })` — the second one matters, because a rule that only holds for test fakes
|
|
@@ -640,6 +669,8 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
640
669
|
|
|
641
670
|
| File | Owns |
|
|
642
671
|
|---|---|
|
|
672
|
+
| `index.ts` | the `.` barrel: the client half, and the only thing a browser island may import |
|
|
673
|
+
| `server.ts` | the `./server` barrel: the bus, the WAL path, the node. Disjoint from `index.ts` by test |
|
|
643
674
|
| `sync-protocol.ts` | the wire: 10 frame kinds, `encode`/`decode`, `PROTOCOL_VERSION` |
|
|
644
675
|
| `channel.ts` / `presence.ts` / `socket.ts` | tier 1 |
|
|
645
676
|
| `live-query.ts` / `live-definition.ts` / `changefeed.ts` / `changefeed-env.ts` / `replicator.ts` / `pg-advisory-lock.ts` / `fanout.ts` / `transport-env.ts` / `matcher-bridge.ts` | tier 2 |
|
package/README.md
CHANGED
|
@@ -44,23 +44,45 @@ frame handler is unchanged between rungs.
|
|
|
44
44
|
|
|
45
45
|
`local` must be pure — no I/O, no `Date.now()`, no `Math.random()` — because rebase replays it.
|
|
46
46
|
|
|
47
|
+
## Two entries, and which one an island may import
|
|
48
|
+
|
|
49
|
+
`As of 2026-08`, `@ultimat3/realtime` is the **client** half — the hooks, the identity map, the offline queue, the
|
|
50
|
+
wire and the reconnect vocabulary. `@ultimat3/realtime/server` is the bus, the Postgres replication
|
|
51
|
+
path and the sync node. A name lives in exactly one of them; the `Entry` column below says which.
|
|
52
|
+
|
|
53
|
+
The split is not cosmetic. `nats` `require()`s `stream/web`, so one barrel carrying `openNatsClient`
|
|
54
|
+
beside `useLive` made the browser island this package promises **unbuildable** —
|
|
55
|
+
`Browser build cannot require() Node.js builtin: "stream/web"`. `packages/cli/src/realtime-browser-barrel.test.ts`
|
|
56
|
+
bundles an entry importing only `useLive` for `target: 'browser'` and fails the build if either
|
|
57
|
+
half reaches the other; `barrel-split.test.ts` fails if one name is exported from both.
|
|
58
|
+
|
|
59
|
+
Migrating from 7.x: an import of a **server** name changes its specifier and nothing else.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
- import { ChannelHub, createSyncNode, LiveQueryRegistry } from '@ultimat3/realtime';
|
|
63
|
+
+ import { ChannelHub, createSyncNode, LiveQueryRegistry } from '@ultimat3/realtime/server';
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Client names — `useLive`, `liveHookFor`, `LiveClient`, `OfflineQueue`, `RebaseLog`, `IdentityMap`,
|
|
67
|
+
`encode`/`decode`, every `X_*` error class — are unchanged.
|
|
68
|
+
|
|
47
69
|
## Public API
|
|
48
70
|
|
|
49
|
-
| Concern | Export |
|
|
50
|
-
|
|
51
|
-
| tier 1 | `topic`, `ChannelHub`, `PresenceRegistry`, `SyncSocket`, `SocketRegistry` |
|
|
52
|
-
| tier 2 | `LiveQueryRegistry`, `InMemoryChangeFeed`, `PgLogicalReplicationFeed`, `selectChangeFeed`, `createReplicator`, `PgAdvisoryLock`, `matcherFor` |
|
|
53
|
-
| replication | `parsePgUrl`, `bunPgStream`, `PgOutputDecoder`, `entityRow`, `changeLsn`, `commitPositionOf` |
|
|
54
|
-
| fanout | `Transport`, `InProcessTransport`, `NatsTransport`, `selectTransport`, `subjectMatches` |
|
|
55
|
-
| the bus, behind `NatsTransport` | the port — `NatsClient`, `NatsMessage`, `NatsSubscription`, `NatsConnect`, `NatsTarget`, `parseNatsUrl` — plus `openNatsClient` (the `nats` adapter), `NatsKvSet`, `ensureKvBucket`, `kvGet`/`kvLast`/`kvWrite`, `assertBucket`, `encodeToken`/`decodeToken`, and `FakeNatsBroker`/`fakeNatsConnect` for tests |
|
|
56
|
-
| reconnect | `LiveCursor`, `resumeFrom`, `shouldResnapshot`, `defaultReconnectBudget`, `
|
|
57
|
-
| the client store | `IdentityMap` — one row value per `(entity, id)` — plus `RowWindows`, `rowKey`, `privateScope`, `applyPatches`/`orderAfterPatches` |
|
|
58
|
-
| tier 3 | `MemoryLocalStore`, `createOpfsLocalStore`, `OfflineQueue`, `RebaseLog`, `reconcile`, `custom` |
|
|
59
|
-
| wire | `PROTOCOL_VERSION`, `encode`, `decode`, `Frame` |
|
|
60
|
-
| halves | `LiveClient`
|
|
61
|
-
| a socket's identity | `SyncAuthenticator`, `SyncGrant`, `GrantBook`, `sweepGrants`, `DEFAULT_REAUTH_INTERVAL_MS` |
|
|
62
|
-
| hooks | `setLiveClient`, `useLive`, `useConnection`, `useMutation`, `useMutationQueue` |
|
|
63
|
-
| the typed projection | `liveHookFor` — one query bound to one named hook |
|
|
71
|
+
| Concern | Entry | Export |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| tier 1 | `./server` | `topic`, `ChannelHub`, `PresenceRegistry`, `SyncSocket`, `SocketRegistry` |
|
|
74
|
+
| tier 2 | `./server` | `LiveQueryRegistry`, `InMemoryChangeFeed`, `PgLogicalReplicationFeed`, `selectChangeFeed`, `createReplicator`, `PgAdvisoryLock`, `matcherFor` |
|
|
75
|
+
| replication | `./server` | `parsePgUrl`, `bunPgStream`, `PgOutputDecoder`, `entityRow`, `changeLsn`, `commitPositionOf` |
|
|
76
|
+
| fanout | `./server` | `Transport`, `InProcessTransport`, `NatsTransport`, `selectTransport`, `subjectMatches` |
|
|
77
|
+
| the bus, behind `NatsTransport` | `./server` | the port — `NatsClient`, `NatsMessage`, `NatsSubscription`, `NatsConnect`, `NatsTarget`, `parseNatsUrl` — plus `openNatsClient` (the `nats` adapter), `NatsKvSet`, `ensureKvBucket`, `kvGet`/`kvLast`/`kvWrite`, `assertBucket`, `encodeToken`/`decodeToken`, and `FakeNatsBroker`/`fakeNatsConnect` for tests |
|
|
78
|
+
| reconnect | both | `LiveCursor`, `resumeFrom`, `shouldResnapshot`, `defaultReconnectBudget`, `backoffDelay`, `Scheduler`, `timeoutScheduler` on `.`; `RingChangeBuffer`, `drainPlan`, `AcceptBudget`, `reconnectFrame` on `./server` — the node's half of the reconnect is the node's |
|
|
79
|
+
| the client store | `.` | `IdentityMap` — one row value per `(entity, id)` — plus `RowWindows`, `rowKey`, `privateScope`, `applyPatches`/`orderAfterPatches` |
|
|
80
|
+
| tier 3 | `.` | `MemoryLocalStore`, `createOpfsLocalStore`, `OfflineQueue`, `RebaseLog`, `reconcile`, `custom` |
|
|
81
|
+
| wire | `.` | `PROTOCOL_VERSION`, `encode`, `decode`, `Frame` |
|
|
82
|
+
| halves | both | `LiveClient` on `.`; `createSyncNode` / `listenSyncNode` (`sync` role) on `./server` |
|
|
83
|
+
| a socket's identity | `./server` | `SyncAuthenticator`, `SyncGrant`, `GrantBook`, `sweepGrants`, `DEFAULT_REAUTH_INTERVAL_MS` |
|
|
84
|
+
| hooks | `.` | `setLiveClient`, `useLive`, `useConnection`, `useMutation`, `useMutationQueue` |
|
|
85
|
+
| the typed projection | `.` | `liveHookFor` — one query bound to one named hook |
|
|
64
86
|
|
|
65
87
|
## The four hooks
|
|
66
88
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/realtime",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Three-tier realtime: channels, live queries, local-first sync — one protocol, one mutator shape",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"./src/errors.ts"
|
|
9
|
+
],
|
|
7
10
|
"repository": {
|
|
8
11
|
"type": "git",
|
|
9
12
|
"url": "git+https://github.com/developerz-ai/ultimate.git",
|
|
@@ -14,7 +17,8 @@
|
|
|
14
17
|
"provenance": true
|
|
15
18
|
},
|
|
16
19
|
"exports": {
|
|
17
|
-
".": "./src/index.ts"
|
|
20
|
+
".": "./src/index.ts",
|
|
21
|
+
"./server": "./src/server.ts"
|
|
18
22
|
},
|
|
19
23
|
"files": [
|
|
20
24
|
"src",
|
|
@@ -32,8 +36,8 @@
|
|
|
32
36
|
"test": "bun test"
|
|
33
37
|
},
|
|
34
38
|
"dependencies": {
|
|
35
|
-
"@ultimat3/core": "
|
|
36
|
-
"@ultimat3/query": "
|
|
39
|
+
"@ultimat3/core": "9.0.0",
|
|
40
|
+
"@ultimat3/query": "9.0.0",
|
|
37
41
|
"nats": "2.29.3"
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,49 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
// the
|
|
1
|
+
// The CLIENT half of the public API — everything a browser island may bundle. Explicit, tier by
|
|
2
|
+
// tier: the wire, the hooks, the identity map, the offline queue and the reconnect vocabulary.
|
|
3
|
+
// Nothing here reaches `nats`, a Postgres socket or the sync node; those are `./server`, and
|
|
4
|
+
// `packages/cli/src/realtime-browser-barrel.test.ts` is the build error that keeps them apart.
|
|
3
5
|
|
|
4
6
|
// ---- the client's one stateless piece, reusable against an app's own store ----------------------
|
|
5
7
|
export { applyPatches, orderAfterPatches } from './apply-patches';
|
|
6
|
-
export {
|
|
7
|
-
type ChangeBufferOptions,
|
|
8
|
-
DEFAULT_MAX_BUFFER_BYTES,
|
|
9
|
-
DEFAULT_MAX_BUFFER_BYTES_PER_QUERY,
|
|
10
|
-
RingChangeBuffer,
|
|
11
|
-
} from './change-buffer';
|
|
12
|
-
// ---- tier 2: live queries ----------------------------------------------------------------------
|
|
13
|
-
export {
|
|
14
|
-
type ChangeEvent,
|
|
15
|
-
type ChangeFeed,
|
|
16
|
-
type ChangeFeedStartOptions,
|
|
17
|
-
type ChangeOp,
|
|
18
|
-
formatLsn,
|
|
19
|
-
InMemoryChangeFeed,
|
|
20
|
-
type InMemoryChangeFeedOptions,
|
|
21
|
-
PgLogicalReplicationFeed,
|
|
22
|
-
type PgLogicalReplicationOptions,
|
|
23
|
-
parseLsn,
|
|
24
|
-
} from './changefeed';
|
|
25
|
-
export {
|
|
26
|
-
type ChangeFeedSelection,
|
|
27
|
-
DEFAULT_REPLICATION_PUBLICATION,
|
|
28
|
-
DEFAULT_REPLICATION_SLOT,
|
|
29
|
-
REPLICATION_ENV_KEYS,
|
|
30
|
-
type ReplicationEnvironment,
|
|
31
|
-
replicatorLockKey,
|
|
32
|
-
type SelectChangeFeedOptions,
|
|
33
|
-
selectChangeFeed,
|
|
34
|
-
} from './changefeed-env';
|
|
35
|
-
// ---- tier 1: channels + presence ---------------------------------------------------------------
|
|
36
|
-
export {
|
|
37
|
-
ChannelHub,
|
|
38
|
-
type ChannelHubOptions,
|
|
39
|
-
channelFrame,
|
|
40
|
-
DEFAULT_MAX_TOPICS_PER_NODE,
|
|
41
|
-
type Topic,
|
|
42
|
-
type TopicGuard,
|
|
43
|
-
type TopicGuardArgs,
|
|
44
|
-
type TopicGuardResult,
|
|
45
|
-
topic,
|
|
46
|
-
} from './channel';
|
|
47
8
|
// ---- server + client halves -------------------------------------------------------------------
|
|
48
9
|
export {
|
|
49
10
|
type ClientSocket,
|
|
@@ -75,7 +36,7 @@ export {
|
|
|
75
36
|
shouldResnapshot,
|
|
76
37
|
verifyDigest,
|
|
77
38
|
} from './cursor';
|
|
78
|
-
// ---- errors
|
|
39
|
+
// ---- errors: one vocabulary for both halves, because every code reaches the wire ---------------
|
|
79
40
|
export {
|
|
80
41
|
CursorStaleError,
|
|
81
42
|
FrameRateLimitError,
|
|
@@ -99,16 +60,6 @@ export {
|
|
|
99
60
|
TransportProtocolError,
|
|
100
61
|
TransportUnavailableError,
|
|
101
62
|
} from './errors';
|
|
102
|
-
export {
|
|
103
|
-
InProcessTransport,
|
|
104
|
-
type InProcessTransportOptions,
|
|
105
|
-
subjectMatches,
|
|
106
|
-
type Transport,
|
|
107
|
-
type TransportHandler,
|
|
108
|
-
type TransportSet,
|
|
109
|
-
type TransportSetEntry,
|
|
110
|
-
type TransportSubscription,
|
|
111
|
-
} from './fanout';
|
|
112
63
|
// ---- the client hooks --------------------------------------------------------------------------
|
|
113
64
|
export {
|
|
114
65
|
type ConflictLike,
|
|
@@ -147,17 +98,6 @@ export {
|
|
|
147
98
|
type RowOp,
|
|
148
99
|
type RowPatch,
|
|
149
100
|
} from './json';
|
|
150
|
-
export type {
|
|
151
|
-
LiveQueryDefinition,
|
|
152
|
-
LiveSubscription,
|
|
153
|
-
SnapshotResult,
|
|
154
|
-
} from './live-contract';
|
|
155
|
-
export { type LiveDefinitionOptions, liveQueryDefinition } from './live-definition';
|
|
156
|
-
export {
|
|
157
|
-
DEFAULT_MAX_ENTRIES,
|
|
158
|
-
LiveQueryRegistry,
|
|
159
|
-
type LiveQueryRegistryOptions,
|
|
160
|
-
} from './live-query';
|
|
161
101
|
export { type Registration, RowWindows } from './live-rows';
|
|
162
102
|
// ---- tier 3: local-first ------------------------------------------------------------------------
|
|
163
103
|
export {
|
|
@@ -169,51 +109,6 @@ export {
|
|
|
169
109
|
type OpfsLocalStoreOptions,
|
|
170
110
|
type TableMap,
|
|
171
111
|
} from './local-store';
|
|
172
|
-
export {
|
|
173
|
-
applyToWindow,
|
|
174
|
-
type BridgeResult,
|
|
175
|
-
bridgeChange,
|
|
176
|
-
canAffect,
|
|
177
|
-
type IncrementalMatcher,
|
|
178
|
-
matcherFor,
|
|
179
|
-
NO_CHANGE,
|
|
180
|
-
normalizePatch,
|
|
181
|
-
patchFromChange,
|
|
182
|
-
type SubscriptionShape,
|
|
183
|
-
toBridgeResult,
|
|
184
|
-
} from './matcher-bridge';
|
|
185
|
-
// ---- the production bus -------------------------------------------------------------------------
|
|
186
|
-
export {
|
|
187
|
-
DEFAULT_NATS_PORT,
|
|
188
|
-
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
189
|
-
type NatsClient,
|
|
190
|
-
type NatsClientOptions,
|
|
191
|
-
type NatsConnect,
|
|
192
|
-
type NatsHeaders,
|
|
193
|
-
type NatsMessage,
|
|
194
|
-
type NatsMessageHandler,
|
|
195
|
-
type NatsRequestManyOptions,
|
|
196
|
-
type NatsRequestOptions,
|
|
197
|
-
type NatsSubscription,
|
|
198
|
-
type NatsTarget,
|
|
199
|
-
parseNatsUrl,
|
|
200
|
-
} from './nats-client';
|
|
201
|
-
export { FakeNatsBroker, type FakeNatsOptions, fakeNatsConnect } from './nats-fake';
|
|
202
|
-
export {
|
|
203
|
-
assertBucket,
|
|
204
|
-
assertServerVersion,
|
|
205
|
-
ensureKvBucket,
|
|
206
|
-
type JsError,
|
|
207
|
-
type KvRecord,
|
|
208
|
-
kvGet,
|
|
209
|
-
kvLast,
|
|
210
|
-
kvStream,
|
|
211
|
-
kvSubject,
|
|
212
|
-
kvWrite,
|
|
213
|
-
} from './nats-jetstream';
|
|
214
|
-
export { decodeToken, encodeToken, NatsKvSet, type NatsKvSetOptions } from './nats-kv';
|
|
215
|
-
export { openNatsClient } from './nats-lib-client';
|
|
216
|
-
export { NatsTransport, type NatsTransportOptions } from './nats-transport';
|
|
217
112
|
export {
|
|
218
113
|
type DrainReport,
|
|
219
114
|
MemoryQueueStore,
|
|
@@ -225,47 +120,12 @@ export {
|
|
|
225
120
|
type QueueState,
|
|
226
121
|
type QueueStore,
|
|
227
122
|
} from './offline-queue';
|
|
228
|
-
export { PgAdvisoryLock, type PgAdvisoryLockOptions } from './pg-advisory-lock';
|
|
229
|
-
// ---- the postgres replication path ------------------------------------------------------------
|
|
230
|
-
export { camel, entityRow } from './pg-entity-row';
|
|
231
|
-
export {
|
|
232
|
-
changeLsn,
|
|
233
|
-
commitPositionOf,
|
|
234
|
-
type ReplicationStreamStats,
|
|
235
|
-
} from './pg-replication';
|
|
236
|
-
export { bunPgStream, type PgTarget, parsePgUrl, type SslMode } from './pg-socket';
|
|
237
|
-
export type { PgStream } from './pg-wire';
|
|
238
|
-
export {
|
|
239
|
-
type PgColumn,
|
|
240
|
-
PgOutputDecoder,
|
|
241
|
-
type PgOutputMessage,
|
|
242
|
-
type PgRelation,
|
|
243
|
-
} from './pgoutput';
|
|
244
|
-
export { authorizeWithPolicy, type GateOptions, visibleWithPolicy } from './policy-gate';
|
|
245
|
-
export {
|
|
246
|
-
DEFAULT_MAX_PRESENCE_MEMBERS,
|
|
247
|
-
PRESENCE_KEY_PREFIX,
|
|
248
|
-
PRESENCE_SWEEP_PREFIX,
|
|
249
|
-
type PresenceInput,
|
|
250
|
-
type PresenceOptions,
|
|
251
|
-
PresenceRegistry,
|
|
252
|
-
type PresenceRoster,
|
|
253
|
-
presenceFrame,
|
|
254
|
-
} from './presence';
|
|
255
123
|
/** The typed projection: one query bound to one named hook, `useLiveFeed({ orgId })`. */
|
|
256
124
|
export {
|
|
257
125
|
type LiveQueryHook,
|
|
258
126
|
type LiveQuerySource,
|
|
259
127
|
liveHookFor,
|
|
260
128
|
} from './query-hook';
|
|
261
|
-
export {
|
|
262
|
-
createEntry,
|
|
263
|
-
fillWindow,
|
|
264
|
-
orgIdOf,
|
|
265
|
-
type PendingRead,
|
|
266
|
-
type QueryEntry,
|
|
267
|
-
refillWindowInLane,
|
|
268
|
-
} from './query-window';
|
|
269
129
|
export {
|
|
270
130
|
type ConflictStrategy,
|
|
271
131
|
type CustomMerge,
|
|
@@ -280,72 +140,6 @@ export {
|
|
|
280
140
|
type ServerAck,
|
|
281
141
|
strategyName,
|
|
282
142
|
} from './rebase';
|
|
283
|
-
export {
|
|
284
|
-
type AdvisoryLock,
|
|
285
|
-
CHANGE_SUBJECT_PREFIX,
|
|
286
|
-
type ChangeEnvelope,
|
|
287
|
-
changeSubject,
|
|
288
|
-
createReplicator,
|
|
289
|
-
InMemoryAdvisoryLock,
|
|
290
|
-
normalize,
|
|
291
|
-
parseChange,
|
|
292
|
-
parseEnvelope,
|
|
293
|
-
type Replicator,
|
|
294
|
-
type ReplicatorOptions,
|
|
295
|
-
type ReplicatorStats,
|
|
296
|
-
SeqGapDetector,
|
|
297
|
-
} from './replicator';
|
|
298
|
-
export {
|
|
299
|
-
actorIdOf,
|
|
300
|
-
CLOSE,
|
|
301
|
-
DEFAULT_FRAME_BURST,
|
|
302
|
-
DEFAULT_IDLE_TIMEOUT_MS,
|
|
303
|
-
DEFAULT_MAX_BUFFERED_BYTES,
|
|
304
|
-
DEFAULT_MAX_FRAMES_PER_SECOND,
|
|
305
|
-
idleSweepPeriodMs,
|
|
306
|
-
SocketRegistry,
|
|
307
|
-
type SocketRegistryOptions,
|
|
308
|
-
SyncSocket,
|
|
309
|
-
type SyncSocketOptions,
|
|
310
|
-
type WsLike,
|
|
311
|
-
} from './socket';
|
|
312
|
-
export type {
|
|
313
|
-
GateFailed,
|
|
314
|
-
GateStage,
|
|
315
|
-
RowDenied,
|
|
316
|
-
Subscriber,
|
|
317
|
-
SubscriberGateOptions,
|
|
318
|
-
} from './subscriber-gate';
|
|
319
|
-
export {
|
|
320
|
-
GrantBook,
|
|
321
|
-
type GrantSweepDeps,
|
|
322
|
-
type GrantSweepResult,
|
|
323
|
-
type SyncAuthenticator,
|
|
324
|
-
type SyncGrant,
|
|
325
|
-
sweepGrants,
|
|
326
|
-
} from './sync-auth';
|
|
327
|
-
export {
|
|
328
|
-
createFrameRouter,
|
|
329
|
-
type FrameRouter,
|
|
330
|
-
type FrameRouterOptions,
|
|
331
|
-
type MutationHandler,
|
|
332
|
-
} from './sync-frames';
|
|
333
|
-
export {
|
|
334
|
-
type ListenOptions,
|
|
335
|
-
listenSyncNode,
|
|
336
|
-
type SyncListener,
|
|
337
|
-
} from './sync-listen';
|
|
338
|
-
export {
|
|
339
|
-
createSyncNode,
|
|
340
|
-
DEFAULT_MAX_CONNECTIONS,
|
|
341
|
-
DEFAULT_MAX_FRAME_BYTES,
|
|
342
|
-
DEFAULT_REAUTH_INTERVAL_MS,
|
|
343
|
-
type SyncNode,
|
|
344
|
-
type SyncNodeOptions,
|
|
345
|
-
type SyncWs,
|
|
346
|
-
type UpgradeTarget,
|
|
347
|
-
type WsData,
|
|
348
|
-
} from './sync-node';
|
|
349
143
|
// ---- the wire -------------------------------------------------------------------------------------
|
|
350
144
|
export {
|
|
351
145
|
type AckFrame,
|
|
@@ -371,29 +165,14 @@ export {
|
|
|
371
165
|
type UpdateAvailableFrame,
|
|
372
166
|
type WireError,
|
|
373
167
|
} from './sync-protocol';
|
|
168
|
+
// ---- the client's own reconnect: the backoff it computes and the timer it arms -----------------
|
|
374
169
|
export {
|
|
375
|
-
AcceptBudget,
|
|
376
|
-
type AcceptBudgetOptions,
|
|
377
170
|
type BackoffPolicy,
|
|
378
171
|
backoffDelay,
|
|
379
|
-
type DrainedSocket,
|
|
380
|
-
type DrainPlanEntry,
|
|
381
|
-
type DrainPlanOptions,
|
|
382
172
|
defaultBackoff,
|
|
383
|
-
drainPlan,
|
|
384
173
|
type JitterMode,
|
|
385
174
|
type ReconnectReason,
|
|
386
175
|
type Rng,
|
|
387
|
-
reconnectFrame,
|
|
388
176
|
type Scheduler,
|
|
389
177
|
timeoutScheduler,
|
|
390
178
|
} from './thundering-herd';
|
|
391
|
-
export {
|
|
392
|
-
DEFAULT_PRESENCE_BUCKET,
|
|
393
|
-
DEFAULT_PRESENCE_TTL_MS,
|
|
394
|
-
type SelectTransportOptions,
|
|
395
|
-
selectTransport,
|
|
396
|
-
TRANSPORT_ENV_KEYS,
|
|
397
|
-
type TransportEnvironment,
|
|
398
|
-
type TransportSelection,
|
|
399
|
-
} from './transport-env';
|
package/src/offline-queue.ts
CHANGED
|
@@ -217,6 +217,23 @@ export class OfflineQueue {
|
|
|
217
217
|
await this.#persist();
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Re-asked at the top of every iteration, because `#sendable()` is a SNAPSHOT and the loop awaits
|
|
222
|
+
* a network `send` inside it. An `ack` landing mid-pass removes its entry from the queue, a
|
|
223
|
+
* `fail` marks it terminal and a `clear` drops all of them — and the pass, holding the snapshot,
|
|
224
|
+
* re-sent an intent the server had already settled AND counted it in `DrainReport.sent`, so the
|
|
225
|
+
* number a UI renders as "synced" was larger than the frames that left the tab.
|
|
226
|
+
*
|
|
227
|
+
* Deliberately NOT an `#epoch` bump in `ack`/`fail`: an ack arriving during a drain is the
|
|
228
|
+
* ordinary case — the server answers frame 1 while the pass is on frame 2 — and invalidating the
|
|
229
|
+
* pass would stop every drain against a server that answers promptly. The epoch means "the
|
|
230
|
+
* connection this pass was draining into is gone", which is one mutation's settlement away from
|
|
231
|
+
* nothing at all.
|
|
232
|
+
*/
|
|
233
|
+
#stillSendable(mutation: QueuedMutation): boolean {
|
|
234
|
+
return mutation.status === 'pending' && this.#mutations.includes(mutation);
|
|
235
|
+
}
|
|
236
|
+
|
|
220
237
|
/** Never sent on this connection. `inflight` is excluded: it is already on a socket. */
|
|
221
238
|
#sendable(): readonly QueuedMutation[] {
|
|
222
239
|
return this.#mutations
|
|
@@ -246,6 +263,9 @@ export class OfflineQueue {
|
|
|
246
263
|
stoppedAt: mutation.key,
|
|
247
264
|
};
|
|
248
265
|
}
|
|
266
|
+
// Settled by the server, or dropped by `clear()`, since the snapshot was taken. Skipped
|
|
267
|
+
// rather than stopping the pass: nothing behind it moved, so the order invariant holds.
|
|
268
|
+
if (!this.#stillSendable(mutation)) continue;
|
|
249
269
|
mutation.status = 'inflight';
|
|
250
270
|
mutation.attempts += 1;
|
|
251
271
|
try {
|
package/src/pgoutput.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderThrowable } from '@ultimat3/core';
|
|
1
2
|
// Decodes pgoutput logical-replication messages (protocol version 1, Postgres >= 12) into typed
|
|
2
3
|
// PgOutputMessage values, and the postgres text-format values inside each tuple into JsonValue.
|
|
3
4
|
// Pure byte decoding: no sockets, no I/O. A decoder instance owns the per-connection relation
|
|
@@ -92,7 +93,7 @@ function decodeValue(typeOid: number, text: string): JsonValue {
|
|
|
92
93
|
} catch (cause) {
|
|
93
94
|
throw new ReplicationProtocolError({
|
|
94
95
|
stage: 'value',
|
|
95
|
-
detail: `type oid ${typeOid} carried invalid json: ${
|
|
96
|
+
detail: `type oid ${typeOid} carried invalid json: ${renderThrowable(cause)}`,
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
return parsed as JsonValue;
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// The SERVER half of the public API: the bus, the Postgres replication path, the sync node and the
|
|
2
|
+
// live-query registry it fans out through. Split from `index.ts` because `nats` require()s
|
|
3
|
+
// `stream/web` and the WAL decoder is a Postgres client — one barrel carrying both made the browser
|
|
4
|
+
// island `useLive` promises unbuildable. Every name here has exactly one home; the shared
|
|
5
|
+
// vocabulary (the wire, the errors, `Row`, the backoff) stays on `@ultimat3/realtime`.
|
|
6
|
+
|
|
7
|
+
// ---- the retained change window one node fans out from ------------------------------------------
|
|
8
|
+
export {
|
|
9
|
+
type ChangeBufferOptions,
|
|
10
|
+
DEFAULT_MAX_BUFFER_BYTES,
|
|
11
|
+
DEFAULT_MAX_BUFFER_BYTES_PER_QUERY,
|
|
12
|
+
RingChangeBuffer,
|
|
13
|
+
} from './change-buffer';
|
|
14
|
+
// ---- tier 2: live queries ----------------------------------------------------------------------
|
|
15
|
+
export {
|
|
16
|
+
type ChangeEvent,
|
|
17
|
+
type ChangeFeed,
|
|
18
|
+
type ChangeFeedStartOptions,
|
|
19
|
+
type ChangeOp,
|
|
20
|
+
formatLsn,
|
|
21
|
+
InMemoryChangeFeed,
|
|
22
|
+
type InMemoryChangeFeedOptions,
|
|
23
|
+
PgLogicalReplicationFeed,
|
|
24
|
+
type PgLogicalReplicationOptions,
|
|
25
|
+
parseLsn,
|
|
26
|
+
} from './changefeed';
|
|
27
|
+
export {
|
|
28
|
+
type ChangeFeedSelection,
|
|
29
|
+
DEFAULT_REPLICATION_PUBLICATION,
|
|
30
|
+
DEFAULT_REPLICATION_SLOT,
|
|
31
|
+
REPLICATION_ENV_KEYS,
|
|
32
|
+
type ReplicationEnvironment,
|
|
33
|
+
replicatorLockKey,
|
|
34
|
+
type SelectChangeFeedOptions,
|
|
35
|
+
selectChangeFeed,
|
|
36
|
+
} from './changefeed-env';
|
|
37
|
+
// ---- tier 1: channels + presence ---------------------------------------------------------------
|
|
38
|
+
export {
|
|
39
|
+
ChannelHub,
|
|
40
|
+
type ChannelHubOptions,
|
|
41
|
+
channelFrame,
|
|
42
|
+
DEFAULT_MAX_TOPICS_PER_NODE,
|
|
43
|
+
type Topic,
|
|
44
|
+
type TopicGuard,
|
|
45
|
+
type TopicGuardArgs,
|
|
46
|
+
type TopicGuardResult,
|
|
47
|
+
topic,
|
|
48
|
+
} from './channel';
|
|
49
|
+
export {
|
|
50
|
+
InProcessTransport,
|
|
51
|
+
type InProcessTransportOptions,
|
|
52
|
+
subjectMatches,
|
|
53
|
+
type Transport,
|
|
54
|
+
type TransportHandler,
|
|
55
|
+
type TransportSet,
|
|
56
|
+
type TransportSetEntry,
|
|
57
|
+
type TransportSubscription,
|
|
58
|
+
} from './fanout';
|
|
59
|
+
export type {
|
|
60
|
+
LiveQueryDefinition,
|
|
61
|
+
LiveSubscription,
|
|
62
|
+
SnapshotResult,
|
|
63
|
+
} from './live-contract';
|
|
64
|
+
export { type LiveDefinitionOptions, liveQueryDefinition } from './live-definition';
|
|
65
|
+
export {
|
|
66
|
+
DEFAULT_MAX_ENTRIES,
|
|
67
|
+
LiveQueryRegistry,
|
|
68
|
+
type LiveQueryRegistryOptions,
|
|
69
|
+
} from './live-query';
|
|
70
|
+
export {
|
|
71
|
+
applyToWindow,
|
|
72
|
+
type BridgeResult,
|
|
73
|
+
bridgeChange,
|
|
74
|
+
canAffect,
|
|
75
|
+
type IncrementalMatcher,
|
|
76
|
+
matcherFor,
|
|
77
|
+
NO_CHANGE,
|
|
78
|
+
normalizePatch,
|
|
79
|
+
patchFromChange,
|
|
80
|
+
type SubscriptionShape,
|
|
81
|
+
toBridgeResult,
|
|
82
|
+
} from './matcher-bridge';
|
|
83
|
+
// ---- the production bus -------------------------------------------------------------------------
|
|
84
|
+
export {
|
|
85
|
+
DEFAULT_NATS_PORT,
|
|
86
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
87
|
+
type NatsClient,
|
|
88
|
+
type NatsClientOptions,
|
|
89
|
+
type NatsConnect,
|
|
90
|
+
type NatsHeaders,
|
|
91
|
+
type NatsMessage,
|
|
92
|
+
type NatsMessageHandler,
|
|
93
|
+
type NatsRequestManyOptions,
|
|
94
|
+
type NatsRequestOptions,
|
|
95
|
+
type NatsSubscription,
|
|
96
|
+
type NatsTarget,
|
|
97
|
+
parseNatsUrl,
|
|
98
|
+
} from './nats-client';
|
|
99
|
+
export { FakeNatsBroker, type FakeNatsOptions, fakeNatsConnect } from './nats-fake';
|
|
100
|
+
export {
|
|
101
|
+
assertBucket,
|
|
102
|
+
assertServerVersion,
|
|
103
|
+
ensureKvBucket,
|
|
104
|
+
type JsError,
|
|
105
|
+
type KvRecord,
|
|
106
|
+
kvGet,
|
|
107
|
+
kvLast,
|
|
108
|
+
kvStream,
|
|
109
|
+
kvSubject,
|
|
110
|
+
kvWrite,
|
|
111
|
+
} from './nats-jetstream';
|
|
112
|
+
export { decodeToken, encodeToken, NatsKvSet, type NatsKvSetOptions } from './nats-kv';
|
|
113
|
+
export { openNatsClient } from './nats-lib-client';
|
|
114
|
+
export { NatsTransport, type NatsTransportOptions } from './nats-transport';
|
|
115
|
+
export { PgAdvisoryLock, type PgAdvisoryLockOptions } from './pg-advisory-lock';
|
|
116
|
+
// ---- the postgres replication path ------------------------------------------------------------
|
|
117
|
+
export { camel, entityRow } from './pg-entity-row';
|
|
118
|
+
export {
|
|
119
|
+
changeLsn,
|
|
120
|
+
commitPositionOf,
|
|
121
|
+
type ReplicationStreamStats,
|
|
122
|
+
} from './pg-replication';
|
|
123
|
+
export { bunPgStream, type PgTarget, parsePgUrl, type SslMode } from './pg-socket';
|
|
124
|
+
export type { PgStream } from './pg-wire';
|
|
125
|
+
export {
|
|
126
|
+
type PgColumn,
|
|
127
|
+
PgOutputDecoder,
|
|
128
|
+
type PgOutputMessage,
|
|
129
|
+
type PgRelation,
|
|
130
|
+
} from './pgoutput';
|
|
131
|
+
export { authorizeWithPolicy, type GateOptions, visibleWithPolicy } from './policy-gate';
|
|
132
|
+
export {
|
|
133
|
+
DEFAULT_MAX_PRESENCE_MEMBERS,
|
|
134
|
+
PRESENCE_KEY_PREFIX,
|
|
135
|
+
PRESENCE_SWEEP_PREFIX,
|
|
136
|
+
type PresenceInput,
|
|
137
|
+
type PresenceOptions,
|
|
138
|
+
PresenceRegistry,
|
|
139
|
+
type PresenceRoster,
|
|
140
|
+
presenceFrame,
|
|
141
|
+
} from './presence';
|
|
142
|
+
export {
|
|
143
|
+
createEntry,
|
|
144
|
+
fillWindow,
|
|
145
|
+
orgIdOf,
|
|
146
|
+
type PendingRead,
|
|
147
|
+
type QueryEntry,
|
|
148
|
+
refillWindowInLane,
|
|
149
|
+
} from './query-window';
|
|
150
|
+
export {
|
|
151
|
+
type AdvisoryLock,
|
|
152
|
+
CHANGE_SUBJECT_PREFIX,
|
|
153
|
+
type ChangeEnvelope,
|
|
154
|
+
changeSubject,
|
|
155
|
+
createReplicator,
|
|
156
|
+
InMemoryAdvisoryLock,
|
|
157
|
+
normalize,
|
|
158
|
+
parseChange,
|
|
159
|
+
parseEnvelope,
|
|
160
|
+
type Replicator,
|
|
161
|
+
type ReplicatorOptions,
|
|
162
|
+
type ReplicatorStats,
|
|
163
|
+
SeqGapDetector,
|
|
164
|
+
} from './replicator';
|
|
165
|
+
export {
|
|
166
|
+
actorIdOf,
|
|
167
|
+
CLOSE,
|
|
168
|
+
DEFAULT_FRAME_BURST,
|
|
169
|
+
DEFAULT_IDLE_TIMEOUT_MS,
|
|
170
|
+
DEFAULT_MAX_BUFFERED_BYTES,
|
|
171
|
+
DEFAULT_MAX_FRAMES_PER_SECOND,
|
|
172
|
+
idleSweepPeriodMs,
|
|
173
|
+
SocketRegistry,
|
|
174
|
+
type SocketRegistryOptions,
|
|
175
|
+
SyncSocket,
|
|
176
|
+
type SyncSocketOptions,
|
|
177
|
+
type WsLike,
|
|
178
|
+
} from './socket';
|
|
179
|
+
export type {
|
|
180
|
+
GateFailed,
|
|
181
|
+
GateStage,
|
|
182
|
+
RowDenied,
|
|
183
|
+
Subscriber,
|
|
184
|
+
SubscriberGateOptions,
|
|
185
|
+
} from './subscriber-gate';
|
|
186
|
+
export {
|
|
187
|
+
GrantBook,
|
|
188
|
+
type GrantSweepDeps,
|
|
189
|
+
type GrantSweepResult,
|
|
190
|
+
type SyncAuthenticator,
|
|
191
|
+
type SyncGrant,
|
|
192
|
+
sweepGrants,
|
|
193
|
+
} from './sync-auth';
|
|
194
|
+
export {
|
|
195
|
+
createFrameRouter,
|
|
196
|
+
type FrameRouter,
|
|
197
|
+
type FrameRouterOptions,
|
|
198
|
+
type MutationHandler,
|
|
199
|
+
} from './sync-frames';
|
|
200
|
+
export {
|
|
201
|
+
type ListenOptions,
|
|
202
|
+
listenSyncNode,
|
|
203
|
+
type SyncListener,
|
|
204
|
+
} from './sync-listen';
|
|
205
|
+
export {
|
|
206
|
+
createSyncNode,
|
|
207
|
+
DEFAULT_MAX_CONNECTIONS,
|
|
208
|
+
DEFAULT_MAX_FRAME_BYTES,
|
|
209
|
+
DEFAULT_REAUTH_INTERVAL_MS,
|
|
210
|
+
type SyncNode,
|
|
211
|
+
type SyncNodeOptions,
|
|
212
|
+
type SyncWs,
|
|
213
|
+
type UpgradeTarget,
|
|
214
|
+
type WsData,
|
|
215
|
+
} from './sync-node';
|
|
216
|
+
// ---- admission and drain: the node's half of the reconnect vocabulary ---------------------------
|
|
217
|
+
export {
|
|
218
|
+
AcceptBudget,
|
|
219
|
+
type AcceptBudgetOptions,
|
|
220
|
+
type DrainedSocket,
|
|
221
|
+
type DrainPlanEntry,
|
|
222
|
+
type DrainPlanOptions,
|
|
223
|
+
drainPlan,
|
|
224
|
+
reconnectFrame,
|
|
225
|
+
} from './thundering-herd';
|
|
226
|
+
export {
|
|
227
|
+
DEFAULT_PRESENCE_BUCKET,
|
|
228
|
+
DEFAULT_PRESENCE_TTL_MS,
|
|
229
|
+
type SelectTransportOptions,
|
|
230
|
+
selectTransport,
|
|
231
|
+
TRANSPORT_ENV_KEYS,
|
|
232
|
+
type TransportEnvironment,
|
|
233
|
+
type TransportSelection,
|
|
234
|
+
} from './transport-env';
|