@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 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.0",
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.3.0",
54
- "@ultimat3/entity": "30.3.0",
55
- "@ultimat3/policy": "30.3.0",
56
- "@ultimat3/query": "30.3.0",
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
+ }
@@ -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 { deferredScriptsPending, peekPageRealtime } from './page-store';
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
+ }
@@ -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
- teardowns.add(() => {
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 has no page to reset.
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
- for (const teardown of teardowns) teardown();
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 KEY: unique symbol = Symbol.for('ultimate.realtime');
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 = { [KEY]?: PageRealtime };
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[KEY];
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, KEY, { value: created, configurable: true });
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)[KEY];
184
+ return (globalThis as Host)[PAGE_KEY];
179
185
  }
180
186
 
181
187
  /**