@askrjs/askr 0.0.30 → 0.0.33

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
@@ -23,8 +23,8 @@ createIsland({ root: document.body, component: Counter });
23
23
  ### Runtime
24
24
 
25
25
  `@askrjs/askr` exports the core runtime primitives: `state()`, `derive()`,
26
- `selector()`, `resource()`, routing helpers, JSX control-flow helpers, and SSR/SSG
27
- entrypoints.
26
+ `selector()`, `resource()`, `ErrorBoundary`, routing helpers, JSX control-flow
27
+ helpers, and SSR/SSG entrypoints.
28
28
 
29
29
  ### Explicit reactivity
30
30
 
@@ -69,7 +69,25 @@ function Data({ id }: { id: string }) {
69
69
 
70
70
  if (data.pending) return <div>Loading...</div>;
71
71
  if (data.error) return <div>Failed to load</div>;
72
- return <div>{data.value.name}</div>;
72
+ return <div>{data.value.name}</div>;
73
+ }
74
+ ```
75
+
76
+ ### Developer error boundaries
77
+
78
+ `ErrorBoundary` is the opt-in boundary primitive for render-time failures. It
79
+ renders a visible fallback in development, still logs the underlying error, and
80
+ can reset when your app state changes.
81
+
82
+ ```ts
83
+ import { ErrorBoundary } from '@askrjs/askr';
84
+
85
+ function App() {
86
+ return (
87
+ <ErrorBoundary fallback={<div>Something went wrong</div>}>
88
+ <FlakyView />
89
+ </ErrorBoundary>
90
+ );
73
91
  }
74
92
  ```
75
93
 
package/dist/benchmark.js CHANGED
@@ -10,8 +10,8 @@ import { BenchmarkTable } from "./bench/components/benchmark-table.js";
10
10
  installRendererBridge();
11
11
  const benchmarkMetadata = {
12
12
  packageName: "@askrjs/askr",
13
- packageVersion: "0.0.30",
14
- buildLabel: "0.0.30-local"
13
+ packageVersion: "0.0.33",
14
+ buildLabel: "0.0.33-local"
15
15
  };
16
16
  function getBenchmarkMetadata() {
17
17
  return { ...benchmarkMetadata };
@@ -2,6 +2,7 @@ import { Props } from "./props.js";
2
2
  import { ForState } from "../runtime/for.js";
3
3
  import { ControlBoundaryState } from "../runtime/control.js";
4
4
  //#region src/common/vnode.d.ts
5
+ declare const __ERROR_BOUNDARY__: unique symbol;
5
6
  interface DOMElement {
6
7
  type: string | ((props: Props) => any) | symbol;
7
8
  props?: Props;
@@ -1 +1 @@
1
- {"version":3,"file":"vnode.d.ts","names":[],"sources":["../../src/common/vnode.ts"],"mappings":";;;;UAUiB,UAAA;EAKf,IAAA,aAAiB,KAAA,EAAO,KAAA;EACxB,KAAA,GAAQ,KAAA;EACR,QAAA,GAAW,KAAA;EACX,GAAA;EAAA,CACC,MAAA,CAAO,QAAA;EACR,aAAA,GAAgB,oBAAA;EAChB,SAAA,GAAY,QAAA;AAAA;AAAA,KAIF,KAAA,GAAQ,UAAA;AAAA,cAIP,gBAAA;AAAA,iBAEG,aAAA,CAAc,IAAA,YAAgB,IAAA,IAAQ,UAAA"}
1
+ {"version":3,"file":"vnode.d.ts","names":[],"sources":["../../src/common/vnode.ts"],"mappings":";;;;cAUa,kBAAA;AAAA,UAEI,UAAA;EAKf,IAAA,aAAiB,KAAA,EAAO,KAAA;EACxB,KAAA,GAAQ,KAAA;EACR,QAAA,GAAW,KAAA;EACX,GAAA;EAAA,CACC,MAAA,CAAO,QAAA;EACR,aAAA,GAAgB,oBAAA;EAChB,SAAA,GAAY,QAAA;AAAA;AAAA,KAIF,KAAA,GAAQ,UAAA;AAAA,cAIP,gBAAA;AAAA,iBAEG,aAAA,CAAc,IAAA,YAAgB,IAAA,IAAQ,UAAA"}
@@ -1,10 +1,11 @@
1
1
  import { __CONTROL_BOUNDARY__ } from "./control.js";
2
2
  //#region src/common/vnode.ts
3
+ const __ERROR_BOUNDARY__ = Symbol.for("askr.error-boundary");
3
4
  const __FOR_BOUNDARY__ = __CONTROL_BOUNDARY__;
4
5
  function _isDOMElement(node) {
5
6
  return typeof node === "object" && node !== null && "type" in node;
6
7
  }
7
8
  //#endregion
8
- export { __FOR_BOUNDARY__, _isDOMElement };
9
+ export { __ERROR_BOUNDARY__, __FOR_BOUNDARY__, _isDOMElement };
9
10
 
10
11
  //# sourceMappingURL=vnode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vnode.js","names":[],"sources":["../../src/common/vnode.ts"],"sourcesContent":["/**\n * Common call contracts: VNode / virtual DOM shapes\n */\n\nimport type { Props } from './props';\nimport type { ControlBoundaryState } from '../runtime/control';\nimport type { ForState } from '../runtime/for';\nexport { __CONTROL_BOUNDARY__ } from './control';\nimport { __CONTROL_BOUNDARY__ } from './control';\n\nexport interface DOMElement {\n // Element `type` can be an intrinsic tag name, a component function, or\n // a special symbol (e.g. `Fragment`). Include `symbol` in the type union\n // so runtime comparisons against `Fragment` are type-safe.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n type: string | ((props: Props) => any) | symbol;\n props?: Props;\n children?: VNode[];\n key?: string | number | null;\n [Symbol.iterator]?: never;\n _controlState?: ControlBoundaryState; // Internal: control boundary state\n _forState?: ForState<unknown>; // Deprecated internal alias during migration\n}\n\n// Type for virtual DOM nodes\nexport type VNode = DOMElement | string | number | boolean | null | undefined;\n\n// Backward-compatible internal alias while renderer/runtime migrates away from\n// the old For-only boundary naming.\nexport const __FOR_BOUNDARY__ = __CONTROL_BOUNDARY__;\n\nexport function _isDOMElement(node: unknown): node is DOMElement {\n return typeof node === 'object' && node !== null && 'type' in node;\n}\n"],"mappings":";;AA6BA,MAAa,mBAAmB;AAEhC,SAAgB,cAAc,MAAmC;AAC/D,QAAO,OAAO,SAAS,YAAY,SAAS,QAAQ,UAAU"}
1
+ {"version":3,"file":"vnode.js","names":[],"sources":["../../src/common/vnode.ts"],"sourcesContent":["/**\n * Common call contracts: VNode / virtual DOM shapes\n */\n\nimport type { Props } from './props';\nimport type { ControlBoundaryState } from '../runtime/control';\nimport type { ForState } from '../runtime/for';\nexport { __CONTROL_BOUNDARY__ } from './control';\nimport { __CONTROL_BOUNDARY__ } from './control';\n\nexport const __ERROR_BOUNDARY__ = Symbol.for('askr.error-boundary');\n\nexport interface DOMElement {\n // Element `type` can be an intrinsic tag name, a component function, or\n // a special symbol (e.g. `Fragment`). Include `symbol` in the type union\n // so runtime comparisons against `Fragment` are type-safe.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n type: string | ((props: Props) => any) | symbol;\n props?: Props;\n children?: VNode[];\n key?: string | number | null;\n [Symbol.iterator]?: never;\n _controlState?: ControlBoundaryState; // Internal: control boundary state\n _forState?: ForState<unknown>; // Deprecated internal alias during migration\n}\n\n// Type for virtual DOM nodes\nexport type VNode = DOMElement | string | number | boolean | null | undefined;\n\n// Backward-compatible internal alias while renderer/runtime migrates away from\n// the old For-only boundary naming.\nexport const __FOR_BOUNDARY__ = __CONTROL_BOUNDARY__;\n\nexport function _isDOMElement(node: unknown): node is DOMElement {\n return typeof node === 'object' && node !== null && 'type' in node;\n}\n"],"mappings":";;AAUA,MAAa,qBAAqB,OAAO,IAAI,sBAAsB;AAqBnE,MAAa,mBAAmB;AAEhC,SAAgB,cAAc,MAAmC;AAC/D,QAAO,OAAO,SAAS,YAAY,SAAS,QAAQ,UAAU"}
@@ -0,0 +1,25 @@
1
+ import { Props } from "../common/props.js";
2
+ import { JSXElement } from "../common/jsx.js";
3
+ import { ComponentInstance } from "../runtime/component.js";
4
+
5
+ //#region src/components/error-boundary.d.ts
6
+ type ErrorBoundaryFallbackRender = (error: unknown, reset: () => void) => unknown;
7
+ interface ErrorBoundaryProps extends Props {
8
+ children?: unknown;
9
+ fallback?: unknown | ErrorBoundaryFallbackRender;
10
+ onError?: (error: unknown) => void;
11
+ resetKey?: unknown;
12
+ }
13
+ type ErrorBoundaryState = NonNullable<ComponentInstance['errorBoundaryState']>;
14
+ type ErrorBoundaryVNode = JSXElement & {
15
+ __instance?: ComponentInstance;
16
+ };
17
+ declare function ErrorBoundary(props: ErrorBoundaryProps): JSXElement;
18
+ declare function isErrorBoundaryVNode(value: unknown): value is ErrorBoundaryVNode;
19
+ declare function resolveErrorBoundaryState(vnode: ErrorBoundaryVNode): ErrorBoundaryState | null;
20
+ declare function resolveErrorBoundaryFallback(fallback: ErrorBoundaryProps['fallback'], error: unknown, reset: () => void): unknown;
21
+ declare function createBoundaryReset(instance: ComponentInstance): () => void;
22
+ declare function reportBoundaryError(instance: ComponentInstance, error: unknown, onError?: (error: unknown) => void): void;
23
+ //#endregion
24
+ export { ErrorBoundary, ErrorBoundaryFallbackRender, ErrorBoundaryProps };
25
+ //# sourceMappingURL=error-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.d.ts","names":[],"sources":["../../src/components/error-boundary.tsx"],"mappings":";;;;;KAUY,2BAAA,IACV,KAAA,WACA,KAAA;AAAA,UAGe,kBAAA,SAA2B,KAAA;EAC1C,QAAA;EACA,QAAA,aAAqB,2BAAA;EACrB,OAAA,IAAW,KAAA;EACX,QAAA;AAAA;AAAA,KAGG,kBAAA,GAAqB,WAAA,CAAY,iBAAA;AAAA,KAEjC,kBAAA,GAAqB,UAAA;EACxB,UAAA,GAAa,iBAAA;AAAA;AAAA,iBA+DC,aAAA,CAAc,KAAA,EAAO,kBAAA,GAAqB,UAAA;AAAA,iBAY1C,oBAAA,CACd,KAAA,YACC,KAAA,IAAS,kBAAA;AAAA,iBAQI,yBAAA,CACd,KAAA,EAAO,kBAAA,GACN,kBAAA;AAAA,iBAIa,4BAAA,CACd,QAAA,EAAU,kBAAA,cACV,KAAA,WACA,KAAA;AAAA,iBAoDc,mBAAA,CAAoB,QAAA,EAAU,iBAAA;AAAA,iBAc9B,mBAAA,CACd,QAAA,EAAU,iBAAA,EACV,KAAA,WACA,OAAA,IAAW,KAAA"}
@@ -0,0 +1,114 @@
1
+ import { __ERROR_BOUNDARY__ } from "../common/vnode.js";
2
+ import { isDevelopmentEnvironment } from "../common/env.js";
3
+ import { logger } from "../dev/logger.js";
4
+ import { ELEMENT_TYPE } from "../common/jsx.js";
5
+ import { getCurrentComponentInstance } from "../runtime/component.js";
6
+ //#region src/components/error-boundary.tsx
7
+ function getBoundaryMessage(error) {
8
+ if (error instanceof Error) return error.message || error.name || "Unknown error";
9
+ if (typeof error === "string") return error;
10
+ try {
11
+ return JSON.stringify(error);
12
+ } catch {
13
+ return String(error);
14
+ }
15
+ }
16
+ function ensureBoundaryState(instance, resetKey) {
17
+ const boundaryState = instance.errorBoundaryState ?? (instance.errorBoundaryState = {
18
+ error: null,
19
+ resetKey,
20
+ notified: false
21
+ });
22
+ if (!Object.is(boundaryState.resetKey, resetKey)) {
23
+ boundaryState.error = null;
24
+ boundaryState.resetKey = resetKey;
25
+ boundaryState.notified = false;
26
+ }
27
+ return boundaryState;
28
+ }
29
+ function createBoundaryVNode(instance, props) {
30
+ const key = typeof props.key === "symbol" ? null : props.key ?? null;
31
+ return {
32
+ $$typeof: ELEMENT_TYPE,
33
+ type: __ERROR_BOUNDARY__,
34
+ props: {
35
+ children: props.children,
36
+ fallback: props.fallback,
37
+ onError: props.onError,
38
+ resetKey: props.resetKey
39
+ },
40
+ key,
41
+ __instance: instance
42
+ };
43
+ }
44
+ function ErrorBoundary(props) {
45
+ const instance = getCurrentComponentInstance();
46
+ if (!instance) throw new Error("[Askr] ErrorBoundary() can only be used during component render execution.");
47
+ ensureBoundaryState(instance, props.resetKey);
48
+ return createBoundaryVNode(instance, props);
49
+ }
50
+ function resolveErrorBoundaryFallback(fallback, error, reset) {
51
+ if (typeof fallback === "function") return fallback(error, reset);
52
+ if (fallback !== void 0) return fallback;
53
+ const message = getBoundaryMessage(error);
54
+ const wrapper = document.createElement("div");
55
+ wrapper.setAttribute("role", "alert");
56
+ wrapper.setAttribute("data-askr-error-boundary", "true");
57
+ wrapper.style.boxSizing = "border-box";
58
+ wrapper.style.padding = "1rem";
59
+ wrapper.style.border = "1px solid currentColor";
60
+ wrapper.style.borderRadius = "0.75rem";
61
+ wrapper.style.display = "grid";
62
+ wrapper.style.gap = "0.75rem";
63
+ wrapper.style.maxWidth = "100%";
64
+ const title = document.createElement("strong");
65
+ title.textContent = "Something went wrong while rendering this view.";
66
+ const summary = document.createElement("p");
67
+ summary.textContent = "The app recovered into a visible fallback so the error is not hidden in the console.";
68
+ summary.style.margin = "0";
69
+ const details = document.createElement("details");
70
+ details.open = isDevelopmentEnvironment();
71
+ const detailsSummary = document.createElement("summary");
72
+ detailsSummary.textContent = "Error details";
73
+ const pre = document.createElement("pre");
74
+ pre.textContent = message;
75
+ pre.style.margin = "0";
76
+ pre.style.whiteSpace = "pre-wrap";
77
+ pre.style.wordBreak = "break-word";
78
+ const button = document.createElement("button");
79
+ button.type = "button";
80
+ button.textContent = "Try again";
81
+ button.addEventListener("click", () => reset());
82
+ details.append(detailsSummary, pre);
83
+ wrapper.append(title, summary, details, button);
84
+ return wrapper;
85
+ }
86
+ function createBoundaryReset(instance) {
87
+ return () => {
88
+ const boundaryState = instance.errorBoundaryState;
89
+ if (!boundaryState) return;
90
+ boundaryState.error = null;
91
+ boundaryState.notified = false;
92
+ queueMicrotask(() => {
93
+ instance._enqueueRun?.();
94
+ });
95
+ };
96
+ }
97
+ function reportBoundaryError(instance, error, onError) {
98
+ const boundaryState = instance.errorBoundaryState;
99
+ if (boundaryState && Object.is(boundaryState.error, error) && boundaryState.notified) return;
100
+ if (boundaryState) {
101
+ boundaryState.error = error;
102
+ boundaryState.notified = true;
103
+ }
104
+ try {
105
+ onError?.(error);
106
+ } catch (hookError) {
107
+ logger.error("[Askr] ErrorBoundary onError handler threw:", hookError);
108
+ }
109
+ logger.error("[Askr] ErrorBoundary caught render error:", error);
110
+ }
111
+ //#endregion
112
+ export { ErrorBoundary, createBoundaryReset, reportBoundaryError, resolveErrorBoundaryFallback };
113
+
114
+ //# sourceMappingURL=error-boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-boundary.js","names":[],"sources":["../../src/components/error-boundary.tsx"],"sourcesContent":["import type { Props } from '../common/props';\nimport { ELEMENT_TYPE, type JSXElement } from '../common/jsx';\nimport { __ERROR_BOUNDARY__ } from '../common/vnode';\nimport {\n getCurrentComponentInstance,\n type ComponentInstance,\n} from '../runtime/component';\nimport { logger } from '../dev/logger';\nimport { isDevelopmentEnvironment } from '../common/env';\n\nexport type ErrorBoundaryFallbackRender = (\n error: unknown,\n reset: () => void\n) => unknown;\n\nexport interface ErrorBoundaryProps extends Props {\n children?: unknown;\n fallback?: unknown | ErrorBoundaryFallbackRender;\n onError?: (error: unknown) => void;\n resetKey?: unknown;\n}\n\ntype ErrorBoundaryState = NonNullable<ComponentInstance['errorBoundaryState']>;\n\ntype ErrorBoundaryVNode = JSXElement & {\n __instance?: ComponentInstance;\n};\n\nfunction getBoundaryMessage(error: unknown): string {\n if (error instanceof Error) {\n return error.message || error.name || 'Unknown error';\n }\n\n if (typeof error === 'string') {\n return error;\n }\n\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n}\n\nfunction ensureBoundaryState(\n instance: ComponentInstance,\n resetKey: unknown\n): ErrorBoundaryState {\n const boundaryState =\n instance.errorBoundaryState ??\n (instance.errorBoundaryState = {\n error: null,\n resetKey,\n notified: false,\n });\n\n if (!Object.is(boundaryState.resetKey, resetKey)) {\n boundaryState.error = null;\n boundaryState.resetKey = resetKey;\n boundaryState.notified = false;\n }\n\n return boundaryState;\n}\n\nfunction createBoundaryVNode(\n instance: ComponentInstance,\n props: ErrorBoundaryProps\n): ErrorBoundaryVNode {\n const key =\n typeof props.key === 'symbol'\n ? null\n : ((props.key ?? null) as string | number | null);\n\n return {\n $$typeof: ELEMENT_TYPE,\n type: __ERROR_BOUNDARY__,\n props: {\n children: props.children,\n fallback: props.fallback,\n onError: props.onError,\n resetKey: props.resetKey,\n },\n key,\n __instance: instance,\n };\n}\n\nexport function ErrorBoundary(props: ErrorBoundaryProps): JSXElement {\n const instance = getCurrentComponentInstance();\n if (!instance) {\n throw new Error(\n '[Askr] ErrorBoundary() can only be used during component render execution.'\n );\n }\n\n ensureBoundaryState(instance, props.resetKey);\n return createBoundaryVNode(instance, props);\n}\n\nexport function isErrorBoundaryVNode(\n value: unknown\n): value is ErrorBoundaryVNode {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as ErrorBoundaryVNode).type === __ERROR_BOUNDARY__\n );\n}\n\nexport function resolveErrorBoundaryState(\n vnode: ErrorBoundaryVNode\n): ErrorBoundaryState | null {\n return vnode.__instance?.errorBoundaryState ?? null;\n}\n\nexport function resolveErrorBoundaryFallback(\n fallback: ErrorBoundaryProps['fallback'],\n error: unknown,\n reset: () => void\n): unknown {\n if (typeof fallback === 'function') {\n return fallback(error, reset);\n }\n\n if (fallback !== undefined) {\n return fallback;\n }\n\n const message = getBoundaryMessage(error);\n const wrapper = document.createElement('div');\n wrapper.setAttribute('role', 'alert');\n wrapper.setAttribute('data-askr-error-boundary', 'true');\n wrapper.style.boxSizing = 'border-box';\n wrapper.style.padding = '1rem';\n wrapper.style.border = '1px solid currentColor';\n wrapper.style.borderRadius = '0.75rem';\n wrapper.style.display = 'grid';\n wrapper.style.gap = '0.75rem';\n wrapper.style.maxWidth = '100%';\n\n const title = document.createElement('strong');\n title.textContent = 'Something went wrong while rendering this view.';\n\n const summary = document.createElement('p');\n summary.textContent =\n 'The app recovered into a visible fallback so the error is not hidden in the console.';\n summary.style.margin = '0';\n\n const details = document.createElement('details');\n details.open = isDevelopmentEnvironment();\n\n const detailsSummary = document.createElement('summary');\n detailsSummary.textContent = 'Error details';\n\n const pre = document.createElement('pre');\n pre.textContent = message;\n pre.style.margin = '0';\n pre.style.whiteSpace = 'pre-wrap';\n pre.style.wordBreak = 'break-word';\n\n const button = document.createElement('button');\n button.type = 'button';\n button.textContent = 'Try again';\n button.addEventListener('click', () => reset());\n\n details.append(detailsSummary, pre);\n wrapper.append(title, summary, details, button);\n return wrapper;\n}\n\nexport function createBoundaryReset(instance: ComponentInstance): () => void {\n return () => {\n const boundaryState = instance.errorBoundaryState;\n if (!boundaryState) {\n return;\n }\n boundaryState.error = null;\n boundaryState.notified = false;\n queueMicrotask(() => {\n instance._enqueueRun?.();\n });\n };\n}\n\nexport function reportBoundaryError(\n instance: ComponentInstance,\n error: unknown,\n onError?: (error: unknown) => void\n): void {\n const boundaryState = instance.errorBoundaryState;\n if (\n boundaryState &&\n Object.is(boundaryState.error, error) &&\n boundaryState.notified\n ) {\n return;\n }\n\n if (boundaryState) {\n boundaryState.error = error;\n boundaryState.notified = true;\n }\n\n try {\n onError?.(error);\n } catch (hookError) {\n logger.error('[Askr] ErrorBoundary onError handler threw:', hookError);\n }\n\n logger.error('[Askr] ErrorBoundary caught render error:', error);\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ //#region src/data/index.d.ts
2
+ type QueryConsistency = 'fresh' | 'stale' | 'refreshing' | 'pending-write';
3
+ type Query<T> = {
4
+ data: T | null;
5
+ error: unknown | null;
6
+ loading: boolean;
7
+ refreshing: boolean;
8
+ stale: boolean;
9
+ consistency: QueryConsistency;
10
+ refresh(): Promise<void>;
11
+ };
12
+ type Mutation<TInput, TResult> = {
13
+ status: 'idle' | 'pending' | 'success' | 'error';
14
+ pending: boolean;
15
+ error: unknown | null;
16
+ result: TResult | null;
17
+ execute(input: TInput): Promise<TResult>;
18
+ abort(): void;
19
+ reset(): void;
20
+ };
21
+ type QueryOptions<T> = {
22
+ key: string;
23
+ fetch: (ctx: {
24
+ signal: AbortSignal;
25
+ }) => Promise<T>;
26
+ isConsistent?: (data: T) => boolean;
27
+ reconcile?: (data: T, ctx: {
28
+ key: string;
29
+ }) => Promise<boolean> | boolean;
30
+ };
31
+ type MutationOptions<TInput, TResult> = {
32
+ action: (input: TInput, ctx: {
33
+ signal: AbortSignal;
34
+ }) => Promise<TResult>;
35
+ affects?: (input: TInput, result: TResult) => string[];
36
+ afterSuccess?: 'invalidate';
37
+ };
38
+ declare function createQuery<T>(options: QueryOptions<T>): Query<T>;
39
+ declare function invalidate(prefix: string): void;
40
+ declare function createMutation<TInput, TResult>(options: MutationOptions<TInput, TResult>): Mutation<TInput, TResult>;
41
+ //#endregion
42
+ export { Mutation, Query, QueryConsistency, createMutation, createQuery, invalidate };
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/data/index.ts"],"mappings":";KASY,gBAAA;AAAA,KAMA,KAAA;EACV,IAAA,EAAM,CAAA;EACN,KAAA;EACA,OAAA;EACA,UAAA;EACA,KAAA;EACA,WAAA,EAAa,gBAAA;EAEb,OAAA,IAAW,OAAA;AAAA;AAAA,KAGD,QAAA;EACV,MAAA;EACA,OAAA;EACA,KAAA;EACA,MAAA,EAAQ,OAAA;EAER,OAAA,CAAQ,KAAA,EAAO,MAAA,GAAS,OAAA,CAAQ,OAAA;EAChC,KAAA;EACA,KAAA;AAAA;AAAA,KAGG,YAAA;EACH,GAAA;EACA,KAAA,GAAQ,GAAA;IAAO,MAAA,EAAQ,WAAA;EAAA,MAAkB,OAAA,CAAQ,CAAA;EACjD,YAAA,IAAgB,IAAA,EAAM,CAAA;EACtB,SAAA,IAAa,IAAA,EAAM,CAAA,EAAG,GAAA;IAAO,GAAA;EAAA,MAAkB,OAAA;AAAA;AAAA,KAG5C,eAAA;EACH,MAAA,GAAS,KAAA,EAAO,MAAA,EAAQ,GAAA;IAAO,MAAA,EAAQ,WAAA;EAAA,MAAkB,OAAA,CAAQ,OAAA;EACjE,OAAA,IAAW,KAAA,EAAO,MAAA,EAAQ,MAAA,EAAQ,OAAA;EAClC,YAAA;AAAA;AAAA,iBA6Wc,WAAA,GAAA,CAAe,OAAA,EAAS,YAAA,CAAa,CAAA,IAAK,KAAA,CAAM,CAAA;AAAA,iBAahD,UAAA,CAAW,MAAA;AAAA,iBAIX,cAAA,iBAAA,CACd,OAAA,EAAS,eAAA,CAAgB,MAAA,EAAQ,OAAA,IAChC,QAAA,CAAS,MAAA,EAAQ,OAAA"}
@@ -0,0 +1,293 @@
1
+ import { globalScheduler } from "../runtime/scheduler.js";
2
+ import { markReactivePropsDirtySource, markReadableDerivedSubscribersDirty, notifyReadableReaders, recordReadableRead } from "../runtime/readable.js";
3
+ //#region src/data/index.ts
4
+ function createReadableSource() {
5
+ return (() => void 0);
6
+ }
7
+ function notifySource(source) {
8
+ markReadableDerivedSubscribersDirty(source);
9
+ markReactivePropsDirtySource(source);
10
+ notifyReadableReaders(source);
11
+ }
12
+ function isAbortError(error, signal) {
13
+ return signal.aborted || error instanceof Error && error.name === "AbortError" || typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError";
14
+ }
15
+ const queryCache = /* @__PURE__ */ new Map();
16
+ function invalidateQueries(prefix, markPendingWrite) {
17
+ for (const [key, query] of queryCache) {
18
+ if (!key.startsWith(prefix)) continue;
19
+ if (markPendingWrite) query.markPendingWrite();
20
+ query.refresh();
21
+ }
22
+ }
23
+ var QueryCell = class {
24
+ constructor(options) {
25
+ this.source = createReadableSource();
26
+ this.controller = null;
27
+ this.generation = 0;
28
+ this.startQueued = false;
29
+ this.pendingRefresh = null;
30
+ this.reconcileAttemptCount = 0;
31
+ this.state = {
32
+ data: null,
33
+ error: null,
34
+ loading: true,
35
+ refreshing: false,
36
+ stale: false,
37
+ consistency: "fresh"
38
+ };
39
+ this.options = options;
40
+ }
41
+ setOptions(options) {
42
+ this.options = options;
43
+ }
44
+ get data() {
45
+ recordReadableRead(this.source);
46
+ return this.state.data;
47
+ }
48
+ get error() {
49
+ recordReadableRead(this.source);
50
+ return this.state.error;
51
+ }
52
+ get loading() {
53
+ recordReadableRead(this.source);
54
+ return this.state.loading;
55
+ }
56
+ get refreshing() {
57
+ recordReadableRead(this.source);
58
+ return this.state.refreshing;
59
+ }
60
+ get stale() {
61
+ recordReadableRead(this.source);
62
+ return this.state.stale;
63
+ }
64
+ get consistency() {
65
+ recordReadableRead(this.source);
66
+ return this.state.consistency;
67
+ }
68
+ ensureStarted() {
69
+ if (this.state.data !== null || this.pendingRefresh || this.startQueued) return;
70
+ this.queueStart("initial");
71
+ }
72
+ refresh() {
73
+ if (this.pendingRefresh) return this.pendingRefresh;
74
+ this.queueStart("manual");
75
+ return this.pendingRefresh ?? Promise.resolve();
76
+ }
77
+ markPendingWrite() {
78
+ this.setState({
79
+ refreshing: true,
80
+ stale: true,
81
+ consistency: "pending-write"
82
+ });
83
+ }
84
+ queueStart(reason) {
85
+ this.startQueued = true;
86
+ this.pendingRefresh = new Promise((resolve) => {
87
+ globalScheduler.enqueue(() => {
88
+ this.startQueued = false;
89
+ this.start(reason).finally(() => {
90
+ this.pendingRefresh = null;
91
+ resolve();
92
+ });
93
+ });
94
+ });
95
+ }
96
+ setState(next) {
97
+ this.state = {
98
+ ...this.state,
99
+ ...next
100
+ };
101
+ notifySource(this.source);
102
+ }
103
+ async start(reason) {
104
+ this.generation += 1;
105
+ const generation = this.generation;
106
+ this.controller?.abort();
107
+ const controller = new AbortController();
108
+ this.controller = controller;
109
+ const hasData = this.state.data !== null;
110
+ this.setState({
111
+ loading: !hasData,
112
+ refreshing: hasData,
113
+ stale: hasData,
114
+ consistency: reason === "pending-write" ? "pending-write" : hasData ? "refreshing" : "fresh",
115
+ error: null
116
+ });
117
+ let nextData;
118
+ try {
119
+ nextData = await this.options.fetch({ signal: controller.signal });
120
+ } catch (error) {
121
+ if (this.generation !== generation || this.controller !== controller) return;
122
+ if (isAbortError(error, controller.signal)) {
123
+ this.setState({
124
+ loading: false,
125
+ refreshing: false
126
+ });
127
+ return;
128
+ }
129
+ this.setState({
130
+ loading: false,
131
+ refreshing: false,
132
+ stale: true,
133
+ consistency: "stale",
134
+ error
135
+ });
136
+ return;
137
+ }
138
+ if (this.generation !== generation || this.controller !== controller) return;
139
+ if (!(this.options.isConsistent?.(nextData) ?? true)) {
140
+ this.setState({
141
+ data: nextData,
142
+ loading: false,
143
+ refreshing: false,
144
+ stale: true,
145
+ consistency: "stale"
146
+ });
147
+ await this.reconcile(nextData);
148
+ return;
149
+ }
150
+ this.reconcileAttemptCount = 0;
151
+ this.setState({
152
+ data: nextData,
153
+ loading: false,
154
+ refreshing: false,
155
+ stale: false,
156
+ consistency: "fresh",
157
+ error: null
158
+ });
159
+ }
160
+ async reconcile(data) {
161
+ if (!(this.options.reconcile?.(data, { key: this.options.key }) ?? false)) return;
162
+ this.reconcileAttemptCount += 1;
163
+ if (this.reconcileAttemptCount > 3) {
164
+ this.setState({
165
+ consistency: "stale",
166
+ refreshing: false
167
+ });
168
+ return;
169
+ }
170
+ await new Promise((resolve) => setTimeout(resolve, 25));
171
+ if (this.state.consistency === "fresh") return;
172
+ await this.refresh();
173
+ }
174
+ };
175
+ var MutationCell = class {
176
+ constructor(options) {
177
+ this.source = createReadableSource();
178
+ this.controller = null;
179
+ this.generation = 0;
180
+ this.state = {
181
+ status: "idle",
182
+ error: null,
183
+ result: null
184
+ };
185
+ this.action = options.action;
186
+ this.affects = options.affects;
187
+ this.afterSuccess = options.afterSuccess;
188
+ }
189
+ get status() {
190
+ recordReadableRead(this.source);
191
+ return this.state.status;
192
+ }
193
+ get pending() {
194
+ recordReadableRead(this.source);
195
+ return this.state.status === "pending";
196
+ }
197
+ get error() {
198
+ recordReadableRead(this.source);
199
+ return this.state.error;
200
+ }
201
+ get result() {
202
+ recordReadableRead(this.source);
203
+ return this.state.result;
204
+ }
205
+ setState(next) {
206
+ this.state = {
207
+ ...this.state,
208
+ ...next
209
+ };
210
+ notifySource(this.source);
211
+ }
212
+ async execute(input) {
213
+ this.generation += 1;
214
+ const generation = this.generation;
215
+ this.controller?.abort();
216
+ const controller = new AbortController();
217
+ this.controller = controller;
218
+ this.setState({
219
+ status: "pending",
220
+ error: null,
221
+ result: null
222
+ });
223
+ let result;
224
+ try {
225
+ result = await this.action(input, { signal: controller.signal });
226
+ } catch (error) {
227
+ if (this.generation !== generation || this.controller !== controller) throw error;
228
+ if (isAbortError(error, controller.signal)) {
229
+ this.setState({
230
+ status: "idle",
231
+ error: null
232
+ });
233
+ throw error;
234
+ }
235
+ this.setState({
236
+ status: "error",
237
+ error
238
+ });
239
+ throw error;
240
+ }
241
+ if (this.generation !== generation || this.controller !== controller) return result;
242
+ this.setState({
243
+ status: "success",
244
+ error: null,
245
+ result
246
+ });
247
+ if (this.afterSuccess === "invalidate") {
248
+ const prefixes = this.affects?.(input, result) ?? [];
249
+ for (const prefix of new Set(prefixes)) invalidateQueries(prefix, true);
250
+ }
251
+ return result;
252
+ }
253
+ abort() {
254
+ this.generation += 1;
255
+ this.controller?.abort();
256
+ this.controller = null;
257
+ this.setState({
258
+ status: "idle",
259
+ error: null
260
+ });
261
+ }
262
+ reset() {
263
+ this.generation += 1;
264
+ this.controller?.abort();
265
+ this.controller = null;
266
+ this.setState({
267
+ status: "idle",
268
+ error: null,
269
+ result: null
270
+ });
271
+ }
272
+ };
273
+ function createQuery(options) {
274
+ const existing = queryCache.get(options.key);
275
+ if (existing) {
276
+ existing.setOptions(options);
277
+ return existing;
278
+ }
279
+ const created = new QueryCell(options);
280
+ queryCache.set(options.key, created);
281
+ created.ensureStarted();
282
+ return created;
283
+ }
284
+ function invalidate(prefix) {
285
+ invalidateQueries(prefix, false);
286
+ }
287
+ function createMutation(options) {
288
+ return new MutationCell(options);
289
+ }
290
+ //#endregion
291
+ export { createMutation, createQuery, invalidate };
292
+
293
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/data/index.ts"],"sourcesContent":["import { globalScheduler } from '../runtime/scheduler';\nimport {\n markReadableDerivedSubscribersDirty,\n markReactivePropsDirtySource,\n notifyReadableReaders,\n recordReadableRead,\n type ReadableSource,\n} from '../runtime/readable';\n\nexport type QueryConsistency =\n | 'fresh'\n | 'stale'\n | 'refreshing'\n | 'pending-write';\n\nexport type Query<T> = {\n data: T | null;\n error: unknown | null;\n loading: boolean;\n refreshing: boolean;\n stale: boolean;\n consistency: QueryConsistency;\n\n refresh(): Promise<void>;\n};\n\nexport type Mutation<TInput, TResult> = {\n status: 'idle' | 'pending' | 'success' | 'error';\n pending: boolean;\n error: unknown | null;\n result: TResult | null;\n\n execute(input: TInput): Promise<TResult>;\n abort(): void;\n reset(): void;\n};\n\ntype QueryOptions<T> = {\n key: string;\n fetch: (ctx: { signal: AbortSignal }) => Promise<T>;\n isConsistent?: (data: T) => boolean;\n reconcile?: (data: T, ctx: { key: string }) => Promise<boolean> | boolean;\n};\n\ntype MutationOptions<TInput, TResult> = {\n action: (input: TInput, ctx: { signal: AbortSignal }) => Promise<TResult>;\n affects?: (input: TInput, result: TResult) => string[];\n afterSuccess?: 'invalidate';\n};\n\ntype QueryState<T> = {\n data: T | null;\n error: unknown | null;\n loading: boolean;\n refreshing: boolean;\n stale: boolean;\n consistency: QueryConsistency;\n};\n\ntype MutationState<TResult> = {\n status: 'idle' | 'pending' | 'success' | 'error';\n error: unknown | null;\n result: TResult | null;\n};\n\nfunction createReadableSource(): ReadableSource<unknown> {\n return (() => undefined) as ReadableSource<unknown>;\n}\n\nfunction notifySource(source: ReadableSource<unknown>): void {\n markReadableDerivedSubscribersDirty(source);\n markReactivePropsDirtySource(source);\n notifyReadableReaders(source);\n}\n\nfunction isAbortError(error: unknown, signal: AbortSignal): boolean {\n return (\n signal.aborted ||\n (error instanceof Error && error.name === 'AbortError') ||\n (typeof DOMException !== 'undefined' &&\n error instanceof DOMException &&\n error.name === 'AbortError')\n );\n}\n\nconst queryCache = new Map<string, QueryCell<unknown>>();\n\nfunction invalidateQueries(prefix: string, markPendingWrite: boolean): void {\n for (const [key, query] of queryCache) {\n if (!key.startsWith(prefix)) {\n continue;\n }\n\n if (markPendingWrite) {\n query.markPendingWrite();\n }\n\n void query.refresh();\n }\n}\n\nclass QueryCell<T> implements Query<T> {\n private readonly source = createReadableSource();\n private options: QueryOptions<T>;\n private controller: AbortController | null = null;\n private generation = 0;\n private startQueued = false;\n private pendingRefresh: Promise<void> | null = null;\n private reconcileAttemptCount = 0;\n\n private state: QueryState<T> = {\n data: null,\n error: null,\n loading: true,\n refreshing: false,\n stale: false,\n consistency: 'fresh',\n };\n\n constructor(options: QueryOptions<T>) {\n this.options = options;\n }\n\n setOptions(options: QueryOptions<T>): void {\n this.options = options;\n }\n\n get data(): T | null {\n recordReadableRead(this.source);\n return this.state.data;\n }\n\n get error(): unknown | null {\n recordReadableRead(this.source);\n return this.state.error;\n }\n\n get loading(): boolean {\n recordReadableRead(this.source);\n return this.state.loading;\n }\n\n get refreshing(): boolean {\n recordReadableRead(this.source);\n return this.state.refreshing;\n }\n\n get stale(): boolean {\n recordReadableRead(this.source);\n return this.state.stale;\n }\n\n get consistency(): QueryConsistency {\n recordReadableRead(this.source);\n return this.state.consistency;\n }\n\n ensureStarted(): void {\n if (this.state.data !== null || this.pendingRefresh || this.startQueued) {\n return;\n }\n\n this.queueStart('initial');\n }\n\n refresh(): Promise<void> {\n if (this.pendingRefresh) {\n return this.pendingRefresh;\n }\n\n this.queueStart('manual');\n return this.pendingRefresh ?? Promise.resolve();\n }\n\n markPendingWrite(): void {\n this.setState({\n refreshing: true,\n stale: true,\n consistency: 'pending-write',\n });\n }\n\n private queueStart(\n reason: 'initial' | 'manual' | 'invalidate' | 'pending-write'\n ): void {\n this.startQueued = true;\n this.pendingRefresh = new Promise<void>((resolve) => {\n globalScheduler.enqueue(() => {\n this.startQueued = false;\n void this.start(reason).finally(() => {\n this.pendingRefresh = null;\n resolve();\n });\n });\n });\n }\n\n private setState(next: Partial<QueryState<T>>): void {\n this.state = {\n ...this.state,\n ...next,\n };\n notifySource(this.source);\n }\n\n private async start(\n reason: 'initial' | 'manual' | 'invalidate' | 'pending-write'\n ): Promise<void> {\n this.generation += 1;\n const generation = this.generation;\n\n this.controller?.abort();\n const controller = new AbortController();\n this.controller = controller;\n\n const hasData = this.state.data !== null;\n this.setState({\n loading: !hasData,\n refreshing: hasData,\n stale: hasData,\n consistency:\n reason === 'pending-write'\n ? 'pending-write'\n : hasData\n ? 'refreshing'\n : 'fresh',\n error: null,\n });\n\n let nextData: T;\n try {\n nextData = await this.options.fetch({ signal: controller.signal });\n } catch (error) {\n if (this.generation !== generation || this.controller !== controller) {\n return;\n }\n\n if (isAbortError(error, controller.signal)) {\n this.setState({ loading: false, refreshing: false });\n return;\n }\n\n this.setState({\n loading: false,\n refreshing: false,\n stale: true,\n consistency: 'stale',\n error,\n });\n return;\n }\n\n if (this.generation !== generation || this.controller !== controller) {\n return;\n }\n\n const isConsistent = this.options.isConsistent?.(nextData) ?? true;\n if (!isConsistent) {\n this.setState({\n data: nextData,\n loading: false,\n refreshing: false,\n stale: true,\n consistency: 'stale',\n });\n await this.reconcile(nextData);\n return;\n }\n\n this.reconcileAttemptCount = 0;\n this.setState({\n data: nextData,\n loading: false,\n refreshing: false,\n stale: false,\n consistency: 'fresh',\n error: null,\n });\n }\n\n private async reconcile(data: T): Promise<void> {\n const shouldRetry =\n this.options.reconcile?.(data, { key: this.options.key }) ?? false;\n\n if (!shouldRetry) {\n return;\n }\n\n this.reconcileAttemptCount += 1;\n if (this.reconcileAttemptCount > 3) {\n this.setState({ consistency: 'stale', refreshing: false });\n return;\n }\n\n await new Promise<void>((resolve) => setTimeout(resolve, 25));\n if (this.state.consistency === 'fresh') {\n return;\n }\n\n await this.refresh();\n }\n}\n\nclass MutationCell<TInput, TResult> implements Mutation<TInput, TResult> {\n private readonly source = createReadableSource();\n private readonly action: MutationOptions<TInput, TResult>['action'];\n private readonly affects?: MutationOptions<TInput, TResult>['affects'];\n private readonly afterSuccess?: MutationOptions<\n TInput,\n TResult\n >['afterSuccess'];\n private controller: AbortController | null = null;\n private generation = 0;\n\n private state: MutationState<TResult> = {\n status: 'idle',\n error: null,\n result: null,\n };\n\n constructor(options: MutationOptions<TInput, TResult>) {\n this.action = options.action;\n this.affects = options.affects;\n this.afterSuccess = options.afterSuccess;\n }\n\n get status(): 'idle' | 'pending' | 'success' | 'error' {\n recordReadableRead(this.source);\n return this.state.status;\n }\n\n get pending(): boolean {\n recordReadableRead(this.source);\n return this.state.status === 'pending';\n }\n\n get error(): unknown | null {\n recordReadableRead(this.source);\n return this.state.error;\n }\n\n get result(): TResult | null {\n recordReadableRead(this.source);\n return this.state.result;\n }\n\n private setState(next: Partial<MutationState<TResult>>): void {\n this.state = {\n ...this.state,\n ...next,\n };\n notifySource(this.source);\n }\n\n async execute(input: TInput): Promise<TResult> {\n this.generation += 1;\n const generation = this.generation;\n\n this.controller?.abort();\n const controller = new AbortController();\n this.controller = controller;\n\n this.setState({ status: 'pending', error: null, result: null });\n\n let result: TResult;\n try {\n result = await this.action(input, { signal: controller.signal });\n } catch (error) {\n if (this.generation !== generation || this.controller !== controller) {\n throw error;\n }\n\n if (isAbortError(error, controller.signal)) {\n this.setState({ status: 'idle', error: null });\n throw error;\n }\n\n this.setState({ status: 'error', error });\n throw error;\n }\n\n if (this.generation !== generation || this.controller !== controller) {\n return result;\n }\n\n this.setState({ status: 'success', error: null, result });\n\n if (this.afterSuccess === 'invalidate') {\n const prefixes = this.affects?.(input, result) ?? [];\n for (const prefix of new Set(prefixes)) {\n invalidateQueries(prefix, true);\n }\n }\n\n return result;\n }\n\n abort(): void {\n this.generation += 1;\n this.controller?.abort();\n this.controller = null;\n this.setState({ status: 'idle', error: null });\n }\n\n reset(): void {\n this.generation += 1;\n this.controller?.abort();\n this.controller = null;\n this.setState({ status: 'idle', error: null, result: null });\n }\n}\n\nexport function createQuery<T>(options: QueryOptions<T>): Query<T> {\n const existing = queryCache.get(options.key) as QueryCell<T> | undefined;\n if (existing) {\n existing.setOptions(options);\n return existing;\n }\n\n const created = new QueryCell(options);\n queryCache.set(options.key, created as QueryCell<unknown>);\n created.ensureStarted();\n return created;\n}\n\nexport function invalidate(prefix: string): void {\n invalidateQueries(prefix, false);\n}\n\nexport function createMutation<TInput, TResult>(\n options: MutationOptions<TInput, TResult>\n): Mutation<TInput, TResult> {\n return new MutationCell(options);\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -15,7 +15,9 @@ import { DefaultPortal, Portal, PortalProps, definePortal } from "./foundations/
15
15
  import { Derived, derive } from "./runtime/derive.js";
16
16
  import { Selector, selector } from "./runtime/selector.js";
17
17
  import { ResourceResult, resource } from "./runtime/operations.js";
18
+ import { Mutation, Query, QueryConsistency, createMutation, createQuery, invalidate } from "./data/index.js";
18
19
  import { currentRoute, fallback, group, lazy, registerRoutes, resolveRouteRequest, route } from "./router/route.js";
19
20
  import { allow, deny, forbidden, notFound, redirect, requireAuth, requirePermission, requireRole, unauthorized } from "./router/policy.js";
20
21
  import { Link, LinkProps } from "./components/link.js";
21
- export { type AccessDecision, type AccessDenyDecision, type AccessRedirectDecision, Case, type CaseProps, type Context, DefaultPortal, type Derived, For, type ForProps, Fragment, type GroupHelperOptions, type HistoryScrollBehavior, type HydrateSPAConfig, type IslandConfig, type IslandsConfig, Link, type LinkProps, Match, type MatchProps, type NavigateOptions, type NavigationScrollBehavior, Portal, type PortalProps, type Props, type RegisterRoutesOptions, type ResourceResult, type RouteAuthMode, type RouteAuthOptions, type RouteAuthResolver, type RouteAuthState, type RouteComponent, type RouteContext, type RouteDefinition, type RouteManifest, type RouteMatch, type RouteMode, type RouteOptions, type RoutePolicy, type RouteRecord, type RouteRenderResult, type RouteRequestOptions, type RouteRequestResult, type RouteSnapshot, type SPAConfig, type ScrollRestorationOptions, type Selector, Show, type ShowProps, Slot, type SlotProps, type State, allow, cleanupApp, createIsland, createIslands, createSPA, currentRoute, defineContext, definePortal, deny, derive, fallback, forbidden, getSignal, group, hasApp, hydrateSPA, jsx, jsxs, lazy, navigate, notFound, readContext, redirect, registerRoutes, requireAuth, requirePermission, requireRole, resolveRouteRequest, resource, route, selector, state, unauthorized };
22
+ import { ErrorBoundary, ErrorBoundaryFallbackRender, ErrorBoundaryProps } from "./components/error-boundary.js";
23
+ export { type AccessDecision, type AccessDenyDecision, type AccessRedirectDecision, Case, type CaseProps, type Context, DefaultPortal, type Derived, ErrorBoundary, type ErrorBoundaryFallbackRender, type ErrorBoundaryProps, For, type ForProps, Fragment, type GroupHelperOptions, type HistoryScrollBehavior, type HydrateSPAConfig, type IslandConfig, type IslandsConfig, Link, type LinkProps, Match, type MatchProps, type Mutation, type NavigateOptions, type NavigationScrollBehavior, Portal, type PortalProps, type Props, type Query, type QueryConsistency, type RegisterRoutesOptions, type ResourceResult, type RouteAuthMode, type RouteAuthOptions, type RouteAuthResolver, type RouteAuthState, type RouteComponent, type RouteContext, type RouteDefinition, type RouteManifest, type RouteMatch, type RouteMode, type RouteOptions, type RoutePolicy, type RouteRecord, type RouteRenderResult, type RouteRequestOptions, type RouteRequestResult, type RouteSnapshot, type SPAConfig, type ScrollRestorationOptions, type Selector, Show, type ShowProps, Slot, type SlotProps, type State, allow, cleanupApp, createIsland, createIslands, createMutation, createQuery, createSPA, currentRoute, defineContext, definePortal, deny, derive, fallback, forbidden, getSignal, group, hasApp, hydrateSPA, invalidate, jsx, jsxs, lazy, navigate, notFound, readContext, redirect, registerRoutes, requireAuth, requirePermission, requireRole, resolveRouteRequest, resource, route, selector, state, unauthorized };