@solidjs/router 2.0.0-next.16 → 2.0.0-next.18

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
@@ -20,7 +20,7 @@ Explore the official [documentation](https://docs.solidjs.com/solid-router) for
20
20
  - **Plain Anchors**: no link component — `<a>` elements get `aria-current`, `data-active`, and `data-pending` automatically via compiler-claimed anchors
21
21
  - **Universal Rendering**: one factory for browser, hash, memory, and server rendering; history adapters are imports, so unused ones never enter your bundle
22
22
  - **Preload Functions**: parallel data fetching following the render-as-you-fetch pattern, triggered eagerly on link hover/focus
23
- - **Data APIs with Caching**: `query` and `action` with deduplication, revalidation, single-flight mutations, and progressive enhancement
23
+ - **Data APIs with Caching**: `query` and `action` with deduplication, revalidation, single-flight mutations, and progressive enhancement — plus experimental `liveQuery` for keyed queries over live streams
24
24
  - **Typed Search Params**: opt-in per-route [Standard Schema](https://github.com/standard-schema/standard-schema) validation — `search.page` is a `number`, not `"2"`
25
25
 
26
26
  ## Table of Contents
@@ -519,6 +519,38 @@ getUser.keyFor(5); // "users[5]"
519
519
 
520
520
  Revalidate with the `revalidate` export or by setting `revalidate` keys on action responses — the whole key invalidates every entry for the query, `keyFor` invalidates one.
521
521
 
522
+ ### `liveQuery` (experimental)
523
+
524
+ `query`'s live sibling: a keyed query over a value-shaped stream. The function is an async iterable (typically an async generator server function) whose yields are successive **values of one logical query** — each yield is the current state, not an event — with the contract that it re-yields current state on every invocation:
525
+
526
+ ```tsx
527
+ const roomMessages = liveQuery(async function* (room: string) {
528
+ "use server";
529
+ yield await db.messages.list(room); // current state, immediately
530
+ for await (const change of db.messages.watch(room)) {
531
+ yield await db.messages.list(room); // current state again, on change
532
+ }
533
+ }, "messages");
534
+ ```
535
+
536
+ Declaring it is what makes it live — no separate wrapper, and server functions are declared GET at creation just like `query`. Consumption is the same story as `query`: Solid primitives, no router-specific async wrapper. The value updates in place as yields arrive:
537
+
538
+ ```tsx
539
+ const messages = createMemo(() => roomMessages(params.room));
540
+ return <For each={messages()}>{msg => <Message {...msg} />}</For>;
541
+ ```
542
+
543
+ One connection per key (name + arguments) is shared by every consumer: late subscribers get the latest value immediately, delivery is latest-wins, and the connection closes when the last consumer leaves. A connected stream that dies transiently (network, 5xx) reconnects with exponential backoff while the latest value keeps serving; a definite rejection (4xx) or a first-connect failure surfaces to the consumer like any thrown error.
544
+
545
+ Live queries ride the router's existing machinery rather than adding their own:
546
+
547
+ - **Preload** — calling one in a preload function warms the connection, so navigation renders against an already-delivered value instead of holding the transition on connect.
548
+ - **SSR** — the document face renders the first value; hydration adopts it and reconnects. Server-side, consumers of a key within one request observe the same value.
549
+ - **Revalidation** — explicit `revalidate(key)` reconnects (the producer re-yields current state by contract). The post-mutation sweep leaves healthy connections alone: the stream is its own freshness mechanism.
550
+ - **Single-flight** — a mutation's flight payload pushes straight into open channels, so mutated state lands in the same round trip.
551
+
552
+ The callable carries the `query` conventions (`key`, `keyFor`) plus a reactive `status(...args)` read (`"idle" | "connecting" | "connected" | "reconnecting" | "closed"`) for surfacing connection state in UI.
553
+
522
554
  ### `action`
523
555
 
524
556
  A router action is *an action with a URL* — Solid's mutation primitive plus URL addressability, submission tracking, and response handling. Data helpers come from the router; response helpers (`redirect`, `reload`) come from `@solidjs/web` — they're protocol-level and work without the router:
@@ -7,7 +7,7 @@ import { decodeFlashCookie } from "@solidjs/web/server-functions/server";
7
7
  import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
8
8
  import { setRouterFormHandler } from "./events.js";
9
9
  import { mockBase, setFunctionName } from "../utils.js";
10
- import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
10
+ import { cacheKeyOp, deliverFlightData, hashKey, revalidate, query } from "./query.js";
11
11
  const submitHooksSymbol = Symbol("routerActionSubmitHooks");
12
12
  const settledHooksSymbol = Symbol("routerActionSettledHooks");
13
13
  const invokeSymbol = Symbol("routerActionInvoke");
@@ -328,8 +328,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
328
328
  }
329
329
  // invalidate
330
330
  cacheKeyOp(keys, entry => (entry[0] = 0));
331
- // set cache
331
+ // set cache — and fan the payload out to other keyed stores (live query
332
+ // channels adopt delivered values instead of reconnecting)
332
333
  flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
334
+ flightData && deliverFlightData(flightData);
333
335
  // trigger revalidation inside the same transition as the navigation, so
334
336
  // the redirect commits atomically with fresh data: surviving consumers
335
337
  // (shared layouts) the flight payload didn't seed refetch and hold the
@@ -1,2 +1,3 @@
1
1
  export { action, useSubmissions, useAction, type Action } from "./action.js";
2
2
  export { query, revalidate, type CachedFunction } from "./query.js";
3
+ export { liveQuery, type LiveFunction, type LiveQueryStatus } from "./liveQuery.js";
@@ -1,2 +1,3 @@
1
1
  export { action, useSubmissions, useAction } from "./action.js";
2
2
  export { query, revalidate } from "./query.js";
3
+ export { liveQuery } from "./liveQuery.js";
@@ -0,0 +1,44 @@
1
+ export type LiveQueryStatus = "idle" | "connecting" | "connected" | "reconnecting" | "closed";
2
+ export type LiveFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ((...args: A) => AsyncIterable<Awaited<R> extends AsyncIterable<infer V> ? V : Awaited<R>>) & {
3
+ keyFor: (...args: A) => string;
4
+ key: string;
5
+ status: (...args: A) => LiveQueryStatus;
6
+ } : never;
7
+ /**
8
+ * Declares a keyed live query over a value-shaped stream: an async-iterable
9
+ * producer whose yields are successive VALUES of one logical query, with the
10
+ * contract that the producer re-yields current state on every invocation.
11
+ * liveQuery IS the live layer — no separate declaration needed:
12
+ *
13
+ * - One connection per (name + args) key, shared by every consumer. Late
14
+ * subscribers receive the latest value immediately, delivery is
15
+ * latest-wins, and the connection closes when the last consumer leaves.
16
+ * - A connected stream that dies transiently (network, 5xx) reconnects with
17
+ * exponential backoff — the latest value keeps serving meanwhile. A
18
+ * definite rejection (4xx: the transport stamps HTTP statuses onto
19
+ * failures) ends the channel and surfaces the error to consumers, as does
20
+ * a first-connect failure.
21
+ * - Server functions are declared GET at creation (like `query`). Live
22
+ * queries participate in single-flight on the delivery side: a mutation's
23
+ * flight payload pushes straight into open channels (the mutation
24
+ * response is the round trip), while the post-mutation sweep leaves the
25
+ * healthy connection in place — the live stream stays authoritative.
26
+ * - Calling under preload intent warms the channel (a temporary hold keeps
27
+ * it open through the preload window), so navigation renders against an
28
+ * already-connected stream instead of holding the transition on connect
29
+ * plus first yield. Outside preload, calling remains free — only a real
30
+ * pull connects.
31
+ * - SSR: the document face renders the first value; hydration adopts it and
32
+ * reconnects. Channels are request-scoped on the server, so every
33
+ * consumer of a key in one render observes the same value. Explicit
34
+ * `revalidate(key)` reconnects (the producer re-yields current state on
35
+ * invocation), and `status(...args)` is a reactive read of the channel's
36
+ * wire state.
37
+ *
38
+ * ```ts
39
+ * const roomMessages = liveQuery(getMessages, "messages");
40
+ * // in a component — one connection, however many consumers:
41
+ * const messages = createMemo(() => roomMessages(props.room));
42
+ * ```
43
+ */
44
+ export declare function liveQuery<T extends (...args: any) => any>(fn: T, name: string): LiveFunction<T>;
@@ -0,0 +1,377 @@
1
+ import { createSignal } from "solid-js";
2
+ import { getRequestEvent, getServerFunctionMetadata, getServerFunctionRPC, isServer, isServerFunction } from "@solidjs/web";
3
+ import { getIntent } from "../routing.js";
4
+ import { hashKey, matchKey, registerFlightDataHook, registerRevalidateHook } from "./query.js";
5
+ // Matches query's preload lifetime: a channel warmed by preload intent
6
+ // stays held this long waiting for its navigation.
7
+ const PRELOAD_TIMEOUT = 5000;
8
+ // The live-source brand (registered symbol — no transport import needed):
9
+ // computations meeting a branded iterable apply live SSR policy (document
10
+ // face takes the first value; hydration re-runs the compute after adoption
11
+ // to reconnect). liveQuery writes it on both faces itself — the server-side
12
+ // resolved iterable and the client-side multicast iterable — so a producer
13
+ // needs no separate live() declaration to be live here.
14
+ const LIVE_SOURCE = Symbol.for("solid.LiveSource");
15
+ const channelMap = new Map();
16
+ // Status outlives the channel (an "idle"/"closed" read after teardown is
17
+ // meaningful), so signals live in their own keyed map.
18
+ const statusMap = new Map();
19
+ function statusSignal(key) {
20
+ let s = statusMap.get(key);
21
+ // ownedWrite: the first write happens synchronously inside whatever
22
+ // computation pulled the channel open — this is channel-owned state, not
23
+ // the computation's own.
24
+ if (!s)
25
+ statusMap.set(key, (s = createSignal("idle", { ownedWrite: true })));
26
+ return s;
27
+ }
28
+ function wake(ch) {
29
+ const waiters = [...ch.waiters];
30
+ ch.waiters.clear();
31
+ for (const w of waiters)
32
+ w();
33
+ }
34
+ const noStatus = () => { };
35
+ async function connect(ch) {
36
+ const gen = ch.gen;
37
+ const setStatus = ch.report ? statusSignal(ch.key)[1] : noStatus;
38
+ let attempts = 0;
39
+ setStatus(ch.version ? "reconnecting" : "connecting");
40
+ while (true) {
41
+ try {
42
+ const result = await ch.call();
43
+ if (gen !== ch.gen || ch.done) {
44
+ // superseded (reconnect) or torn down while connecting: the arrived
45
+ // stream must still be ended or its request leaks
46
+ result?.[Symbol.asyncIterator] && result[Symbol.asyncIterator]().return?.();
47
+ return;
48
+ }
49
+ // A live()-declared producer's own retry loop erases deaths from the
50
+ // value stream and reports them through onstatus — forward those into
51
+ // the channel's status so both producer kinds read the same. Its
52
+ // "closed" is redundant with our own done handling below.
53
+ if (result !== null && typeof result === "object") {
54
+ try {
55
+ result.onstatus = (state) => {
56
+ if (gen === ch.gen && state !== "closed")
57
+ setStatus(state);
58
+ };
59
+ }
60
+ catch { }
61
+ }
62
+ const it = result !== null && typeof result === "object" && result[Symbol.asyncIterator]
63
+ ? result[Symbol.asyncIterator]()
64
+ : (async function* () {
65
+ yield result;
66
+ })();
67
+ ch.close = () => {
68
+ try {
69
+ const r = it.return && it.return();
70
+ if (r && typeof r.then === "function")
71
+ r.then(undefined, () => { });
72
+ }
73
+ catch { }
74
+ };
75
+ while (true) {
76
+ const r = await it.next();
77
+ if (gen !== ch.gen || ch.done)
78
+ return;
79
+ if (r.done)
80
+ break;
81
+ ch.latest = r.value;
82
+ ch.version++;
83
+ attempts = 0; // healthy value: backoff resets
84
+ setStatus("connected");
85
+ wake(ch);
86
+ }
87
+ ch.done = true;
88
+ setStatus("closed");
89
+ wake(ch);
90
+ return;
91
+ }
92
+ catch (error) {
93
+ if (gen !== ch.gen || ch.done)
94
+ return;
95
+ const status = error?.status;
96
+ if (!ch.version || (typeof status === "number" && status >= 400 && status < 500)) {
97
+ // Terminal: never connected (fail like a normal call), or a
98
+ // definite rejection — the transport stamps HTTP statuses onto
99
+ // failures, and a 4xx means the server understood and refused;
100
+ // retrying cannot change the answer. Consumers rethrow (after
101
+ // draining the latest value), surfacing to error boundaries.
102
+ ch.error = error;
103
+ ch.done = true;
104
+ setStatus("closed");
105
+ wake(ch);
106
+ return;
107
+ }
108
+ // A stream that had delivered died transiently (network, 5xx,
109
+ // severed stream): the channel IS the live layer — reconnect with
110
+ // backoff, latest value keeps serving meanwhile. (A live()-declared
111
+ // producer never reaches here; its transport retries.)
112
+ setStatus("reconnecting");
113
+ let timer, wakeUp;
114
+ await new Promise(resolve => {
115
+ wakeUp = () => resolve();
116
+ timer = setTimeout(wakeUp, Math.min(500 * 2 ** attempts++, 10000));
117
+ // connectivity returning wakes the sleep early
118
+ if (typeof addEventListener === "function")
119
+ addEventListener("online", wakeUp, { once: true });
120
+ });
121
+ clearTimeout(timer);
122
+ // a timer-woken sleep leaves its once-listener behind otherwise —
123
+ // one leaked closure per backoff cycle until an online event fires
124
+ if (typeof removeEventListener === "function")
125
+ removeEventListener("online", wakeUp);
126
+ if (gen !== ch.gen || ch.done)
127
+ return;
128
+ }
129
+ }
130
+ }
131
+ function teardown(ch) {
132
+ // Deferred so a same-key resubscribe in the same tick (a memo re-running
133
+ // its compute) reuses the connection instead of thrashing it.
134
+ queueMicrotask(() => {
135
+ if (ch.count > 0 || ch.map.get(ch.key) !== ch)
136
+ return;
137
+ const ended = ch.done; // completed/failed on its own — "closed" persists
138
+ ch.done = true;
139
+ ch.close();
140
+ wake(ch);
141
+ if (ch.retain)
142
+ return; // request-scoped: latest stays replayable
143
+ ch.map.delete(ch.key);
144
+ if (ch.report && !ended)
145
+ statusSignal(ch.key)[1]("idle");
146
+ });
147
+ }
148
+ function openChannel(map, key, call, report, retain) {
149
+ let ch = map.get(key);
150
+ if (!ch) {
151
+ ch = {
152
+ key,
153
+ map,
154
+ count: 0,
155
+ version: 0,
156
+ latest: undefined,
157
+ error: undefined,
158
+ done: false,
159
+ waiters: new Set(),
160
+ gen: 0,
161
+ close: () => { },
162
+ call,
163
+ report,
164
+ retain
165
+ };
166
+ map.set(key, ch);
167
+ connect(ch);
168
+ }
169
+ return ch;
170
+ }
171
+ // The consumer half: a branded iterable whose iterators subscribe to the
172
+ // channel lazily (first pull connects — a hydration trace or speculative
173
+ // call opens nothing), replay the latest value immediately, then follow
174
+ // with latest-wins delivery.
175
+ function subscriberIterable(open) {
176
+ return {
177
+ [LIVE_SOURCE]: true,
178
+ [Symbol.asyncIterator]() {
179
+ const ch = open();
180
+ ch.count++;
181
+ let seen = 0;
182
+ let released = false;
183
+ const release = () => {
184
+ if (released)
185
+ return;
186
+ released = true;
187
+ if (--ch.count === 0)
188
+ teardown(ch);
189
+ };
190
+ const pull = async () => {
191
+ while (true) {
192
+ if (ch.error && seen >= ch.version) {
193
+ release();
194
+ throw ch.error;
195
+ }
196
+ if (ch.version > seen) {
197
+ seen = ch.version;
198
+ return { done: false, value: ch.latest };
199
+ }
200
+ if (ch.done) {
201
+ release();
202
+ return { done: true, value: undefined };
203
+ }
204
+ await new Promise(resolve => ch.waiters.add(resolve));
205
+ }
206
+ };
207
+ return {
208
+ next: () => (released ? Promise.resolve({ done: true, value: undefined }) : pull()),
209
+ return(value) {
210
+ release();
211
+ return Promise.resolve({ done: true, value });
212
+ }
213
+ };
214
+ }
215
+ };
216
+ }
217
+ function reconnectChannel(ch) {
218
+ ch.gen++;
219
+ ch.close();
220
+ ch.error = undefined;
221
+ ch.done = false;
222
+ connect(ch);
223
+ }
224
+ function push(ch, value) {
225
+ ch.latest = value;
226
+ ch.version++;
227
+ wake(ch);
228
+ }
229
+ // Live channels participate in both data protocols. Registered on first
230
+ // liveQuery() call, not at module scope: a side-effectful top level would
231
+ // anchor this module into graphs that never use it.
232
+ //
233
+ // - Explicit revalidate(key) (force) reconnects — the producer re-yields
234
+ // current state on invocation by contract.
235
+ // - The post-mutation sweep (force=false) does NOT reconnect: the
236
+ // connection is alive and is itself the freshness mechanism — a
237
+ // push-driven producer yields the mutated state on its own, and tearing
238
+ // down healthy connections on every mutation defeats the model.
239
+ // - Single-flight payloads deliver INTO open channels, exactly as they seed
240
+ // the query cache: the mutation response is the round trip, the channel
241
+ // adopts the value immediately, and the live stream stays authoritative
242
+ // for everything after.
243
+ let hooked = false;
244
+ function hookRevalidate() {
245
+ if (hooked)
246
+ return;
247
+ hooked = true;
248
+ registerRevalidateHook((keys, force) => {
249
+ if (!force)
250
+ return;
251
+ for (const [k, ch] of channelMap) {
252
+ if (keys === undefined || matchKey(k, keys))
253
+ reconnectChannel(ch);
254
+ }
255
+ });
256
+ registerFlightDataHook(data => {
257
+ for (const k in data) {
258
+ const ch = channelMap.get(k);
259
+ if (!ch || ch.done)
260
+ continue;
261
+ const v = data[k];
262
+ if (v !== null && typeof v === "object" && v[Symbol.asyncIterator]) {
263
+ // a live key collected server-side arrives stream-shaped: adopt
264
+ // its yields for as long as it runs (it ends with the response
265
+ // body); the channel's own connection remains authoritative
266
+ (async () => {
267
+ try {
268
+ for await (const value of v) {
269
+ if (ch.done)
270
+ return;
271
+ push(ch, value);
272
+ }
273
+ }
274
+ catch { }
275
+ })();
276
+ }
277
+ else
278
+ push(ch, v);
279
+ }
280
+ });
281
+ }
282
+ /**
283
+ * Declares a keyed live query over a value-shaped stream: an async-iterable
284
+ * producer whose yields are successive VALUES of one logical query, with the
285
+ * contract that the producer re-yields current state on every invocation.
286
+ * liveQuery IS the live layer — no separate declaration needed:
287
+ *
288
+ * - One connection per (name + args) key, shared by every consumer. Late
289
+ * subscribers receive the latest value immediately, delivery is
290
+ * latest-wins, and the connection closes when the last consumer leaves.
291
+ * - A connected stream that dies transiently (network, 5xx) reconnects with
292
+ * exponential backoff — the latest value keeps serving meanwhile. A
293
+ * definite rejection (4xx: the transport stamps HTTP statuses onto
294
+ * failures) ends the channel and surfaces the error to consumers, as does
295
+ * a first-connect failure.
296
+ * - Server functions are declared GET at creation (like `query`). Live
297
+ * queries participate in single-flight on the delivery side: a mutation's
298
+ * flight payload pushes straight into open channels (the mutation
299
+ * response is the round trip), while the post-mutation sweep leaves the
300
+ * healthy connection in place — the live stream stays authoritative.
301
+ * - Calling under preload intent warms the channel (a temporary hold keeps
302
+ * it open through the preload window), so navigation renders against an
303
+ * already-connected stream instead of holding the transition on connect
304
+ * plus first yield. Outside preload, calling remains free — only a real
305
+ * pull connects.
306
+ * - SSR: the document face renders the first value; hydration adopts it and
307
+ * reconnects. Channels are request-scoped on the server, so every
308
+ * consumer of a key in one render observes the same value. Explicit
309
+ * `revalidate(key)` reconnects (the producer re-yields current state on
310
+ * invocation), and `status(...args)` is a reactive read of the channel's
311
+ * wire state.
312
+ *
313
+ * ```ts
314
+ * const roomMessages = liveQuery(getMessages, "messages");
315
+ * // in a component — one connection, however many consumers:
316
+ * const messages = createMemo(() => roomMessages(props.room));
317
+ * ```
318
+ */
319
+ export function liveQuery(fn, name) {
320
+ hookRevalidate();
321
+ // liveQuery implies GET, exactly as query does: reads belong on the GET
322
+ // transport (cacheable URLs, no single-flight enveloping). An explicit
323
+ // GET(fn) or live(GET(fn)) declaration passes through untouched.
324
+ if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
325
+ const rpc = getServerFunctionRPC();
326
+ if (rpc)
327
+ fn = rpc.GET(fn);
328
+ }
329
+ const liveFn = ((...args) => {
330
+ const key = name + hashKey(args);
331
+ if (isServer) {
332
+ const e = getRequestEvent();
333
+ if (!e) {
334
+ // No request scope (static render, tests): hand the source through
335
+ // whole, branded, so the signals layer applies live SSR policy
336
+ // (first value, then client takeover).
337
+ const result = fn(...args);
338
+ const brand = (r) => {
339
+ if (r !== null && typeof r === "object" && r[Symbol.asyncIterator])
340
+ r[LIVE_SOURCE] = true;
341
+ return r;
342
+ };
343
+ return result !== null && typeof result === "object" && typeof result.then === "function"
344
+ ? result.then(brand)
345
+ : brand(result);
346
+ }
347
+ // Request-scoped channels: two consumers of one key during one render
348
+ // must observe the SAME first value (query's request cache gives its
349
+ // reads the same guarantee). Channels live in the event, retained
350
+ // past teardown so a later consumer replays the settled value instead
351
+ // of reinvoking; the producer still closes with its last consumer.
352
+ const router = (e.router || (e.router = {}));
353
+ const channels = router.liveChannels || (router.liveChannels = new Map());
354
+ return subscriberIterable(() => openChannel(channels, key, () => fn(...args), false, true));
355
+ }
356
+ const open = () => openChannel(channelMap, key, () => fn(...args), true, false);
357
+ // Preload warms the channel, exactly as calling a query in a preload
358
+ // function warms its cache: connect now, hold a temporary consumer slot
359
+ // so refcounting doesn't close it before navigation renders. When the
360
+ // real consumer arrives it inherits the live connection (first pull
361
+ // replays the already-arrived value — the transition doesn't hold);
362
+ // when navigation never comes, the hold lapses and teardown closes it.
363
+ if (getIntent() === "preload") {
364
+ const ch = open();
365
+ ch.count++;
366
+ setTimeout(() => {
367
+ if (--ch.count === 0)
368
+ teardown(ch);
369
+ }, PRELOAD_TIMEOUT);
370
+ }
371
+ return subscriberIterable(open);
372
+ });
373
+ liveFn.keyFor = (...args) => name + hashKey(args);
374
+ liveFn.key = name;
375
+ liveFn.status = (...args) => statusSignal(name + hashKey(args))[0]();
376
+ return liveFn;
377
+ }
@@ -1,4 +1,9 @@
1
1
  import type { CacheEntry, NarrowResponse } from "../types.js";
2
+ export declare function registerRevalidateHook(hook: (keys: string[] | void, force: boolean) => void): void;
3
+ export declare function registerFlightDataHook(hook: (data: Record<string, any>) => void): void;
4
+ /** Fans a single-flight payload out to registered keyed stores (the query
5
+ * cache itself is seeded by the caller via query.set). */
6
+ export declare function deliverFlightData(data: Record<string, any>): void;
2
7
  /**
3
8
  * Revalidates the given cache entry/entries.
4
9
  */
@@ -18,4 +23,5 @@ export declare namespace query {
18
23
  export var clear: () => void;
19
24
  export { _a as delete };
20
25
  }
26
+ export declare function matchKey(key: string, keys: string[]): boolean;
21
27
  export declare function hashKey<T extends Array<any>>(args: T): string;
@@ -38,6 +38,23 @@ function getCache() {
38
38
  throw new Error("Cannot find cache context");
39
39
  return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
40
40
  }
41
+ // Other keyed stores participating in the data protocols (live query
42
+ // channels) register here — revalidate() stays the one invalidation verb,
43
+ // and single-flight payloads reach every keyed store, not just the cache.
44
+ const revalidateHooks = [];
45
+ export function registerRevalidateHook(hook) {
46
+ revalidateHooks.push(hook);
47
+ }
48
+ const flightHooks = [];
49
+ export function registerFlightDataHook(hook) {
50
+ flightHooks.push(hook);
51
+ }
52
+ /** Fans a single-flight payload out to registered keyed stores (the query
53
+ * cache itself is seeded by the caller via query.set). */
54
+ export function deliverFlightData(data) {
55
+ for (const hook of flightHooks)
56
+ hook(data);
57
+ }
41
58
  /**
42
59
  * Revalidates the given cache entry/entries.
43
60
  */
@@ -47,6 +64,9 @@ export function revalidate(key, force = true) {
47
64
  force && (entry[0] = 0); //force cache miss
48
65
  entry[4][1](now); // retrigger live signals
49
66
  });
67
+ const keys = key === undefined ? undefined : Array.isArray(key) ? key : [key];
68
+ for (const hook of revalidateHooks)
69
+ hook(keys, force);
50
70
  }
51
71
  export function cacheKeyOp(key, fn) {
52
72
  key && !Array.isArray(key) && (key = [key]);
@@ -291,7 +311,7 @@ query.set = (key, value) => {
291
311
  };
292
312
  query.delete = (key) => getCache().delete(key);
293
313
  query.clear = () => getCache().clear();
294
- function matchKey(key, keys) {
314
+ export function matchKey(key, keys) {
295
315
  for (let k of keys) {
296
316
  if (k && key.startsWith(k))
297
317
  return true;
package/dist/fs.js CHANGED
@@ -37,7 +37,9 @@ export function fileRoutes(entries) {
37
37
  }
38
38
  let component = components.get(ref.src);
39
39
  if (!component) {
40
- component = lazy(ref.import, ref.src);
40
+ // moduleUrl is lazy()'s third argument as of solid 2.0.0-rc.1 (options
41
+ // moved second); route components are default exports, so no { export }.
42
+ component = lazy(ref.import, undefined, ref.src);
41
43
  components.set(ref.src, component);
42
44
  }
43
45
  return component;