@lovrozagar/flare 0.1.2 → 0.2.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 +42 -19
- package/package.json +8 -6
- package/src/broadcast/index.ts +1 -1
- package/src/broadcast/provider.tsx +2 -2
- package/src/caches/index.ts +26 -0
- package/src/components/await.tsx +36 -39
- package/src/components/dev-error-overlay.tsx +8 -9
- package/src/components/direction-script.tsx +2 -2
- package/src/components/locale-script.tsx +2 -2
- package/src/components/reset-css.tsx +1 -1
- package/src/components/ssr-context.tsx +7 -14
- package/src/components/theme-script.tsx +2 -2
- package/src/components/view-transition-css.tsx +1 -1
- package/src/defer/index.ts +22 -0
- package/src/direction/index.tsx +61 -36
- package/src/errors/index.ts +45 -11
- package/src/fonts/component.tsx +2 -1
- package/src/form/index.tsx +18 -12
- package/src/generators/index.ts +73 -30
- package/src/head-client/index.ts +7 -0
- package/src/history/index.ts +6 -0
- package/src/hydrate/index.tsx +90 -60
- package/src/hydration/index.ts +26 -6
- package/src/i18n/index.ts +11 -2
- package/src/image/index.tsx +7 -4
- package/src/intercept-outlet/index.tsx +6 -2
- package/src/lazy/index.tsx +50 -50
- package/src/link/index.tsx +97 -58
- package/src/loader-pipeline/index.ts +93 -35
- package/src/locale/cookie.ts +15 -0
- package/src/locale/index.tsx +24 -35
- package/src/locale.ts +1 -0
- package/src/middleware/builtins/api-proxy.ts +58 -4
- package/src/middleware/builtins/cdn-proxy.ts +24 -3
- package/src/middleware/builtins/i18n.ts +23 -25
- package/src/middleware/builtins/keepalive.ts +2 -3
- package/src/mount/index.ts +8 -1
- package/src/navigation/index.ts +575 -129
- package/src/navigation/types.ts +11 -1
- package/src/navigation-progress/index.tsx +17 -16
- package/src/ndjson-client/index.ts +42 -17
- package/src/ndjson-server/index.ts +78 -29
- package/src/outlet/index.tsx +192 -97
- package/src/outlet/types.ts +9 -1
- package/src/plugins/dev-cdn-cache.ts +18 -5
- package/src/plugins/dev-dashboard/client.tsx +130 -118
- package/src/plugins/dev-prerender.ts +6 -1
- package/src/plugins/dev-server.ts +12 -3
- package/src/plugins/image-plugin.ts +18 -4
- package/src/plugins/index.ts +10 -7
- package/src/plugins/prerender-plugin.ts +3 -3
- package/src/plugins/server-fn.ts +15 -20
- package/src/plugins/solid-jsx.d.ts +3 -3
- package/src/plugins/sx-ast/atomic.ts +5 -5
- package/src/plugins/sx-ast/index.ts +9 -0
- package/src/plugins/sx.d.ts +4 -5
- package/src/plugins/virtual.ts +11 -6
- package/src/prerender/index.ts +68 -32
- package/src/protocol.ts +65 -0
- package/src/query-client/index.tsx +73 -14
- package/src/rewrite/index.ts +10 -4
- package/src/route-builder/create-layout.ts +1 -1
- package/src/route-builder/create-root-layout.ts +1 -1
- package/src/route-builder/types.ts +5 -1
- package/src/router-primitives/types.ts +2 -0
- package/src/security/index.ts +41 -5
- package/src/server/index.ts +1 -2
- package/src/server-fn/get-input.ts +90 -0
- package/src/server-fn/http-error.ts +13 -0
- package/src/server-fn/index.ts +94 -70
- package/src/server-fn-query.ts +9 -17
- package/src/server-handler/etag.ts +4 -2
- package/src/server-handler/headers.ts +1 -2
- package/src/server-handler/index.ts +164 -56
- package/src/server-handler/types.ts +2 -0
- package/src/service-worker/template.ts +10 -42
- package/src/ssr/head-prefix.ts +2 -1
- package/src/ssr/hoist-head-markers.ts +25 -0
- package/src/ssr/index.tsx +109 -92
- package/src/state-parser/index.ts +38 -26
- package/src/styles/index.ts +89 -60
- package/src/testing/index.ts +8 -3
- package/src/theme/index.tsx +35 -32
- package/src/tracing/timing.ts +6 -1
- package/src/url/index.ts +9 -0
package/src/ssr/index.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { getNextChildId, getOwner } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
3
|
+
import { createComponent, Hydration, renderToStream as solidRenderToStream } from "@solidjs/web";
|
|
3
4
|
import type { GlobalBoundaries } from "../boundaries/index.ts";
|
|
4
5
|
import { BroadcastProvider } from "../broadcast/provider.tsx";
|
|
5
6
|
import { createMatchCache, createPrefetchCache } from "../caches/index.ts";
|
|
6
|
-
import {
|
|
7
|
+
import { SSRContextProvider } from "../components/ssr-context.tsx";
|
|
7
8
|
import type { DeferContext, DeferredEntry } from "../defer/index.ts";
|
|
8
9
|
import { isDeferred } from "../defer/index.ts";
|
|
9
10
|
import { DirectionProvider } from "../direction.ts";
|
|
@@ -28,8 +29,10 @@ import type { ServerLogEntry } from "@lovrozagar/flare/server-context";
|
|
|
28
29
|
import { clearScopedStyles, getScopedStyles, RUNTIME_SHEET_ID } from "../styles/index.ts";
|
|
29
30
|
import { CRITICAL_SHEET_ID, injectCriticalAppend, type SxCssManifest } from "./critical-css.ts";
|
|
30
31
|
import { ThemeProvider } from "../theme.ts";
|
|
32
|
+
import { GLOBAL_DEFER, GLOBAL_QUERIES } from "../protocol.ts";
|
|
31
33
|
import { parseSearchParams, type SearchParams } from "../url/index.ts";
|
|
32
34
|
import { renderHeadToHtml } from "./head.ts";
|
|
35
|
+
import { hoistHydrationHeadMarkers } from "./hoist-head-markers.ts";
|
|
33
36
|
import { buildHeadPrefix } from "./head-prefix.ts";
|
|
34
37
|
|
|
35
38
|
export { mergeHeadConfigs } from "../internal.ts";
|
|
@@ -394,9 +397,19 @@ function RootRenderer(props: {
|
|
|
394
397
|
preloaderContext: Record<string, unknown> | undefined;
|
|
395
398
|
renderFn: (p: RenderProps) => JSX.Element;
|
|
396
399
|
}): JSX.Element {
|
|
400
|
+
/*
|
|
401
|
+
* Client `createContext` children() consumes one hydration id in this
|
|
402
|
+
* owner before the layout runs; the server's children() memos do not.
|
|
403
|
+
* Advance the server counter so <html> `_hk` matches hydrate.
|
|
404
|
+
*/
|
|
405
|
+
const owner = getOwner();
|
|
406
|
+
if (owner?.id != null) getNextChildId(owner);
|
|
397
407
|
const router = useRouter();
|
|
408
|
+
/* Getter so <Outlet> does not consume hydration child ids before <html>. */
|
|
398
409
|
return props.renderFn({
|
|
399
|
-
children
|
|
410
|
+
get children() {
|
|
411
|
+
return <Outlet />;
|
|
412
|
+
},
|
|
400
413
|
loaderData: props.data,
|
|
401
414
|
location: props.location,
|
|
402
415
|
preloaderContext: props.preloaderContext,
|
|
@@ -419,7 +432,7 @@ function buildComponentTree(config: SSRConfig, flareStateScript: string): () =>
|
|
|
419
432
|
const matches = config.matches;
|
|
420
433
|
|
|
421
434
|
return () => {
|
|
422
|
-
|
|
435
|
+
const ssrCtxValue = {
|
|
423
436
|
direction: config.router?.direction,
|
|
424
437
|
entryScript: config.entryScript,
|
|
425
438
|
flareStateScript,
|
|
@@ -427,7 +440,7 @@ function buildComponentTree(config: SSRConfig, flareStateScript: string): () =>
|
|
|
427
440
|
nonce: config.nonce,
|
|
428
441
|
resolvedHead: config.resolvedHead,
|
|
429
442
|
theme: config.router?.theme,
|
|
430
|
-
}
|
|
443
|
+
};
|
|
431
444
|
|
|
432
445
|
const lastMatch = matches[matches.length - 1];
|
|
433
446
|
const location: ProviderLocation = {
|
|
@@ -452,8 +465,8 @@ function buildComponentTree(config: SSRConfig, flareStateScript: string): () =>
|
|
|
452
465
|
* Full-document hydration: providers wrap root layout so components
|
|
453
466
|
* inside the root layout (e.g. NavigationProgress) have RouterContext.
|
|
454
467
|
*
|
|
455
|
-
* SSR:
|
|
456
|
-
*
|
|
468
|
+
* SSR and client: Hydration > QCP? > SSRContextProvider > Theme >
|
|
469
|
+
* Direction > Broadcast > FlareProvider > rootRenderFn({children: Outlet})
|
|
457
470
|
*
|
|
458
471
|
* All JSX inside the Hydration boundary — no pre-evaluated constants outside.
|
|
459
472
|
*/
|
|
@@ -480,37 +493,39 @@ function buildComponentTree(config: SSRConfig, flareStateScript: string): () =>
|
|
|
480
493
|
* inside the Provider's context scope during Solid SSR.
|
|
481
494
|
*/
|
|
482
495
|
const renderInner = () => (
|
|
483
|
-
<
|
|
484
|
-
<
|
|
485
|
-
<
|
|
486
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
496
|
+
<SSRContextProvider value={ssrCtxValue}>
|
|
497
|
+
<ThemeProvider config={config.router?.theme}>
|
|
498
|
+
<DirectionProvider config={config.router?.direction}>
|
|
499
|
+
<BroadcastProvider>
|
|
500
|
+
<FlareProvider
|
|
501
|
+
boundaries={rootBoundaries}
|
|
502
|
+
caseSensitive={config.router?.caseSensitive}
|
|
503
|
+
initialLocation={location}
|
|
504
|
+
layouts={config.router?.layouts ?? {}}
|
|
505
|
+
localeConfig={config.router?.locale}
|
|
506
|
+
matchCache={matchCache}
|
|
507
|
+
matches={clientMatches}
|
|
508
|
+
params={location.params}
|
|
509
|
+
prefetchCache={prefetchCache}
|
|
510
|
+
resolvers={new Map()}
|
|
511
|
+
routeTree={config.router?.routeTree ?? createTreeNode()}
|
|
512
|
+
search={location.search}
|
|
513
|
+
>
|
|
514
|
+
{rootRenderFn ? (
|
|
515
|
+
RootRenderer({
|
|
516
|
+
data: rootMatch?.loaderData,
|
|
517
|
+
location,
|
|
518
|
+
preloaderContext: rootMatch?.preloaderContext,
|
|
519
|
+
renderFn: rootRenderFn,
|
|
520
|
+
})
|
|
521
|
+
) : (
|
|
522
|
+
<Outlet />
|
|
523
|
+
)}
|
|
524
|
+
</FlareProvider>
|
|
525
|
+
</BroadcastProvider>
|
|
526
|
+
</DirectionProvider>
|
|
527
|
+
</ThemeProvider>
|
|
528
|
+
</SSRContextProvider>
|
|
514
529
|
);
|
|
515
530
|
|
|
516
531
|
return (
|
|
@@ -532,18 +547,15 @@ function buildComponentTree(config: SSRConfig, flareStateScript: string): () =>
|
|
|
532
547
|
* Inject resolved head tags, CSP nonce meta, and scoped styles before </head>.
|
|
533
548
|
* Returns updated buffer with head content injected, or original buffer if </head> not found.
|
|
534
549
|
*
|
|
535
|
-
* Head structure
|
|
536
|
-
* 1.
|
|
537
|
-
* 2. viewport
|
|
538
|
-
* 3.
|
|
539
|
-
* 4. <
|
|
540
|
-
* 5.
|
|
541
|
-
* 6. Resolved <head> tags (title, meta, etc.)
|
|
542
|
-
* 7. Scoped styles
|
|
550
|
+
* Head structure:
|
|
551
|
+
* 1. Solid's hydratable head children (`<!--$-->` first — required for Solid 2)
|
|
552
|
+
* 2. CSP nonce / viewport / theme / direction / locale scripts (headPrefix)
|
|
553
|
+
* 3. modulepreload + stylesheets
|
|
554
|
+
* 4. Resolved <head> tags (title, meta, etc.)
|
|
555
|
+
* 5. Scoped styles
|
|
543
556
|
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* has no 'strict-dynamic' (crbug.com/702612).
|
|
557
|
+
* Prefix is appended (not prepended) so hydrate can walk Solid's markers.
|
|
558
|
+
* Theme scripts still run in `<head>` before `<body>` is parsed.
|
|
547
559
|
*/
|
|
548
560
|
function injectHeadContent(
|
|
549
561
|
buffer: string,
|
|
@@ -565,11 +577,12 @@ function injectHeadContent(
|
|
|
565
577
|
|
|
566
578
|
let headSuffix = "";
|
|
567
579
|
const headHtml = renderHeadToHtml(config.resolvedHead, config.nonce);
|
|
568
|
-
|
|
580
|
+
/* Runtime scoped sheet first so route `head().custom.styles` win at equal specificity. */
|
|
569
581
|
if (scopedStyles) {
|
|
570
582
|
const safeStyles = scopedStyles.replace(/<\/style\b/gi, "<\\/style");
|
|
571
583
|
headSuffix += `<style id="${RUNTIME_SHEET_ID}" nonce="${escapedNonce}">${safeStyles}</style>`;
|
|
572
584
|
}
|
|
585
|
+
if (headHtml) headSuffix += headHtml;
|
|
573
586
|
/* Critical-CSS placeholder — body not known yet; late-inject populates it near </body>. */
|
|
574
587
|
if (config.sxCssManifest) {
|
|
575
588
|
const nonceAttr = config.nonce ? ` nonce="${escapedNonce}"` : "";
|
|
@@ -617,8 +630,8 @@ function injectHeadContent(
|
|
|
617
630
|
headSuffix += `<style nonce="${escapedNonce}">body{${cssText}}</style>`;
|
|
618
631
|
}
|
|
619
632
|
|
|
620
|
-
const result = buffer.replace(
|
|
621
|
-
return result
|
|
633
|
+
const result = buffer.replace("</head>", `${headPrefix}${headSuffix}${extraSuffix}</head>`);
|
|
634
|
+
return hoistHydrationHeadMarkers(result);
|
|
622
635
|
}
|
|
623
636
|
|
|
624
637
|
/**
|
|
@@ -688,7 +701,7 @@ export function renderToStream(config: SSRConfig): SSRResult {
|
|
|
688
701
|
const decoder = new TextDecoder();
|
|
689
702
|
|
|
690
703
|
/*
|
|
691
|
-
* Solid only injects _$HY when async work exists (
|
|
704
|
+
* Solid only injects _$HY when async work exists (Loading, deferred data).
|
|
692
705
|
* For sync-only rendering, _$HY is never set, but client `hydrate()` always
|
|
693
706
|
* reads `globalThis._$HY.done`. Inject a minimal _$HY init unconditionally.
|
|
694
707
|
*/
|
|
@@ -805,14 +818,6 @@ export function renderToStream(config: SSRConfig): SSRResult {
|
|
|
805
818
|
|
|
806
819
|
/* Stream deferred resolution scripts after HTML */
|
|
807
820
|
if (config.deferContexts && config.deferContexts.size > 0) {
|
|
808
|
-
/* Install QC tracking so deferred callbacks' setQueryData calls are captured */
|
|
809
|
-
let trackedQC: { drain(): Array<{ data: unknown; key: unknown[]; staleTime?: number }> } | undefined;
|
|
810
|
-
if (config.queryClient) {
|
|
811
|
-
const { createTrackedQueryClient } = await import("../query-client");
|
|
812
|
-
const typed = config.queryClient as Parameters<typeof createTrackedQueryClient>[0];
|
|
813
|
-
trackedQC = createTrackedQueryClient(typed);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
821
|
const entries: DeferredEntry[] = [];
|
|
817
822
|
for (const ctx of config.deferContexts.values()) {
|
|
818
823
|
for (const e of ctx.entries()) {
|
|
@@ -821,41 +826,53 @@ export function renderToStream(config: SSRConfig): SSRResult {
|
|
|
821
826
|
}
|
|
822
827
|
const escapedNonce = escapeAttr(config.nonce);
|
|
823
828
|
const esc = (s: string) => JSON.stringify(s).replace(/</g, "\\u003c");
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
),
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
)
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/* Stream QC delta entries accumulated during this deferred resolve */
|
|
846
|
-
if (trackedQC) {
|
|
847
|
-
const qcEntries = trackedQC.drain();
|
|
848
|
-
for (const qcEntry of qcEntries) {
|
|
849
|
-
const qcJson = JSON.stringify(qcEntry).replace(/</g, "\\u003c");
|
|
829
|
+
|
|
830
|
+
const streamDeferred = async (trackedQC?: {
|
|
831
|
+
drain(): Array<{ data: unknown; key: unknown[]; staleTime?: number }>;
|
|
832
|
+
}) => {
|
|
833
|
+
await Promise.allSettled(
|
|
834
|
+
entries.map(async (entry) => {
|
|
835
|
+
const resolverKey = esc(`${entry.matchId}:${entry.key}`);
|
|
836
|
+
try {
|
|
837
|
+
const data = await entry.promise;
|
|
838
|
+
const json = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
839
|
+
controller.enqueue(
|
|
840
|
+
encoder.encode(
|
|
841
|
+
`<script nonce="${escapedNonce}">(self.${GLOBAL_DEFER}=self.${GLOBAL_DEFER}||[]).push([${resolverKey},${json}])</script>`,
|
|
842
|
+
),
|
|
843
|
+
);
|
|
844
|
+
} catch (err) {
|
|
845
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
846
|
+
const safeMsg = esc(msg);
|
|
850
847
|
controller.enqueue(
|
|
851
848
|
encoder.encode(
|
|
852
|
-
`<script nonce="${escapedNonce}">(self
|
|
849
|
+
`<script nonce="${escapedNonce}">(self.${GLOBAL_DEFER}=self.${GLOBAL_DEFER}||[]).push([${resolverKey},${safeMsg},true])</script>`,
|
|
853
850
|
),
|
|
854
851
|
);
|
|
855
852
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
853
|
+
|
|
854
|
+
if (trackedQC) {
|
|
855
|
+
const qcEntries = trackedQC.drain();
|
|
856
|
+
for (const qcEntry of qcEntries) {
|
|
857
|
+
const qcJson = JSON.stringify(qcEntry).replace(/</g, "\\u003c");
|
|
858
|
+
controller.enqueue(
|
|
859
|
+
encoder.encode(
|
|
860
|
+
`<script nonce="${escapedNonce}">(self.${GLOBAL_QUERIES}=self.${GLOBAL_QUERIES}||[]).push([${qcJson}])</script>`,
|
|
861
|
+
),
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}),
|
|
866
|
+
);
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
if (config.queryClient) {
|
|
870
|
+
const { withTrackedQueryClient } = await import("../query-client");
|
|
871
|
+
const typed = config.queryClient as Parameters<typeof withTrackedQueryClient>[0];
|
|
872
|
+
await withTrackedQueryClient(typed, streamDeferred);
|
|
873
|
+
} else {
|
|
874
|
+
await streamDeferred();
|
|
875
|
+
}
|
|
859
876
|
}
|
|
860
877
|
|
|
861
878
|
controller.close();
|
|
@@ -57,6 +57,17 @@ export function isDeferredMarker(value: unknown): value is DeferredMarker {
|
|
|
57
57
|
return obj.__deferred === true && "key" in obj && typeof obj.key === "string";
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const MAX_DEFERRED_WALK = 50;
|
|
61
|
+
|
|
62
|
+
/** Prefetch stores `{ __deferred, key }` with no promise. Hydrated shells use `__key` + `promise`. */
|
|
63
|
+
export function hasRawDeferredMarkers(data: unknown, depth = 0): boolean {
|
|
64
|
+
if (depth > MAX_DEFERRED_WALK) return false;
|
|
65
|
+
if (data === null || data === undefined || typeof data !== "object") return false;
|
|
66
|
+
if (isDeferredMarker(data) && !("promise" in data)) return true;
|
|
67
|
+
if (Array.isArray(data)) return data.some((v) => hasRawDeferredMarkers(v, depth + 1));
|
|
68
|
+
return Object.values(data as Record<string, unknown>).some((v) => hasRawDeferredMarkers(v, depth + 1));
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
export function hydrateLoaderData(matchId: string, data: unknown, resolvers: Map<string, DeferredResolver>): unknown {
|
|
61
72
|
if (data === null || data === undefined) return data;
|
|
62
73
|
if (typeof data !== "object") return data;
|
|
@@ -68,6 +79,10 @@ export function hydrateLoaderData(matchId: string, data: unknown, resolvers: Map
|
|
|
68
79
|
resolveFn = resolve;
|
|
69
80
|
rejectFn = reject;
|
|
70
81
|
});
|
|
82
|
+
/* Handler attached in the same tick as construction so a later
|
|
83
|
+
reject() (NDJSON error, navigation cancel) is not unhandled
|
|
84
|
+
before Await's then() attaches. Multiple handlers still fire. */
|
|
85
|
+
promise.catch(() => {});
|
|
71
86
|
resolvers.set(`${matchId}:${data.key}`, { reject: rejectFn, resolve: resolveFn });
|
|
72
87
|
return { __deferred: true, __key: data.key, promise };
|
|
73
88
|
}
|
|
@@ -86,15 +101,15 @@ export function hydrateLoaderData(matchId: string, data: unknown, resolvers: Map
|
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
104
|
+
* SSR deferred chunks push into `self.__flare_defer`. `__flare_r` / `__flare_re`
|
|
105
|
+
* are leftovers and are no longer installed.
|
|
91
106
|
*/
|
|
92
107
|
declare global {
|
|
93
|
-
var
|
|
108
|
+
var __flare_defer:
|
|
94
109
|
| Array<[string, unknown, boolean?]>
|
|
95
110
|
| { push: (entry: [string, unknown, boolean?]) => number }
|
|
96
111
|
| undefined;
|
|
97
|
-
var
|
|
112
|
+
var __flare_queries: Array<[unknown]> | { push: (entry: [unknown]) => number } | undefined;
|
|
98
113
|
var __flare_r: ((key: string, data: unknown) => void) | undefined;
|
|
99
114
|
var __flare_re: ((key: string, message: string) => void) | undefined;
|
|
100
115
|
}
|
|
@@ -102,7 +117,7 @@ declare global {
|
|
|
102
117
|
/*
|
|
103
118
|
* Multi-instance registry: supports multiple Flare apps hydrating on the same page.
|
|
104
119
|
* Each call to installDeferredResolver registers its resolver map. The global
|
|
105
|
-
*
|
|
120
|
+
* `__flare_defer` push proxy searches ALL active maps when resolving a key.
|
|
106
121
|
* Globals are only cleaned when every active map is drained AND no pending entries remain.
|
|
107
122
|
*/
|
|
108
123
|
const activeInstances = new Set<Map<string, DeferredResolver>>();
|
|
@@ -131,7 +146,7 @@ function cleanupIfAllEmpty(): void {
|
|
|
131
146
|
}
|
|
132
147
|
globalThis.__flare_r = undefined;
|
|
133
148
|
globalThis.__flare_re = undefined;
|
|
134
|
-
globalThis.
|
|
149
|
+
globalThis.__flare_defer = undefined;
|
|
135
150
|
activeInstances.clear();
|
|
136
151
|
}
|
|
137
152
|
|
|
@@ -140,7 +155,7 @@ function cleanupIfAllEmpty(): void {
|
|
|
140
155
|
* then install live resolver for any late-arriving chunks.
|
|
141
156
|
*
|
|
142
157
|
* SSR scripts push `[key, data]` (or `[key, msg, true]` for errors)
|
|
143
|
-
* into `self.
|
|
158
|
+
* into `self.__flare_defer` array. This function drains them, then replaces
|
|
144
159
|
* the array push with a direct resolver so future chunks resolve immediately.
|
|
145
160
|
*
|
|
146
161
|
* Safe for multiple Flare instances on the same page — resolvers compose
|
|
@@ -149,8 +164,10 @@ function cleanupIfAllEmpty(): void {
|
|
|
149
164
|
export function installDeferredResolver(resolvers: Map<string, DeferredResolver>): void {
|
|
150
165
|
if (typeof globalThis === "undefined") return;
|
|
151
166
|
|
|
152
|
-
/* Reset
|
|
153
|
-
|
|
167
|
+
/* Reset leftover instances when this is a new page load: no live proxy
|
|
168
|
+
* (undefined after cleanup, or a fresh SSR array to drain). */
|
|
169
|
+
const deferIsLiveProxy = globalThis.__flare_defer !== undefined && !Array.isArray(globalThis.__flare_defer);
|
|
170
|
+
if (!deferIsLiveProxy && activeInstances.size > 0) {
|
|
154
171
|
activeInstances.clear();
|
|
155
172
|
pendingEntries.length = 0;
|
|
156
173
|
}
|
|
@@ -158,7 +175,7 @@ export function installDeferredResolver(resolvers: Map<string, DeferredResolver>
|
|
|
158
175
|
activeInstances.add(resolvers);
|
|
159
176
|
|
|
160
177
|
/* Drain any buffered entries from SSR script tags */
|
|
161
|
-
const queue = globalThis.
|
|
178
|
+
const queue = globalThis.__flare_defer;
|
|
162
179
|
if (Array.isArray(queue)) {
|
|
163
180
|
for (const entry of queue) {
|
|
164
181
|
resolveOrBuffer(entry[0], entry[1], Boolean(entry[2]));
|
|
@@ -174,28 +191,20 @@ export function installDeferredResolver(resolvers: Map<string, DeferredResolver>
|
|
|
174
191
|
}
|
|
175
192
|
}
|
|
176
193
|
|
|
177
|
-
/* Install live resolvers for chunks that stream after hydration */
|
|
178
|
-
globalThis.__flare_r = (key: string, data: unknown) => {
|
|
179
|
-
resolveOrBuffer(key, data, false);
|
|
180
|
-
cleanupIfAllEmpty();
|
|
181
|
-
};
|
|
182
|
-
globalThis.__flare_re = (key: string, message: string) => {
|
|
183
|
-
resolveOrBuffer(key, message, true);
|
|
184
|
-
cleanupIfAllEmpty();
|
|
185
|
-
};
|
|
186
|
-
|
|
187
194
|
/*
|
|
188
|
-
* Trap
|
|
189
|
-
* SSR scripts do `(self.
|
|
195
|
+
* Trap __flare_defer so late-arriving SSR script pushes resolve immediately.
|
|
196
|
+
* SSR scripts do `(self.__flare_defer=self.__flare_defer||[]).push([...])` — after
|
|
190
197
|
* drain, new pushes must route through the live resolver, not the array.
|
|
191
198
|
*/
|
|
192
|
-
globalThis.
|
|
199
|
+
globalThis.__flare_defer = {
|
|
193
200
|
push(entry: [string, unknown, boolean?]) {
|
|
194
201
|
resolveOrBuffer(entry[0], entry[1], Boolean(entry[2]));
|
|
195
202
|
cleanupIfAllEmpty();
|
|
196
203
|
return 0;
|
|
197
204
|
},
|
|
198
205
|
};
|
|
206
|
+
globalThis.__flare_r = undefined;
|
|
207
|
+
globalThis.__flare_re = undefined;
|
|
199
208
|
|
|
200
209
|
/* If no resolvers needed (all data was instant), clean up immediately */
|
|
201
210
|
cleanupIfAllEmpty();
|
|
@@ -205,9 +214,12 @@ export function installDeferredResolver(resolvers: Map<string, DeferredResolver>
|
|
|
205
214
|
* Drain buffered deferred QC entries from SSR streamed `<script>` tags,
|
|
206
215
|
* then install live proxy for late-arriving entries.
|
|
207
216
|
*
|
|
208
|
-
* SSR scripts push `[{data, key, staleTime?}]` into `self.
|
|
217
|
+
* SSR scripts push `[{data, key, staleTime?}]` into `self.__flare_queries` array.
|
|
209
218
|
* This function drains them, then replaces the array with a push-proxy
|
|
210
219
|
* so future scripts hydrate immediately.
|
|
220
|
+
*
|
|
221
|
+
* Call after `useQuery` observers have attached (a microtask after
|
|
222
|
+
* `solidHydrate`). Draining earlier writes into a cache with no subscribers.
|
|
211
223
|
*/
|
|
212
224
|
export function installQueryCacheResolver(queryClient: unknown): void {
|
|
213
225
|
if (typeof globalThis === "undefined") return;
|
|
@@ -228,7 +240,7 @@ export function installQueryCacheResolver(queryClient: unknown): void {
|
|
|
228
240
|
}
|
|
229
241
|
|
|
230
242
|
/* Drain any buffered entries from SSR script tags */
|
|
231
|
-
const queue = globalThis.
|
|
243
|
+
const queue = globalThis.__flare_queries;
|
|
232
244
|
if (Array.isArray(queue)) {
|
|
233
245
|
for (const entry of queue) {
|
|
234
246
|
applyEntry(entry[0]);
|
|
@@ -236,7 +248,7 @@ export function installQueryCacheResolver(queryClient: unknown): void {
|
|
|
236
248
|
}
|
|
237
249
|
|
|
238
250
|
/* Install push-proxy for late-arriving SSR scripts */
|
|
239
|
-
globalThis.
|
|
251
|
+
globalThis.__flare_queries = {
|
|
240
252
|
push(entry: [unknown]) {
|
|
241
253
|
applyEntry(entry[0]);
|
|
242
254
|
return 0;
|