@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 +21 -3
- package/dist/benchmark.js +2 -2
- package/dist/common/vnode.d.ts +1 -0
- package/dist/common/vnode.d.ts.map +1 -1
- package/dist/common/vnode.js +2 -1
- package/dist/common/vnode.js.map +1 -1
- package/dist/components/error-boundary.d.ts +25 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +114 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/data/index.d.ts +43 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +293 -0
- package/dist/data/index.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/renderer/children.js +1 -0
- package/dist/renderer/children.js.map +1 -1
- package/dist/renderer/dom.js +24 -2
- package/dist/renderer/dom.js.map +1 -1
- package/dist/renderer/fastpath.js +2 -1
- package/dist/renderer/fastpath.js.map +1 -1
- package/dist/resources/index.d.ts +2 -1
- package/dist/resources/index.js +2 -1
- package/dist/runtime/component.d.ts +5 -0
- package/dist/runtime/component.d.ts.map +1 -1
- package/dist/runtime/component.js.map +1 -1
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/ssr/index.js +166 -0
- package/dist/ssr/index.js.map +1 -1
- package/package.json +6 -2
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
|
|
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
|
-
|
|
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.
|
|
14
|
-
buildLabel: "0.0.
|
|
13
|
+
packageVersion: "0.0.33",
|
|
14
|
+
buildLabel: "0.0.33-local"
|
|
15
15
|
};
|
|
16
16
|
function getBenchmarkMetadata() {
|
|
17
17
|
return { ...benchmarkMetadata };
|
package/dist/common/vnode.d.ts
CHANGED
|
@@ -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":";;;;
|
|
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"}
|
package/dist/common/vnode.js
CHANGED
|
@@ -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
|
package/dist/common/vnode.js.map
CHANGED
|
@@ -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":";;
|
|
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
|
-
|
|
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 };
|