@ultimat3/realtime 24.0.0 → 25.1.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 +61 -63
- package/README.md +131 -46
- package/package.json +9 -6
- package/src/advisory-lock.ts +10 -5
- package/src/boot.ts +14 -9
- package/src/changefeed-env.ts +8 -8
- package/src/changefeed.ts +42 -18
- package/src/channel-authz.ts +7 -7
- package/src/channel-describe.ts +1 -1
- package/src/channel-logs.ts +16 -25
- package/src/channel-presence.ts +3 -3
- package/src/channel-records.ts +3 -3
- package/src/channel-ref.ts +2 -2
- package/src/channel-registry.ts +2 -2
- package/src/channel.ts +8 -7
- package/src/client-channels.ts +2 -0
- package/src/client-frames.ts +9 -2
- package/src/client.ts +29 -2
- package/src/error-titles.ts +91 -0
- package/src/errors.ts +47 -90
- package/src/first-paint-hold.ts +49 -0
- package/src/idb-types.ts +2 -2
- package/src/index.ts +24 -6
- package/src/json.ts +16 -3
- package/src/live-contract.ts +14 -0
- package/src/live-definition.ts +12 -2
- package/src/live-deliver.ts +55 -0
- package/src/live-fanout.ts +81 -24
- package/src/live-query-options.ts +6 -0
- package/src/live-query.ts +66 -144
- package/src/live-reauth.ts +127 -0
- package/src/live-replicator.ts +28 -8
- package/src/live-resume.ts +44 -8
- package/src/live-rows.ts +32 -3
- package/src/live-spend.ts +50 -0
- package/src/local-store-idb.ts +5 -0
- package/src/matcher-bridge.ts +11 -5
- package/src/nats-client.ts +26 -4
- package/src/nats-lib-client.ts +36 -7
- package/src/nats-subscriptions.ts +63 -0
- package/src/nats-transport.ts +127 -15
- package/src/offline-queue.ts +5 -0
- package/src/page-errors.ts +8 -9
- package/src/page-outbox.ts +5 -5
- package/src/page-runtime-wait.ts +72 -0
- package/src/page-runtime.ts +32 -0
- package/src/page-socket.ts +7 -16
- package/src/page-store.ts +83 -16
- package/src/pg-advisory-lock.ts +9 -4
- package/src/pg-connection.ts +7 -1
- package/src/pg-replication.ts +6 -6
- package/src/pg-socket.ts +20 -3
- package/src/pg-values.ts +29 -14
- package/src/policy-gate.ts +7 -7
- package/src/presence.ts +2 -2
- package/src/principal-sockets.ts +75 -0
- package/src/query-window.ts +1 -1
- package/src/record-publisher.ts +178 -0
- package/src/record-store.ts +25 -4
- package/src/record-synced.ts +43 -1
- package/src/replication-errors.ts +3 -3
- package/src/replicator-envelope.ts +61 -11
- package/src/replicator-row.ts +1 -1
- package/src/replicator.ts +12 -23
- package/src/server.ts +36 -18
- package/src/socket-defaults.ts +20 -0
- package/src/socket.ts +14 -19
- package/src/subscription-book.ts +129 -71
- package/src/subscription-count.ts +151 -0
- package/src/sync-actor-change.ts +56 -0
- package/src/sync-bus-handlers.ts +131 -0
- package/src/sync-frames.ts +1 -1
- package/src/sync-node-bounds.ts +1 -1
- package/src/sync-node-contract.ts +16 -2
- package/src/sync-node.ts +115 -109
- package/src/sync-origin.ts +1 -1
- package/src/sync-protocol.ts +73 -13
- package/src/sync-upgrade.ts +39 -0
- package/src/type-pins.ts +9 -1
- package/src/use-channel.ts +19 -14
- package/src/use-connection.ts +3 -3
- package/src/use-mutation.ts +9 -16
- package/src/use-query.ts +11 -11
- package/src/use-record.ts +89 -10
- package/src/wire-channel.ts +5 -5
- package/src/wire-read.ts +2 -2
- package/src/idb-fake.ts +0 -133
- package/src/policy-fake.ts +0 -14
package/CLAUDE.md
CHANGED
|
@@ -7,7 +7,7 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
7
7
|
| May import | Must not |
|
|
8
8
|
|---|---|
|
|
9
9
|
| `@ultimat3/core`, `@ultimat3/query`, `@ultimat3/entity` (`/record` only, server-side) | anything tier 4+ (`render`, `pwa`, `mcp`, `ui`, `cli`) |
|
|
10
|
-
| `@ultimat3/policy` **only via** `@ultimat3/query`'s `
|
|
10
|
+
| `@ultimat3/policy` **only via** `@ultimat3/query`'s `guardQuery` for a decision (`policyCapability` / `policyPermissions` are display reads) | a second authz path of any kind |
|
|
11
11
|
| — | `solid-js` (each island bundle installs its own signal factory: `installRealtime`) |
|
|
12
12
|
| `nats` (the one external dependency, pinned exact) — from `nats-lib-client.ts` and no other file | `nats` from anywhere else. Every other file is written against the port in `nats-client.ts` |
|
|
13
13
|
|
|
@@ -21,12 +21,12 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
21
21
|
link). `packages/cli/src/realtime-browser-barrel.test.ts` bundles a `useLive`-only entry;
|
|
22
22
|
`barrel-split.test.ts` refuses a name exported from both. `./boot` and `./sync-worker` are the page
|
|
23
23
|
boot and the SharedWorker entry.
|
|
24
|
-
- **`
|
|
25
|
-
|
|
26
|
-
Postgres
|
|
27
|
-
|
|
28
|
-
- **`sideEffects` is
|
|
29
|
-
|
|
24
|
+
- **`error-titles.ts` holds the codes, titles and `registerErrorCodes()`**; `errors.ts` the server
|
|
25
|
+
refusals, `page-errors.ts` the browser ones, `realtime-error.ts` the base class (TDZ),
|
|
26
|
+
`replication-errors.ts` Postgres. All re-exported from `./errors`. A hook's chunk never loads
|
|
27
|
+
`errors.ts` (`page-errors-bundle.test.ts`).
|
|
28
|
+
- **`sideEffects` is `["./src/error-titles.ts"]`**, imported bare by the barrel (`refusalError` and
|
|
29
|
+
`page-errors.ts` title by code); never `false` or absent.
|
|
30
30
|
- **Every `@ultimat3/realtime/<subpath>` written in shipped source must be a key of `exports`**,
|
|
31
31
|
comments included — `fix-specifier.test.ts`.
|
|
32
32
|
- A browser file imports core from `@ultimat3/core/page` (`packages/core/src/page-bundle.test.ts`).
|
|
@@ -34,10 +34,9 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
34
34
|
## Numbers and ceilings
|
|
35
35
|
|
|
36
36
|
- **Every numeric option is refused when it is not FINITE** — `@ultimat3/core`'s `finiteOption()`.
|
|
37
|
-
`bun run finite-bounds`
|
|
38
|
-
by reading the option interfaces. `SyncGrant.expiresAt` is app DATA, deliberately unscreened.
|
|
37
|
+
`bun run finite-bounds` cannot see an option with no `??` default: read the option interfaces. `SyncGrant.expiresAt` is app DATA, deliberately unscreened.
|
|
39
38
|
- **A ceiling is refused where the object is BUILT**, never in a per-connection callback:
|
|
40
|
-
`socketCeilings()` (`sync-node-bounds.ts`) runs once in `
|
|
39
|
+
`socketCeilings()` (`sync-node-bounds.ts`) runs once in `syncNode`; `SyncSocket` keeps its own
|
|
41
40
|
screen because it is exported. `AcceptBudget`, `SyncSocket` and `SocketRegistry` throw
|
|
42
41
|
`X_INVARIANT` at construction on `NaN`/`±Infinity`.
|
|
43
42
|
- **A ceiling per resource, and the wire's are not options** (table in `README.md`): the accept
|
|
@@ -57,7 +56,7 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
57
56
|
per-tenant count; `SocketRegistry` keeps `#byTopic`, changed only by `joinTopic`/`leaveTopic`.
|
|
58
57
|
`reauthorize` calls `book.retenant(socket)`.
|
|
59
58
|
- **A cap is a RESERVATION taken before the first await**: `SubscriptionBook.reserve(socket, sid)`
|
|
60
|
-
decides the sid claim, `maxPerSocket` and `
|
|
59
|
+
decides the sid claim, `maxPerSocket`, `maxPerTenant` and `maxPerActor` in one synchronous step;
|
|
61
60
|
`ChannelHub.subscribe` does the same for its caps. Released in a `finally`, idempotently.
|
|
62
61
|
- **Readiness AND the connection cap are functions on `UpgradeDeps`**, re-asked after
|
|
63
62
|
`authenticate` (app code with an await) and right before `server.upgrade`. The recheck sheds with
|
|
@@ -103,9 +102,9 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
103
102
|
- **A change the window already holds is refused** (`change.lsn <= entry.lsn`, counted as
|
|
104
103
|
`staleChanges`). **A gap is detected**: the replicator stamps `producer` + `seq`; a skipped
|
|
105
104
|
sequence — or a NEW producer after a known one — stales every window; `refillWindowInLane` replaces it.
|
|
106
|
-
- **A bulk write stales only
|
|
107
|
-
`onBulk
|
|
108
|
-
on a non-
|
|
105
|
+
- **A bulk write stales only its entity's readers**: windows + channel topics, `invalidate(entity)`
|
|
106
|
+
on both from `onBulk`. **A stale window's re-read re-snapshots EVERY subscriber**, before the lsn
|
|
107
|
+
guard, even on a non-match (`live-replicator-bulk`, `live-fanout` tests).
|
|
109
108
|
- **A WINDOW is per TENANT**: its id is `windowId(queryHash(name, input), tenant)` (`live-tenant.ts`;
|
|
110
109
|
tenant = the subscriber's `actor.orgId`). This package owns no hash. The read runs `readFor`
|
|
111
110
|
(`live-definition.ts`): the node's ctx, a service actor carrying ONLY that org — never the
|
|
@@ -114,6 +113,7 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
114
113
|
- **A `sid` is CLIENT data: a subscription is keyed by `(socket, sid)`**
|
|
115
114
|
(`subscription-book.ts` is the only spelling). Reusing a sid the same socket holds is
|
|
116
115
|
`X_SUBSCRIPTION_ID_TAKEN`.
|
|
116
|
+
- **`rateLimit:` is spent once per NEW subscription** (`live-spend.ts`).
|
|
117
117
|
- Truth is the server. A client is never the merge authority.
|
|
118
118
|
|
|
119
119
|
## The node, the bus and presence
|
|
@@ -122,9 +122,9 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
122
122
|
- **`selectTransport(env, realtime)` is the one place `realtime.transport` + env become a bus** (KV
|
|
123
123
|
bucket and presence TTL come back with it). The config decides, the env supplies: `'nats'` refuses
|
|
124
124
|
(`X_CONFIG_INVALID`) when the `urlEnv` variable is unset; `'memory'` refuses a set one.
|
|
125
|
-
- **`nats` is imported only by `nats-lib-client.ts`**; reconnect
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
- **`nats` is imported only by `nats-lib-client.ts`**; its reconnect re-subscribes. Only when it
|
|
126
|
+
GIVES UP (`onClosed`) does `NatsTransport` re-dial on backoff and re-bind kept subscriptions
|
|
127
|
+
(`nats-subscriptions.ts`); callers meanwhile are refused. KV semantics: `nats-jetstream`/`nats-kv`.
|
|
128
128
|
- **Nothing leaves `NatsTransport` uncoded** — `#translating` wraps `publish`/`subscribe` refusals
|
|
129
129
|
as `X_TRANSPORT_UNAVAILABLE`. `#ensure` reuses a mid-reconnect client on purpose;
|
|
130
130
|
`Transport.onReconnect(listener)` (required) announces each recovery.
|
|
@@ -138,13 +138,12 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
138
138
|
- **A socket the node evicts ITSELF goes through `evict(socket, code, reason)`** (the full
|
|
139
139
|
`teardown`), never `sockets.remove`. **`drain()` waits for the presence leaves it started** —
|
|
140
140
|
`evictInChunks` (`drain-evictions.ts`, `DRAIN_EVICT_CHUNK`, `allSettled`).
|
|
141
|
-
- **The idle sweep is armed by `start()`** (
|
|
142
|
-
|
|
143
|
-
the node evicts.
|
|
141
|
+
- **The idle sweep is armed by `start()`** (`idleTimeoutMs / 4`, floored at 1 s, `.unref()`), on
|
|
142
|
+
`Clock.monotonic()`. `SocketRegistry.idle()` is a query; the node evicts.
|
|
144
143
|
- **`drain()` and `stop()` both call one idempotent `release()`**; `drain()` releases after the
|
|
145
144
|
sockets are gone and before `hub.close()`.
|
|
146
|
-
- **Refusing
|
|
147
|
-
`
|
|
145
|
+
- **Refusing and draining are two phases**: `stopAccepting()` is `accept`, `drain()` + `stop()`
|
|
146
|
+
`close`; `listenSyncNode` unregisters both on `stop()`.
|
|
148
147
|
- **A hub that closed opens nothing**: `close()` sets `#closed` before the walk; `#open` closes a
|
|
149
148
|
late subscription and RAISES `X_TRANSPORT_UNAVAILABLE`. `#release` takes the reserved bridge, never
|
|
150
149
|
a name.
|
|
@@ -154,7 +153,7 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
154
153
|
the asker and the list. `x dev` keeps the reached-on origin (`admitReachedOrigin`) and adds
|
|
155
154
|
its web role's beside a declared list; a container adds nothing. `AcceptBudget` is reserved before
|
|
156
155
|
`authenticate`, `refund()`ed on every exit that takes no socket.
|
|
157
|
-
- **A socket's actor comes from `
|
|
156
|
+
- **A socket's actor comes from `syncNode({ authenticate })` only**, run before
|
|
158
157
|
`server.upgrade`. `null` = 401 `X_SOCKET_UNAUTHENTICATED`; a throw = 503
|
|
159
158
|
`X_SOCKET_AUTH_UNAVAILABLE`. Absent = anonymous, and `start()` warns. The actor lives only in the
|
|
160
159
|
`GrantBook`.
|
|
@@ -162,18 +161,15 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
162
161
|
released on every path that never opens — `onUngranted` is REQUIRED on `UpgradeDeps`, and a
|
|
163
162
|
`server.upgrade` that THROWS releases too. `sync-node-auth.test.ts`'s harness opens inside
|
|
164
163
|
`upgrade()`, as Bun does.
|
|
165
|
-
- **A grant expires; a socket does not**: expired grants are re-decided on an interval
|
|
166
|
-
`hub.onActorChange
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
a dropped presence roster is logged (`sync.presence_roster_dropped`); `drain()` returns
|
|
170
|
-
`DrainedSocket[]` with `notified` and logs `sync.drain_frames_dropped`.
|
|
164
|
+
- **A grant expires; a socket does not**: expired grants are re-decided on an interval
|
|
165
|
+
(`hub.onActorChange`, `registry.reauthorize`). No `refresh` = close `1008`; one that raises retries.
|
|
166
|
+
- **Every `socket.send` reads its answer**: a dropped subscribe reply marks it desynced; a dropped
|
|
167
|
+
roster logs `sync.presence_roster_dropped`; `drain()` returns `DrainedSocket[]` (`notified`).
|
|
171
168
|
- **Inbound frames run in a lane, never the socket**: `subscribe` is `sub:<sid>` or `topic:<name>`,
|
|
172
169
|
everything else unlaned (`frame-lanes.ts`); a lane exists only while work is queued.
|
|
173
170
|
- **A dropped `records` frame is counted AND repaired**: per-node `seq`/`epoch` per channel; a refused
|
|
174
|
-
frame marks the socket gapped, and the
|
|
175
|
-
(`channel_replay_gaps_total`
|
|
176
|
-
channel's `catchUp`, holding frames meanwhile.
|
|
171
|
+
frame marks the socket gapped, and the `drain` handler sends `replay-gap`
|
|
172
|
+
(`channel_replay_gaps_total`). The client re-runs `catchUp`, holding frames meanwhile.
|
|
177
173
|
- **A channel is a DECLARATION**: `channel(name, { params, policy, catchUp, records?, events? })`.
|
|
178
174
|
Deny by default, and `policy` is REQUIRED (`X_CHANNEL_DECLARATION_INVALID`; a public channel says
|
|
179
175
|
`policy: allow('public')`). Presence rides `events: true` channels.
|
|
@@ -182,14 +178,17 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
182
178
|
- **A seat is decided for the actor ON the socket when the guard resolves** (`#settle`); a denied
|
|
183
179
|
re-ask on a suspended seat is refused and latched. The latch is capped at `maxTopicsPerSocket`.
|
|
184
180
|
- **A missed change reopens every records topic** (`ChannelHub.invalidate()`, beside both
|
|
185
|
-
`registry.invalidate()` sites).
|
|
186
|
-
- **A
|
|
181
|
+
`registry.invalidate()` sites; `sync-bus-handlers.test.ts`).
|
|
182
|
+
- **A `recordPublisher` change (`source: 'publisher'`) feeds channels ONLY** — no commit position:
|
|
183
|
+
never `registry.deliver`, its gap stales no window; a new publisher is no gap. One producer KIND
|
|
184
|
+
per table per node (`ProducerKinds`, `X_REALTIME_PRODUCER_CONFLICT`); the x dev bridge skips a
|
|
185
|
+
claimed table (`isPublishedTable`). `ChangeEvent.table` is the relation on every producer.
|
|
187
186
|
- **An error never renders a credential**: `parsePgUrl`/`parseNatsUrl` name the variable.
|
|
188
187
|
|
|
189
188
|
## Replication
|
|
190
189
|
|
|
191
190
|
- Exactly one `replicator` per DB, by a session-level advisory lock. **`start()` and
|
|
192
|
-
`
|
|
191
|
+
`PostgresAdvisoryLock.tryAcquire` are MEMOISED**; `stop()`/`release()` await the in-flight one. A start
|
|
193
192
|
that FAILS hands the lock back; `running` is set once the feed pumps.
|
|
194
193
|
- **The pump has one way out, `#die`**: failure recorded, timer stopped, connection closed, then
|
|
195
194
|
`onEnd(reason)`. `start()` awaits the previous pump. **`stop()` releases everything first.**
|
|
@@ -206,7 +205,7 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
206
205
|
entity table, or `ALTER … ADD TABLE` the missing; never a DROP, never `FOR ALL TABLES`.
|
|
207
206
|
- **TLS is libpq's `sslmode`** (`pg-tls.ts`): only `verify-*` verify; the runtime never rejects,
|
|
208
207
|
`judgeHandshake` decides; the raw socket is DEAF after `upgradeTLS`.
|
|
209
|
-
- **Every session pins `datestyle=ISO`, `intervalstyle=postgres`, `extra_float_digits=3`**.
|
|
208
|
+
- **Every session pins `datestyle=ISO`, `intervalstyle=postgres`, `extra_float_digits=3`, `TimeZone=UTC`**.
|
|
210
209
|
- A change lsn is `<16 hex commit position><8 hex row position>`; never order by one half, a wall
|
|
211
210
|
time or a counter.
|
|
212
211
|
- Slot, publication and entity names match `[a-z_][a-z0-9_]*` before interpolation — a security
|
|
@@ -221,14 +220,14 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
221
220
|
optimistic writes, REPLAYED over synced truth whenever it moves. Installed as core's `RecordSink` on
|
|
222
221
|
`pageClient().store`.
|
|
223
222
|
- **Page state lives on `globalThis[Symbol.for('ultimate.realtime')]` (`page-store.ts`)**, never module
|
|
224
|
-
scope — every island is its own bundle (`page-client.test.ts` builds
|
|
223
|
+
scope — every island is its own bundle (`page-client.test.ts` builds two islands). Never
|
|
225
224
|
`instanceof` across copies.
|
|
226
225
|
- **The signal factory is per BUNDLE**: `installRealtime({ signal, sync })` (`reactivity.ts`). No
|
|
227
226
|
install and a DOM is `X_REALTIME_UNINSTALLED`; no DOM is a server render — hooks answer
|
|
228
227
|
`pending`/online and create no page state. `useRecord` and `useQuery` return `AsyncState` accessors.
|
|
229
|
-
- **
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
- **The page runtime (`page-runtime.ts`) is installed once per page, never in an island**;
|
|
229
|
+
hooks call `installedPage(hook).services`. One socket per page (`page-socket.ts`);
|
|
230
|
+
`browser-socket.ts` holds the only `new WebSocket`. `rescope` clears the store, redials.
|
|
232
231
|
- **One socket per ORIGIN and principal, in a SharedWorker**: `socket-engine.ts` holds it and talks
|
|
233
232
|
to tabs over `MessagePort`s (`socket-host.ts`'s virtual socket); wants are ref-counted and frames
|
|
234
233
|
routed; a port silent for `REAP_AFTER_BEATS` is reaped; each tab resubscribes from its OWN cursors.
|
|
@@ -239,27 +238,27 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
239
238
|
- **`LiveClient` holds no signal** — plain reads plus `onStatus` / `onChange`; hooks wrap them.
|
|
240
239
|
- **An answer that did not carry a touched row keeps that row's overlay** until the server's next row
|
|
241
240
|
for it, bounded by `DEFAULT_AWAIT_SERVER_MS` (10 s).
|
|
242
|
-
- **A
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
- **A frame names the write that produced it**: `records.write`, `patch.writes` (a live patch
|
|
242
|
+
one, a resume each it replays, off `RowPatch.write` in the ring) — `writeDigest(key)`, from a
|
|
243
|
+
REQUIRED `ChangeEvent.write: string | null`. `RecordStore.push` digests each overlay key first
|
|
244
|
+
(`record-names.ts`); `client-channels.ts` / `live-rows.ts` settle in the frame's own batch. A
|
|
245
|
+
non-digest is a protocol error, dropped on the bus. Partly confirmed rows go to
|
|
246
|
+
`OverlayEntry.confirmed`. `write-echo.test.ts`, `live-write-echo.test.ts`.
|
|
247
247
|
- `local(tx, input)` is pure and CONVERGENT: no I/O, no `Date.now()`, no `Math.random()`.
|
|
248
248
|
- **One conflict vocabulary**: `ConflictPolicy` from `@ultimat3/core`, resolved by core's
|
|
249
249
|
`resolveConflict(overlayRow, serverRow)` in `RecordStore.settle`. A server delete is not a conflict.
|
|
250
250
|
- Anything a component reads is a getter or accessor. `MutatorLike.local` uses method syntax.
|
|
251
251
|
`useQuery`'s input is read once, at call time.
|
|
252
|
-
-
|
|
253
|
-
|
|
254
|
-
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
`X_LOCAL_STORE_UNAVAILABLE
|
|
262
|
-
when its ack is written.
|
|
252
|
+
- **`useRecords(type, selection)` holds the TYPE** (`retainType`): `ready` at once, recomputed per
|
|
253
|
+
change to that type, no row of it evicted while held; the last type hold evicts what it spared.
|
|
254
|
+
- Every subscription handle is `Disposable`; `[Symbol.dispose]` IS the release. Type claims go in
|
|
255
|
+
`type-pins.ts`.
|
|
256
|
+
- **The outbox**: only `pending` is sendable; `drain()` is one chained pass at a time; `#epoch` is
|
|
257
|
+
bumped before the requeue scan, read every pass. `#persist` hands SNAPSHOTS by KEY; one tab drains
|
|
258
|
+
(`navigator.locks`), resetting a stored `inflight`. A `rescope` abandons the queue and bumps the
|
|
259
|
+
epoch synchronously; `enqueue` decides WHOSE queue before its first await
|
|
260
|
+
(`X_OFFLINE_QUEUE_ABANDONED`); `useMutation` fences its POST on its scope epoch. A refused disk is
|
|
261
|
+
warned `X_LOCAL_STORE_UNAVAILABLE`, never rejecting `ready`. An entry leaves disk at its ack.
|
|
263
262
|
|
|
264
263
|
## The client connection
|
|
265
264
|
|
|
@@ -285,12 +284,10 @@ Tier 3 package. Channels, live queries, local-first sync. One protocol for all t
|
|
|
285
284
|
|
|
286
285
|
## The page boot
|
|
287
286
|
|
|
288
|
-
Browser bytes per hook, as last measured: `docs/history/realtime.md`. Re-measure before quoting.
|
|
289
|
-
|
|
290
287
|
- **The disk boot is ONE page script (`boot.ts`, `./boot`)**, served at `/_x/page-boot/<hash>.js`
|
|
291
|
-
on a document with the scope tag and a realtime island; it wipes every other
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
on a document with the scope tag and a realtime island; it installs the runtime, wipes every other
|
|
289
|
+
principal's stored scope (read at the wipe), then restores. `booted` reads its promise off
|
|
290
|
+
`globalThis[Symbol.for('ultimate.page-boot')]`. No boot: islands load its chunk.
|
|
294
291
|
- **An island never carries the outbox**: `boot.ts` builds it; `useMutation` and the page socket read
|
|
295
292
|
it through `outbox-slot.ts` after `page.booted`. No boot ⇒ a refused write is rejected, not queued.
|
|
296
293
|
|
|
@@ -309,7 +306,7 @@ Browser bytes per hook, as last measured: `docs/history/realtime.md`. Re-measure
|
|
|
309
306
|
## Commands
|
|
310
307
|
|
|
311
308
|
```
|
|
312
|
-
bun test packages/realtime/src
|
|
309
|
+
bun test packages/realtime/src # from the REPO ROOT
|
|
313
310
|
bun run typecheck
|
|
314
311
|
```
|
|
315
312
|
|
|
@@ -320,4 +317,5 @@ without one) and bumping `PROTOCOL_VERSION` **when an old frame becomes unreadab
|
|
|
320
317
|
direction**. `decode` is a whitelist: an additive optional field, or removing a field read through
|
|
321
318
|
`list()`, is free; removing a field read through `str()`/`num()` (which throw) is a bump.
|
|
322
319
|
|
|
323
|
-
Why each rule
|
|
320
|
+
Why each rule is shaped so, and browser bytes per hook (re-measure before quoting):
|
|
321
|
+
[`docs/history/realtime.md`](../../docs/history/realtime.md).
|