@ultimat3/realtime 30.3.0 → 30.5.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/README.md +6 -1
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/page-reset.ts +28 -0
- package/src/page-runtime-wait.ts +12 -1
- package/src/page-socket.ts +15 -5
- package/src/page-store.ts +11 -5
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ class — stay on `.`.
|
|
|
86
86
|
| wire | `.` | `PROTOCOL_VERSION` (3), `encode`, `decode`, `Frame` |
|
|
87
87
|
| the node | `./server` | `syncNode` / `listenSyncNode` (`sync` role) |
|
|
88
88
|
| a socket's identity | `./server` | `SyncAuthenticator`, `SyncGrant`, `GrantBook`, `sweepGrants`, `DEFAULT_REAUTH_INTERVAL_MS` |
|
|
89
|
-
| hooks | `.` | `useQuery` (and `pollEvery` for its `poll`), `useRecord`, `useRecords` (by key, or a whole type: `RecordSelection`), `useRecordChanges`, `useMutation`, `useMutationQueue`, `useOutbox`, `useConnection`, `useChannel`, `usePresence`, `hasPageSocket`, `installRealtime` |
|
|
89
|
+
| hooks | `.` | `useQuery` (and `pollEvery` for its `poll`), `useRecord`, `useRecords` (by key, or a whole type: `RecordSelection`), `useRecordChanges`, `useMutation`, `useMutationQueue`, `useOutbox`, `useConnection`, `useChannel`, `usePresence`, `hasPageSocket`, `installRealtime`, `resetPageRealtime` (tests) |
|
|
90
90
|
| channel records, no replicator | `./server` | `recordPublisher` — an app's committed rows as channel `records`, sequenced per producer ([below](#channel-records-with-no-replicator)) |
|
|
91
91
|
| channels | `.` | `channel`, `channelRef`, `ChannelHandle`, `topic`, `readPresence`, the channel frame types |
|
|
92
92
|
| first paint | `.` | `holdFirstPaint`, `FIRST_PAINT_HOLD_MS` (1 s) — what the island bootstrap awaits before `mount`, for an island that reads the store or the outbox: the boot's restore and the open outbox, capped (#506). An island taking only `useChannel` / `usePresence` / `useConnection` never awaits it and is never hidden, nor is one taking `useQuery` / `useRecord` in an app that declares no `persist: true` entity |
|
|
@@ -304,6 +304,11 @@ state; a `useMutation` call refuses with `X_LIVE_SERVER_RENDER`. With a DOM and
|
|
|
304
304
|
hook is `X_REALTIME_UNINSTALLED`. `hasPageSocket()` is the guard a component with a static fallback
|
|
305
305
|
asks.
|
|
306
306
|
|
|
307
|
+
**Between island tests**, `resetPageRealtime()` (`.`) is the one reset: it closes the page socket —
|
|
308
|
+
whichever bundle's copy built it, through the page object — and drops the page object, the outbox
|
|
309
|
+
and the boot's promise. A mounted chunk carries its own copy of this package, so a test never reaches
|
|
310
|
+
into `Symbol.for('ultimate.realtime')` itself. Tests only; a page's state ends with its tab.
|
|
311
|
+
|
|
307
312
|
## Who a socket is
|
|
308
313
|
|
|
309
314
|
`syncNode({ authenticate })` is the one place a websocket gets an identity. It runs on the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/realtime",
|
|
3
|
-
"version": "30.
|
|
3
|
+
"version": "30.5.0",
|
|
4
4
|
"description": "Three-tier realtime: channels, live queries, local-first sync — one protocol, one mutator shape",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ultimate",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"test": "bun test"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@ultimat3/core": "30.
|
|
54
|
-
"@ultimat3/entity": "30.
|
|
55
|
-
"@ultimat3/policy": "30.
|
|
56
|
-
"@ultimat3/query": "30.
|
|
53
|
+
"@ultimat3/core": "30.5.0",
|
|
54
|
+
"@ultimat3/entity": "30.5.0",
|
|
55
|
+
"@ultimat3/policy": "30.5.0",
|
|
56
|
+
"@ultimat3/query": "30.5.0",
|
|
57
57
|
"nats": "2.29.3"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/index.ts
CHANGED
|
@@ -135,6 +135,7 @@ export {
|
|
|
135
135
|
type PageOutbox,
|
|
136
136
|
pageOutbox,
|
|
137
137
|
} from './page-outbox';
|
|
138
|
+
export { resetPageRealtime } from './page-reset';
|
|
138
139
|
export { hasPageSocket, type SyncTarget } from './page-store';
|
|
139
140
|
export { pollEvery, type QueryPoll } from './query-poll';
|
|
140
141
|
export { installRealtime, type RealtimeInstall } from './reactivity';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The one test reset of a page's realtime state, for an app's island tests. An island test mounts
|
|
2
|
+
// the BUILT chunk, which carries its own copy of this package: module state the test's copy cannot
|
|
3
|
+
// see. So this acts on what every copy shares — the page object and the page globals on `globalThis`.
|
|
4
|
+
|
|
5
|
+
import { pageClient } from '@ultimat3/core/page';
|
|
6
|
+
import { OUTBOX_KEY } from './outbox-slot';
|
|
7
|
+
import { closePageSockets } from './page-socket';
|
|
8
|
+
import { BOOT_KEY, BOOT_RELEASE_KEY, PAGE_KEY, peekPageRealtime } from './page-store';
|
|
9
|
+
import { uninstallRealtime } from './reactivity';
|
|
10
|
+
|
|
11
|
+
/** Every page global the realtime client keeps: the page object, the outbox, the boot's promise. */
|
|
12
|
+
const PAGE_KEYS: readonly symbol[] = [PAGE_KEY, OUTBOX_KEY, BOOT_KEY, BOOT_RELEASE_KEY];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* For tests: the page as no island has touched it. Closes the page socket — whichever bundle's
|
|
16
|
+
* copy built it — drops the page object, the outbox and the boot's promise, and unseats the store
|
|
17
|
+
* and socket from core's page client, so the next case or file starts with no page. Never call it
|
|
18
|
+
* in a browser: a page's state ends with the tab. A host a test handed `resetPageSocket` stays.
|
|
19
|
+
*/
|
|
20
|
+
export function resetPageRealtime(): void {
|
|
21
|
+
peekPageRealtime()?.closeSocket?.();
|
|
22
|
+
closePageSockets();
|
|
23
|
+
uninstallRealtime();
|
|
24
|
+
for (const key of PAGE_KEYS) Reflect.deleteProperty(globalThis, key);
|
|
25
|
+
const handle = pageClient();
|
|
26
|
+
handle.store = undefined;
|
|
27
|
+
handle.socket = undefined;
|
|
28
|
+
}
|
package/src/page-runtime-wait.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// classic script); every other document — a static export, an anonymous page, an app with no sync
|
|
4
4
|
// node — gets it from the runtime chunk the island loads itself. Never both, never twice.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type { TransportRequest } from '@ultimat3/core/page';
|
|
7
|
+
import { deferredScriptsPending, installedPage, peekPageRealtime } from './page-store';
|
|
7
8
|
|
|
8
9
|
export interface AwaitPageRuntimeOptions {
|
|
9
10
|
/**
|
|
@@ -70,3 +71,13 @@ function firstOf(boot: Listenable): Promise<void> {
|
|
|
70
71
|
if (typeof addEventListener === 'function') addEventListener('load', done, { once: true });
|
|
71
72
|
});
|
|
72
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Core's `clientTransport`, as the page runtime installed it. What a realtime island's bundle sends
|
|
77
|
+
* through instead of carrying its own copy (`packages/cli/src/island-realtime.ts`): the island
|
|
78
|
+
* awaited the runtime before its module body ran, so the transport is already on the page.
|
|
79
|
+
* `X_REALTIME_UNINSTALLED` on a page with no runtime, never a silent second transport.
|
|
80
|
+
*/
|
|
81
|
+
export function runtimeSend(req: TransportRequest): Promise<unknown> {
|
|
82
|
+
return installedPage('clientTransport').services.send(req);
|
|
83
|
+
}
|
package/src/page-socket.ts
CHANGED
|
@@ -77,7 +77,9 @@ export function pageSocket(hook: string): LiveClient {
|
|
|
77
77
|
addEventListener('online', wake);
|
|
78
78
|
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', wake);
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
const teardown = (): void => {
|
|
81
|
+
teardowns.delete(teardown);
|
|
82
|
+
if (page.closeSocket === teardown) page.closeSocket = undefined;
|
|
81
83
|
offRescope();
|
|
82
84
|
if (listens) {
|
|
83
85
|
removeEventListener('pagehide', bye);
|
|
@@ -89,14 +91,17 @@ export function pageSocket(hook: string): LiveClient {
|
|
|
89
91
|
host.bye();
|
|
90
92
|
if (page.socket === client) page.socket = undefined;
|
|
91
93
|
if (pageClient().socket === client) pageClient().socket = undefined;
|
|
92
|
-
}
|
|
94
|
+
};
|
|
95
|
+
teardowns.add(teardown);
|
|
96
|
+
page.closeSocket = teardown;
|
|
93
97
|
return client;
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
/**
|
|
97
101
|
* Every socket this module built, as the undo of building it. A page builds one and never tears it
|
|
98
102
|
* down — the tab's end is its teardown — so only a test process, which builds one per case, calls
|
|
99
|
-
* `resetPageSocket`. Not on the barrel: an app
|
|
103
|
+
* `resetPageSocket`. Not on the barrel: an app's test calls `resetPageRealtime` (`page-reset.ts`),
|
|
104
|
+
* which reaches the socket through the page object, whichever bundle's copy built it.
|
|
100
105
|
*/
|
|
101
106
|
const teardowns = new Set<() => void>();
|
|
102
107
|
|
|
@@ -112,11 +117,16 @@ let hosts: (options: SocketHostOptions) => SocketHost = openHost;
|
|
|
112
117
|
export function resetPageSocket(
|
|
113
118
|
options: { readonly openHost?: (options: SocketHostOptions) => SocketHost } = {},
|
|
114
119
|
): void {
|
|
115
|
-
|
|
116
|
-
teardowns.clear();
|
|
120
|
+
closePageSockets();
|
|
117
121
|
hosts = options.openHost ?? openHost;
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
/** Unsubscribe, close and unseat every page socket THIS copy built; the host override stays. */
|
|
125
|
+
export function closePageSockets(): void {
|
|
126
|
+
for (const teardown of [...teardowns]) teardown();
|
|
127
|
+
teardowns.clear();
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
/** Where `hasPageSocket` lives, re-exported for the callers that reach it from here. */
|
|
121
131
|
export { hasPageSocket } from './page-store';
|
|
122
132
|
|
package/src/page-store.ts
CHANGED
|
@@ -39,6 +39,11 @@ export interface PageRealtime {
|
|
|
39
39
|
readonly booted: Promise<void>;
|
|
40
40
|
/** What a hook calls instead of bundling it — installed with the store, by the page runtime. */
|
|
41
41
|
services: PageServices | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Closes the page socket and unhooks its listeners — set by whichever bundle's copy built it, so
|
|
44
|
+
* a test reset in ANOTHER copy (`resetPageRealtime`, from an app's island test) can reach it.
|
|
45
|
+
*/
|
|
46
|
+
closeSocket: (() => void) | undefined;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|
|
@@ -68,7 +73,7 @@ export interface PageWrites {
|
|
|
68
73
|
readonly listeners: Set<() => void>;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
|
-
const
|
|
76
|
+
export const PAGE_KEY: unique symbol = Symbol.for('ultimate.realtime');
|
|
72
77
|
|
|
73
78
|
/**
|
|
74
79
|
* Where the boot script leaves its promise. The boot is ONE page-level script, not code in every
|
|
@@ -149,7 +154,7 @@ function bootComing(): boolean {
|
|
|
149
154
|
return document.querySelector(`meta[name="${CLIENT_SCOPE_META}"]`) !== null;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
type Host = { [
|
|
157
|
+
type Host = { [PAGE_KEY]?: PageRealtime };
|
|
153
158
|
|
|
154
159
|
/**
|
|
155
160
|
* Get-or-create the page object — never its runtime: an island that asks before the runtime is
|
|
@@ -157,7 +162,7 @@ type Host = { [KEY]?: PageRealtime };
|
|
|
157
162
|
*/
|
|
158
163
|
export function pageRealtime(): PageRealtime {
|
|
159
164
|
const host = globalThis as Host;
|
|
160
|
-
const existing = host[
|
|
165
|
+
const existing = host[PAGE_KEY];
|
|
161
166
|
if (existing !== undefined) return existing;
|
|
162
167
|
const created: PageRealtime = {
|
|
163
168
|
store: undefined,
|
|
@@ -168,14 +173,15 @@ export function pageRealtime(): PageRealtime {
|
|
|
168
173
|
return bootedPromise();
|
|
169
174
|
},
|
|
170
175
|
services: undefined,
|
|
176
|
+
closeSocket: undefined,
|
|
171
177
|
};
|
|
172
|
-
Object.defineProperty(host,
|
|
178
|
+
Object.defineProperty(host, PAGE_KEY, { value: created, configurable: true });
|
|
173
179
|
return created;
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
/** The page state when some island already made it — never creates one (a server render must not). */
|
|
177
183
|
export function peekPageRealtime(): PageRealtime | undefined {
|
|
178
|
-
return (globalThis as Host)[
|
|
184
|
+
return (globalThis as Host)[PAGE_KEY];
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
/**
|