@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.
Files changed (88) hide show
  1. package/CLAUDE.md +61 -63
  2. package/README.md +131 -46
  3. package/package.json +9 -6
  4. package/src/advisory-lock.ts +10 -5
  5. package/src/boot.ts +14 -9
  6. package/src/changefeed-env.ts +8 -8
  7. package/src/changefeed.ts +42 -18
  8. package/src/channel-authz.ts +7 -7
  9. package/src/channel-describe.ts +1 -1
  10. package/src/channel-logs.ts +16 -25
  11. package/src/channel-presence.ts +3 -3
  12. package/src/channel-records.ts +3 -3
  13. package/src/channel-ref.ts +2 -2
  14. package/src/channel-registry.ts +2 -2
  15. package/src/channel.ts +8 -7
  16. package/src/client-channels.ts +2 -0
  17. package/src/client-frames.ts +9 -2
  18. package/src/client.ts +29 -2
  19. package/src/error-titles.ts +91 -0
  20. package/src/errors.ts +47 -90
  21. package/src/first-paint-hold.ts +49 -0
  22. package/src/idb-types.ts +2 -2
  23. package/src/index.ts +24 -6
  24. package/src/json.ts +16 -3
  25. package/src/live-contract.ts +14 -0
  26. package/src/live-definition.ts +12 -2
  27. package/src/live-deliver.ts +55 -0
  28. package/src/live-fanout.ts +81 -24
  29. package/src/live-query-options.ts +6 -0
  30. package/src/live-query.ts +66 -144
  31. package/src/live-reauth.ts +127 -0
  32. package/src/live-replicator.ts +28 -8
  33. package/src/live-resume.ts +44 -8
  34. package/src/live-rows.ts +32 -3
  35. package/src/live-spend.ts +50 -0
  36. package/src/local-store-idb.ts +5 -0
  37. package/src/matcher-bridge.ts +11 -5
  38. package/src/nats-client.ts +26 -4
  39. package/src/nats-lib-client.ts +36 -7
  40. package/src/nats-subscriptions.ts +63 -0
  41. package/src/nats-transport.ts +127 -15
  42. package/src/offline-queue.ts +5 -0
  43. package/src/page-errors.ts +8 -9
  44. package/src/page-outbox.ts +5 -5
  45. package/src/page-runtime-wait.ts +72 -0
  46. package/src/page-runtime.ts +32 -0
  47. package/src/page-socket.ts +7 -16
  48. package/src/page-store.ts +83 -16
  49. package/src/pg-advisory-lock.ts +9 -4
  50. package/src/pg-connection.ts +7 -1
  51. package/src/pg-replication.ts +6 -6
  52. package/src/pg-socket.ts +20 -3
  53. package/src/pg-values.ts +29 -14
  54. package/src/policy-gate.ts +7 -7
  55. package/src/presence.ts +2 -2
  56. package/src/principal-sockets.ts +75 -0
  57. package/src/query-window.ts +1 -1
  58. package/src/record-publisher.ts +178 -0
  59. package/src/record-store.ts +25 -4
  60. package/src/record-synced.ts +43 -1
  61. package/src/replication-errors.ts +3 -3
  62. package/src/replicator-envelope.ts +61 -11
  63. package/src/replicator-row.ts +1 -1
  64. package/src/replicator.ts +12 -23
  65. package/src/server.ts +36 -18
  66. package/src/socket-defaults.ts +20 -0
  67. package/src/socket.ts +14 -19
  68. package/src/subscription-book.ts +129 -71
  69. package/src/subscription-count.ts +151 -0
  70. package/src/sync-actor-change.ts +56 -0
  71. package/src/sync-bus-handlers.ts +131 -0
  72. package/src/sync-frames.ts +1 -1
  73. package/src/sync-node-bounds.ts +1 -1
  74. package/src/sync-node-contract.ts +16 -2
  75. package/src/sync-node.ts +115 -109
  76. package/src/sync-origin.ts +1 -1
  77. package/src/sync-protocol.ts +73 -13
  78. package/src/sync-upgrade.ts +39 -0
  79. package/src/type-pins.ts +9 -1
  80. package/src/use-channel.ts +19 -14
  81. package/src/use-connection.ts +3 -3
  82. package/src/use-mutation.ts +9 -16
  83. package/src/use-query.ts +11 -11
  84. package/src/use-record.ts +89 -10
  85. package/src/wire-channel.ts +5 -5
  86. package/src/wire-read.ts +2 -2
  87. package/src/idb-fake.ts +0 -133
  88. 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 `guard` | a second authz path of any kind |
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
- - **`errors.ts` is the code table plus the client-reachable refusals**; `realtime-error.ts` holds
25
- the base class alone (an `extends` must not read it in a TDZ) and `replication-errors.ts` the
26
- Postgres ones. Every name is still exported from `./errors`. A browser path loads `page-errors.ts`,
27
- never the table (`page-errors-bundle.test.ts`).
28
- - **`sideEffects` is the ARRAY `["./src/errors.ts"]`**, never `false` (drops `registerErrorCodes()`)
29
- and never absent. `bun run side-effects` is the check.
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` reads zero here but cannot see an option with no `??` default, so audit
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 `createSyncNode`; `SyncSocket` keeps its own
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 `maxPerTenant` in one synchronous step;
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 the windows reading its entity** (`invalidate(entity)`, from
107
- `onBulk`). **A stale window's re-read re-snapshots EVERY subscriber**, before the lsn guard and
108
- on a non-matching change too (`live-replicator-bulk.test.ts`, `live-fanout.test.ts`).
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 and re-subscription are the
126
- library's — `NatsTransport` never grows subscription bookkeeping. Above the port: the herd jitter
127
- and presence's KV semantics (`nats-jetstream.ts`, `nats-kv.ts`). `nats-fake.ts` has server semantics.
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()`** (a quarter of `idleTimeoutMs`, floored at 1 s, `.unref()`)
142
- and measures on `Clock.monotonic()` (`lastSeenMonotonicMs`). `SocketRegistry.idle()` is a query;
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 new sockets and draining are two phases**: `stopAccepting()` is `accept`, `drain()` +
147
- `stop()` are `close`. `listenSyncNode` unregisters both on `stop()`.
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 `createSyncNode({ authenticate })` only**, run before
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 through
166
- `hub.onActorChange` and `registry.reauthorize`. No `refresh` = close with `1008`; a `refresh` that
167
- raises keeps the socket and retries.
168
- - **Every `socket.send` reads its answer**: a dropped subscribe reply marks the subscription desynced;
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 websocket `drain` handler sends `replay-gap`
175
- (`channel_replay_gaps_total` beside `channel_frames_dropped_total`). The client re-runs the
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 channel patch id carries the node** (`nodeId`, default a per-hub `uuid()`).
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
- `PgAdvisoryLock.tryAcquire` are MEMOISED**; `stop()`/`release()` await the in-flight one. A start
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 the package twice). Never
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
- - **One socket per page, built by the first live hook (`page-socket.ts`)**; `browser-socket.ts`
230
- holds the framework's only `new WebSocket`. `hasPageSocket()` lives in `page-store.ts`. A principal
231
- change (`rescope`) clears the store and redials.
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 `records` frame names the write that produced it** (`ChannelRecordsFrame.write =
243
- writeDigest(idempotencyKey)`, never the key). `RecordStore.push` digests every overlay key before
244
- the request leaves (`record-names.ts`); `client-channels.ts` settles inside the frame's own batch.
245
- A non-digest `write` is a protocol error (`wire-channel.ts`) and dropped on the bus. Rows an answer
246
- or echo partly confirmed go to `OverlayEntry.confirmed` and stop painting. `write-echo.test.ts`.
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
- - Every subscription handle (`LiveHandle`, the hook accessors, `Unsubscribe`) is `Disposable`, and
253
- `[Symbol.dispose]` is the same function as the release. Type claims go in `type-pins.ts`.
254
- - **The outbox**: only `pending` is sendable; `drain()` is one chained pass at a time. `#epoch` is
255
- bumped before the requeue scan and read every pass iteration. `#persist` hands the store
256
- SNAPSHOTS by KEY; one tab drains at a time (`navigator.locks`) and resets a stored `inflight`
257
- under the lock. A `rescope` abandons the queue, clears the single-flight slot and bumps the
258
- outbox's epoch synchronously; `enqueue` decides WHOSE queue before its first await and refuses
259
- otherwise (`X_OFFLINE_QUEUE_ABANDONED`), and `useMutation` fences its own await (the POST) on the
260
- scope epoch it was called under. A wipe or open the disk refuses is warned
261
- `X_LOCAL_STORE_UNAVAILABLE` and never rejects `ready`. At-least-once: an entry leaves the disk
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 principal's stored
292
- scope — the scope kept is read at the wipe, after the disk opens, never at boot start — then
293
- restores. `booted` reads its promise off `globalThis[Symbol.for('ultimate.page-boot')]`.
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 # from the REPO ROOT, never from packages/realtime
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 above is shaped the way it is: [`docs/history/realtime.md`](../../docs/history/realtime.md).
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).