ignotum 0.0.13 → 0.0.15

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.
Files changed (131) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/bin.mjs +6111 -4448
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
  5. package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
  6. package/dist/cli/ffi-7IElU4Co.mjs +148 -0
  7. package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
  8. package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
  9. package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
  10. package/dist/cli/managed-release.d.mts +1 -0
  11. package/dist/cli/managed-release.mjs +14 -0
  12. package/dist/cli/managed-release.mjs.map +1 -0
  13. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
  14. package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
  15. package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
  16. package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
  17. package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
  18. package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
  19. package/dist/runtime/client.d.ts +50 -15
  20. package/dist/runtime/client.js +7 -1138
  21. package/dist/runtime/client.js.map +1 -1
  22. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
  23. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
  24. package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
  25. package/dist/runtime/id--1m0NuUL.js.map +1 -0
  26. package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
  27. package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
  28. package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
  29. package/dist/runtime/image-Djtjy4Z8.js +19 -0
  30. package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
  31. package/dist/runtime/index-D4seNHlX.d.ts +184 -0
  32. package/dist/runtime/internal/api.d.ts +1 -1
  33. package/dist/runtime/internal/api.js +30 -1
  34. package/dist/runtime/internal/api.js.map +1 -0
  35. package/dist/runtime/internal/bootstrap.d.ts +2 -0
  36. package/dist/runtime/internal/bootstrap.js +2 -0
  37. package/dist/runtime/internal/client.d.ts +2 -0
  38. package/dist/runtime/internal/client.js +75 -0
  39. package/dist/runtime/internal/client.js.map +1 -0
  40. package/dist/runtime/internal/host.d.ts +25 -8
  41. package/dist/runtime/internal/host.js +27 -6
  42. package/dist/runtime/internal/host.js.map +1 -1
  43. package/dist/runtime/internal/routes.d.ts +2 -0
  44. package/dist/runtime/internal/routes.js +2 -0
  45. package/dist/runtime/internal/server.d.ts +1 -1
  46. package/dist/runtime/internal/server.js +1 -1
  47. package/dist/runtime/internal/types.d.ts +2 -1
  48. package/dist/runtime/internal/types.js +1 -1
  49. package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
  50. package/dist/runtime/pagination-D3qd6s9N.js +33 -0
  51. package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
  52. package/dist/runtime/result-BkziOG1L.d.ts +1 -0
  53. package/dist/runtime/router-BSBI1oN1.js +2377 -0
  54. package/dist/runtime/router-BSBI1oN1.js.map +1 -0
  55. package/dist/runtime/routes-Ce8TVP-T.js +690 -0
  56. package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
  57. package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
  58. package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
  59. package/dist/runtime/server.d.ts +5 -5
  60. package/dist/runtime/server.js +2 -2
  61. package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
  62. package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
  63. package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
  64. package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
  65. package/package.json +29 -4
  66. package/src/cli/agent-files.ts +14 -8
  67. package/src/cli/build/managed-client.ts +411 -0
  68. package/src/cli/build/managed-server.ts +294 -0
  69. package/src/cli/build/managed.ts +67 -0
  70. package/src/cli/build/output.ts +47 -0
  71. package/src/cli/build/server.ts +51 -220
  72. package/src/cli/client-config.ts +1 -140
  73. package/src/cli/client-styles.ts +1 -0
  74. package/src/cli/codegen.ts +9 -4
  75. package/src/cli/command.ts +28 -5
  76. package/src/cli/control-client.ts +84 -101
  77. package/src/cli/deploy.ts +133 -177
  78. package/src/cli/dev.ts +506 -107
  79. package/src/cli/image-assets.ts +499 -0
  80. package/src/cli/managed-client-boundaries.ts +103 -0
  81. package/src/cli/managed-dev-platform.ts +118 -0
  82. package/src/cli/managed-exports.ts +219 -0
  83. package/src/cli/managed-release-bin.ts +13 -0
  84. package/src/cli/managed-release.ts +260 -0
  85. package/src/cli/module-boundaries.ts +11 -1
  86. package/src/cli/new-app.ts +111 -87
  87. package/src/cli/route-codegen.ts +311 -0
  88. package/src/cli/route-static.ts +710 -0
  89. package/src/client/bootstrap.ts +110 -0
  90. package/src/client/errors.ts +13 -7
  91. package/src/client/files.ts +64 -0
  92. package/src/client/id.ts +9 -5
  93. package/src/client/image.ts +28 -0
  94. package/src/client/index.ts +25 -2
  95. package/src/client/managed-client.ts +729 -0
  96. package/src/client/managed-upgrade.ts +283 -0
  97. package/src/client/recovery-journal.ts +195 -0
  98. package/src/client/route-boundaries.ts +184 -0
  99. package/src/client/router-history.ts +152 -0
  100. package/src/client/router-store.ts +644 -0
  101. package/src/client/router.ts +324 -0
  102. package/src/client/routes.ts +28 -0
  103. package/src/client/sync.ts +66 -592
  104. package/src/dev-runtime/database.ts +77 -2
  105. package/src/dev-runtime/functions.ts +3 -3
  106. package/src/dev-runtime/id.ts +19 -4
  107. package/src/dev-runtime/managed-functions.ts +255 -0
  108. package/src/dev-runtime/managed-sync.ts +263 -0
  109. package/src/dev-runtime/managed-websocket.ts +71 -0
  110. package/src/dev-runtime/migrations.ts +20 -0
  111. package/src/dev-runtime/sync.ts +236 -236
  112. package/src/image-assets.d.ts +36 -0
  113. package/src/internal/client.ts +186 -0
  114. package/src/internal/host.ts +1 -1
  115. package/src/internal/http-paths.ts +0 -3
  116. package/src/internal/routes.ts +45 -0
  117. package/dist/runtime/api-CAgKDij7.js.map +0 -1
  118. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
  119. package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
  120. package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
  121. package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
  122. package/dist/runtime/index-D54flWtH.d.ts +0 -402
  123. package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
  124. package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
  125. package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
  126. package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
  127. package/src/cli/build/client.ts +0 -119
  128. package/src/cli/build/public.ts +0 -186
  129. package/src/cli/client-entry.ts +0 -152
  130. package/src/cli/client-plugin.ts +0 -136
  131. package/src/client/app.ts +0 -15
@@ -0,0 +1,324 @@
1
+ import {
2
+ buildUrl,
3
+ compilePatterns,
4
+ decodeRouteInputs,
5
+ matchPattern,
6
+ parseLocation,
7
+ } from "@ignotum/shared/routing";
8
+ import { Effect, Predicate, Schema } from "effect";
9
+ import { createContext, createElement, type JSX } from "preact";
10
+ import { useContext, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
11
+ import {
12
+ getRouteDefinition,
13
+ type RouteReference,
14
+ type ReferenceTypes,
15
+ } from "../internal/routes.js";
16
+ import type { RouteInputs, RouteMatch, RouteLocation, NavigationResult } from "./routes.js";
17
+ import type {
18
+ RouterStore,
19
+ RouterSnapshot,
20
+ NavigationOptions,
21
+ NavigationState,
22
+ BlockerState,
23
+ } from "./router-store.js";
24
+
25
+ export const RouterContext = createContext<RouterStore | undefined>(undefined);
26
+ const useRouter = (): RouterStore => {
27
+ const router = useContext(RouterContext);
28
+ if (router === undefined)
29
+ throw new Error("Ignotum routing hooks require a managed client mount.");
30
+ return router;
31
+ };
32
+ export const useRouterSnapshot = (router: RouterStore): RouterSnapshot => {
33
+ const [snapshot, setSnapshot] = useState(router.getSnapshot);
34
+ useLayoutEffect(() => {
35
+ const update = () => setSnapshot(router.getSnapshot());
36
+ const unsubscribe = router.subscribe(update);
37
+ update();
38
+ return unsubscribe;
39
+ }, [router]);
40
+ return snapshot;
41
+ };
42
+
43
+ type OptionsArgs<Options> = {} extends Options ? [options?: Options] : [options: Options];
44
+ export type NavigateOptions<Reference extends RouteReference> = RouteInputs<Reference> &
45
+ NavigationOptions;
46
+ export interface Navigate {
47
+ <Reference extends RouteReference>(
48
+ reference: Reference,
49
+ ...args: OptionsArgs<NavigateOptions<NoInfer<Reference>>>
50
+ ): Promise<NavigationResult>;
51
+ back(): void;
52
+ forward(): void;
53
+ }
54
+
55
+ export function url<Reference extends RouteReference>(
56
+ reference: Reference,
57
+ ...args: OptionsArgs<RouteInputs<NoInfer<Reference>>>
58
+ ): string {
59
+ return Effect.runSync(buildUrl(getRouteDefinition(reference), args[0]));
60
+ }
61
+
62
+ // Link and navigate own additional options; only URL fields belong to the shared URL codec.
63
+ const destinationHref = (
64
+ reference: RouteReference,
65
+ input: {
66
+ readonly params?: unknown;
67
+ readonly searchParams?: unknown;
68
+ readonly hash?: string;
69
+ } = {},
70
+ ): string =>
71
+ Effect.runSync(
72
+ buildUrl(getRouteDefinition(reference), {
73
+ params: input.params,
74
+ searchParams: input.searchParams,
75
+ hash: input.hash,
76
+ }),
77
+ );
78
+
79
+ const NavigationInput = Schema.Struct({
80
+ params: Schema.optional(Schema.Unknown),
81
+ searchParams: Schema.optional(Schema.Unknown),
82
+ hash: Schema.optional(Schema.String),
83
+ replace: Schema.optional(Schema.Boolean),
84
+ scroll: Schema.optional(Schema.Literals(["auto", "preserve"])),
85
+ }).annotate({ parseOptions: { onExcessProperty: "error" } });
86
+
87
+ export const useNavigate = (): Navigate => {
88
+ const router = useRouter();
89
+ const [navigate] = useState<Navigate>(() =>
90
+ Object.assign(
91
+ <Reference extends RouteReference>(
92
+ reference: Reference,
93
+ ...args: OptionsArgs<NavigateOptions<NoInfer<Reference>>>
94
+ ) => {
95
+ const options = Schema.decodeUnknownSync(NavigationInput)(args[0] ?? {});
96
+ return router.navigate(destinationHref(reference, options), options);
97
+ },
98
+ { back: router.back, forward: router.forward },
99
+ ),
100
+ );
101
+ return navigate;
102
+ };
103
+ export const useLocation = (): RouteLocation => {
104
+ const snapshot = useRouterSnapshot(useRouter());
105
+ if (snapshot.location === undefined)
106
+ throw new Error("The initial Ignotum location has not committed.");
107
+ return snapshot.location;
108
+ };
109
+ export const useNavigation = (): NavigationState => useRouterSnapshot(useRouter()).navigation;
110
+
111
+ export const useBlocker = ({ when }: { readonly when: boolean }): BlockerState => {
112
+ const router = useRouter();
113
+ const [id] = useState(() => Symbol("blocker"));
114
+ useRouterSnapshot(router);
115
+ useLayoutEffect(() => {
116
+ router.setBlocker(id, when);
117
+ }, [router, id, when]);
118
+ useLayoutEffect(() => () => router.setBlocker(id, false), [router, id]);
119
+ return router.getBlocker(id);
120
+ };
121
+
122
+ const matchReference = <Reference extends RouteReference>(
123
+ reference: Reference,
124
+ snapshot: RouterSnapshot,
125
+ end: boolean,
126
+ ): RouteMatch<Reference> | undefined => {
127
+ const location = snapshot.location;
128
+ const outcome = snapshot.branch?.outcome;
129
+ if (location === undefined || outcome === undefined) return;
130
+ const definition = getRouteDefinition(reference);
131
+ if (end && (outcome._tag !== "Matched" || outcome.route.id !== definition.id)) return;
132
+ const parsed = Effect.runSync(
133
+ parseLocation(`${location.pathname}${location.search}`).pipe(Effect.result),
134
+ );
135
+ if (parsed._tag === "Failure") return;
136
+ const compiled = Effect.runSync(compilePatterns([{ ...definition, layouts: [] }], []));
137
+ for (let length = parsed.success.segments.length; length >= 0; length--) {
138
+ if (length === 0 && parsed.success.segments.length !== 0) break;
139
+ const match = matchPattern(compiled, parsed.success.segments.slice(0, length));
140
+ if (match.route !== undefined) {
141
+ const decoded = Effect.runSync(
142
+ decodeRouteInputs(definition, match.params, location.search).pipe(Effect.result),
143
+ );
144
+ if (decoded._tag === "Failure") return;
145
+ // SAFETY: The reference's own validated descriptors determine these decoded generic input types.
146
+ return { ...decoded.success, hash: location.hash } as RouteMatch<Reference>;
147
+ }
148
+ if (end) break;
149
+ }
150
+ return;
151
+ };
152
+ export function useMatch<Reference extends RouteReference>(
153
+ reference: Reference,
154
+ options?: { readonly end?: boolean },
155
+ ): RouteMatch<Reference> | undefined {
156
+ return matchReference(reference, useRouterSnapshot(useRouter()), options?.end ?? true);
157
+ }
158
+ export type RouteState<Reference extends RouteReference> = RouteMatch<Reference> & {
159
+ readonly setSearchParams: (
160
+ next:
161
+ | ReferenceTypes<Reference>["searchInputs"]
162
+ | ((
163
+ current: ReferenceTypes<Reference>["searchParams"],
164
+ ) => ReferenceTypes<Reference>["searchInputs"]),
165
+ options?: NavigationOptions,
166
+ ) => Promise<NavigationResult>;
167
+ };
168
+ export function useRoute<Reference extends RouteReference>(
169
+ reference: Reference,
170
+ ): RouteState<Reference> {
171
+ const router = useRouter();
172
+ const snapshot = useRouterSnapshot(router);
173
+ const match = matchReference(reference, snapshot, true);
174
+ if (match === undefined)
175
+ throw new Error(`useRoute requires page '${getRouteDefinition(reference).id}' to be active.`);
176
+ return {
177
+ ...match,
178
+ setSearchParams: (next, options) => {
179
+ const latest = matchReference(reference, router.getSnapshot(), true);
180
+ const location = router.getSnapshot().location;
181
+ if (latest === undefined || location === undefined)
182
+ throw new Error("Cannot update search parameters for an inactive route.");
183
+ // A function is the only callable alternative in the public search update union.
184
+ const inputs = Predicate.isFunction(next) ? next(latest.searchParams) : next;
185
+ const href = destinationHref(reference, {
186
+ params: latest.params,
187
+ searchParams: inputs,
188
+ hash: latest.hash,
189
+ });
190
+ const destination = new URL(href, router.browser.location.origin);
191
+ destination.pathname = location.pathname;
192
+ const declared = new Set(
193
+ getRouteDefinition(reference).searchParams.map((field) => field.name),
194
+ );
195
+ for (const [name, value] of new URLSearchParams(location.search)) {
196
+ if (!declared.has(name)) destination.searchParams.append(name, value);
197
+ }
198
+ return router.navigate(`${destination.pathname}${destination.search}${destination.hash}`, {
199
+ scroll: "preserve",
200
+ ...options,
201
+ });
202
+ },
203
+ };
204
+ }
205
+
206
+ type AnchorProps = Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "class" | "ref">;
207
+ export type LinkProps<Reference extends RouteReference> = AnchorProps &
208
+ NavigateOptions<Reference> & {
209
+ readonly to: Reference;
210
+ readonly class?: string;
211
+ readonly preload?: "intent" | "none";
212
+ };
213
+ export type NavLinkProps<Reference extends RouteReference> = Omit<LinkProps<Reference>, "class"> & {
214
+ readonly end?: boolean;
215
+ readonly class?: string | ((state: { readonly isActive: boolean }) => string);
216
+ };
217
+
218
+ const activePath = (current: string, destination: string, end: boolean): boolean => {
219
+ const source = Effect.runSync(parseLocation(current).pipe(Effect.result));
220
+ const target = Effect.runSync(parseLocation(destination).pipe(Effect.result));
221
+ if (source._tag === "Failure" || target._tag === "Failure") return false;
222
+ const from = source.success.segments;
223
+ const to = target.success.segments;
224
+ if (from.length !== to.length && (end || to.length === 0 || from.length < to.length))
225
+ return false;
226
+ return to.every((segment, index) => from[index] === segment);
227
+ };
228
+
229
+ interface AnchorRuntimeProps extends AnchorProps, NavigationOptions {
230
+ readonly to: RouteReference;
231
+ readonly params?: unknown;
232
+ readonly searchParams?: unknown;
233
+ readonly hash?: string;
234
+ readonly preload?: "intent" | "none";
235
+ readonly end?: boolean;
236
+ readonly active?: boolean;
237
+ readonly class?: string | ((state: { readonly isActive: boolean }) => string);
238
+ }
239
+ function Anchor(props: AnchorRuntimeProps): JSX.Element {
240
+ const router = useRouter();
241
+ const snapshot = useRouterSnapshot(router);
242
+ const {
243
+ to,
244
+ params,
245
+ searchParams,
246
+ hash,
247
+ replace,
248
+ scroll,
249
+ preload = "intent",
250
+ end = true,
251
+ active = false,
252
+ class: className,
253
+ onClick,
254
+ onMouseEnter,
255
+ onFocus,
256
+ ...attributes
257
+ } = props;
258
+ const href = destinationHref(to, { params, searchParams, hash });
259
+ const isActive =
260
+ active &&
261
+ snapshot.location !== undefined &&
262
+ activePath(snapshot.location.pathname, href.split(/[?#]/)[0]!, end);
263
+ const prepare = () => {
264
+ if (preload !== "none") void router.preload(href).catch(() => {});
265
+ };
266
+ return createElement("a", {
267
+ ...attributes,
268
+ href,
269
+ class: Predicate.isFunction(className) ? className({ isActive }) : className,
270
+ "aria-current": isActive ? "page" : attributes["aria-current"],
271
+ onClick: (event: JSX.TargetedMouseEvent<HTMLAnchorElement>): void => {
272
+ onClick?.(event);
273
+ const anchor = event.currentTarget;
274
+ if (
275
+ event.defaultPrevented ||
276
+ event.button !== 0 ||
277
+ event.metaKey ||
278
+ event.ctrlKey ||
279
+ event.shiftKey ||
280
+ event.altKey ||
281
+ (anchor.target !== "" && anchor.target.toLowerCase() !== "_self") ||
282
+ anchor.hasAttribute("download") ||
283
+ anchor.relList.contains("external") ||
284
+ anchor.origin !== router.browser.location.origin ||
285
+ `${anchor.pathname}${anchor.search}${anchor.hash}` !== href
286
+ )
287
+ return;
288
+ event.preventDefault();
289
+ void router.navigate(href, { replace, scroll });
290
+ },
291
+ onMouseEnter: (event: JSX.TargetedMouseEvent<HTMLAnchorElement>): void => {
292
+ onMouseEnter?.(event);
293
+ if (!event.defaultPrevented) prepare();
294
+ },
295
+ onFocus: (event: JSX.TargetedFocusEvent<HTMLAnchorElement>): void => {
296
+ onFocus?.(event);
297
+ if (!event.defaultPrevented) prepare();
298
+ },
299
+ });
300
+ }
301
+ export function Link<Reference extends RouteReference>(props: LinkProps<Reference>): JSX.Element {
302
+ return createElement(Anchor, props);
303
+ }
304
+ export function NavLink<Reference extends RouteReference>(
305
+ props: NavLinkProps<Reference>,
306
+ ): JSX.Element {
307
+ return createElement(Anchor, { ...props, active: true });
308
+ }
309
+ export type RedirectProps<Reference extends RouteReference> = NavigateOptions<Reference> & {
310
+ readonly to: Reference;
311
+ };
312
+ export function Redirect<Reference extends RouteReference>(props: RedirectProps<Reference>): null {
313
+ const router = useRouter();
314
+ const href = destinationHref(props.to, props);
315
+ const previous = useRef<string>();
316
+ const replace = props.replace ?? true;
317
+ useEffect(() => {
318
+ const identity = JSON.stringify([href, replace, props.scroll]);
319
+ if (previous.current === identity) return;
320
+ previous.current = identity;
321
+ void router.navigate(href, { replace, scroll: props.scroll }, true);
322
+ }, [router, href, replace, props.scroll]);
323
+ return null;
324
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReferenceTypes } from "../internal/routes.js";
2
+
3
+ type Empty = Readonly<Record<string, never>>;
4
+ type Group<Name extends string, Fields> = keyof Fields extends never
5
+ ? { readonly [Key in Name]?: Empty }
6
+ : {} extends Fields
7
+ ? { readonly [Key in Name]?: Fields }
8
+ : { readonly [Key in Name]: Fields };
9
+
10
+ /** Decoded values delivered to a page component. */
11
+ export type RouteProps<Reference> = {
12
+ readonly params: ReferenceTypes<Reference>["params"];
13
+ readonly searchParams: ReferenceTypes<Reference>["searchParams"];
14
+ };
15
+ /** URL inputs. Search defaults can be omitted when navigating. */
16
+ export type RouteInputs<Reference> = Group<"params", ReferenceTypes<Reference>["params"]> &
17
+ Group<"searchParams", ReferenceTypes<Reference>["searchInputs"]> & { readonly hash?: string };
18
+ export type RouteMatch<Reference> = RouteProps<Reference> & { readonly hash: string };
19
+ export interface RouteLocation {
20
+ readonly pathname: string;
21
+ readonly search: string;
22
+ readonly hash: string;
23
+ readonly key: string;
24
+ }
25
+ export type NavigationResult =
26
+ | { readonly status: "committed" }
27
+ | { readonly status: "cancelled" }
28
+ | { readonly status: "failed"; readonly error: unknown };