ignotum 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/bin.mjs +13263 -2689
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/platform/_headers +3 -0
  5. package/dist/platform/_ignotum/v1.0/client.js +12 -0
  6. package/dist/runtime/{api-B4fwp5a2.d.ts → api-BnUKx9cj-D49Tf0rX.d.ts} +4 -3
  7. package/dist/runtime/api-C-tltz6s.js +31 -0
  8. package/dist/runtime/api-C-tltz6s.js.map +1 -0
  9. package/dist/runtime/artifact-BB5qzoa6.js +42 -0
  10. package/dist/runtime/artifact-BB5qzoa6.js.map +1 -0
  11. package/dist/runtime/{bootstrap-BTFScKAc.d.ts → bootstrap-DjTwGJao-DBx5zdFI.d.ts} +64 -64
  12. package/dist/runtime/{bootstrap-BISQSn_O.js → bootstrap-DlNIfH-r.js} +9 -9
  13. package/dist/runtime/bootstrap-DlNIfH-r.js.map +1 -0
  14. package/dist/runtime/client.d.ts +24 -20
  15. package/dist/runtime/client.js +9 -9
  16. package/dist/runtime/client.js.map +1 -1
  17. package/dist/runtime/id-BI08JkmR.d.ts +1 -0
  18. package/dist/runtime/{id--1m0NuUL.js → id-BrEnxY-r.js} +118 -53
  19. package/dist/runtime/id-BrEnxY-r.js.map +1 -0
  20. package/dist/runtime/{image-Djtjy4Z8.js → image-gofzZeCm.js} +2 -2
  21. package/dist/runtime/image-gofzZeCm.js.map +1 -0
  22. package/dist/runtime/{index-D4seNHlX.d.ts → index-Biaw5vdF.d.ts} +2 -2
  23. package/dist/runtime/internal/api.d.ts +1 -1
  24. package/dist/runtime/internal/api.js +1 -30
  25. package/dist/runtime/internal/bootstrap.d.ts +1 -1
  26. package/dist/runtime/internal/bootstrap.js +1 -1
  27. package/dist/runtime/internal/client.d.ts +2 -2
  28. package/dist/runtime/internal/client.js +4 -4
  29. package/dist/runtime/internal/client.js.map +1 -1
  30. package/dist/runtime/internal/host.d.ts +4 -4
  31. package/dist/runtime/internal/host.js +4 -4
  32. package/dist/runtime/internal/routes.d.ts +1 -1
  33. package/dist/runtime/internal/routes.js +1 -1
  34. package/dist/runtime/internal/server.d.ts +3 -2
  35. package/dist/runtime/internal/server.js +2 -2
  36. package/dist/runtime/internal/server.js.map +1 -1
  37. package/dist/runtime/internal/types.d.ts +2 -2
  38. package/dist/runtime/internal/types.js +1 -1
  39. package/dist/runtime/{pagination-D3qd6s9N.js → pagination--f4kMKf6.js} +2 -2
  40. package/dist/runtime/{pagination-D3qd6s9N.js.map → pagination--f4kMKf6.js.map} +1 -1
  41. package/dist/runtime/pagination-BI08JkmR.d.ts +1 -0
  42. package/dist/runtime/{router-BSBI1oN1.js → router-C1eC9yy5.js} +462 -419
  43. package/dist/runtime/router-C1eC9yy5.js.map +1 -0
  44. package/dist/runtime/routes-DnU39R7c.js +152 -0
  45. package/dist/runtime/routes-DnU39R7c.js.map +1 -0
  46. package/dist/runtime/{schema-D1wOqiNH.js → schema-BSjepoNF.js} +271 -6
  47. package/dist/runtime/schema-BSjepoNF.js.map +1 -0
  48. package/dist/runtime/server.d.ts +5 -4
  49. package/dist/runtime/server.js +3 -3
  50. package/dist/runtime/server.js.map +1 -1
  51. package/dist/runtime/{sync-DzUwA8W9.js → sync-BwgS19Xy.js} +4 -16
  52. package/dist/runtime/sync-BwgS19Xy.js.map +1 -0
  53. package/dist/runtime/{sync-D-GK2sv9.d.ts → sync-By2hX5ev.d.ts} +1 -2
  54. package/dist/runtime/{types-DeCCyqLU-BIm7rU-0.d.ts → types-DeCCyqLU-CHx7KFtx.d.ts} +2 -2
  55. package/package.json +7 -1
  56. package/src/cli/agent-files.ts +2 -0
  57. package/src/cli/build/asset-names.ts +190 -0
  58. package/src/cli/build/managed-client.ts +151 -81
  59. package/src/cli/build/managed-server.ts +4 -4
  60. package/src/cli/build/managed.ts +5 -6
  61. package/src/cli/command.ts +7 -15
  62. package/src/cli/control-client.ts +7 -9
  63. package/src/cli/deploy.ts +45 -15
  64. package/src/cli/dev.ts +50 -99
  65. package/src/cli/image-assets.ts +5 -2
  66. package/src/cli/managed-client-boundaries.ts +6 -6
  67. package/src/cli/managed-dev-platform.ts +52 -66
  68. package/src/cli/managed-exports.ts +5 -66
  69. package/src/cli/new-app.ts +7 -2
  70. package/src/cli/path.ts +11 -0
  71. package/src/cli/route-codegen.ts +2 -1
  72. package/src/cli/route-static.ts +3 -2
  73. package/src/cli/upload-journal.ts +130 -0
  74. package/src/client/bootstrap.ts +1 -110
  75. package/src/client/errors.ts +1 -70
  76. package/src/client/files.ts +1 -233
  77. package/src/client/hooks.ts +1 -367
  78. package/src/client/id.ts +1 -263
  79. package/src/client/image.ts +1 -28
  80. package/src/client/index.ts +1 -126
  81. package/src/client/jsx-dev-runtime.ts +1 -2
  82. package/src/client/jsx-runtime.ts +1 -2
  83. package/src/client/managed-client.ts +1 -729
  84. package/src/client/managed-upgrade.ts +1 -283
  85. package/src/client/page-observers.ts +1 -77
  86. package/src/client/query.ts +1 -17
  87. package/src/client/recovery-journal.ts +1 -195
  88. package/src/client/route-boundaries.ts +1 -184
  89. package/src/client/router-history.ts +1 -152
  90. package/src/client/router-store.ts +1 -644
  91. package/src/client/router.ts +1 -324
  92. package/src/client/routes.ts +1 -28
  93. package/src/client/sync.ts +1 -286
  94. package/src/dev-runtime/id.ts +1 -1
  95. package/src/dev-runtime/managed-functions.ts +10 -18
  96. package/src/dev-runtime/managed-sync.ts +16 -16
  97. package/src/dev-runtime/sync.ts +6 -4
  98. package/src/internal/api.ts +1 -156
  99. package/src/internal/client.ts +1 -186
  100. package/src/internal/host.ts +1 -1
  101. package/src/internal/http-paths.ts +1 -4
  102. package/src/internal/routes.ts +1 -45
  103. package/src/internal/server.ts +1 -3
  104. package/src/internal/types.ts +1 -2
  105. package/src/server/index.ts +1 -27
  106. package/dist/cli/managed-release-WHYLZwaS.mjs +0 -10902
  107. package/dist/cli/managed-release-WHYLZwaS.mjs.map +0 -1
  108. package/dist/cli/managed-release.d.mts +0 -1
  109. package/dist/cli/managed-release.mjs +0 -14
  110. package/dist/cli/managed-release.mjs.map +0 -1
  111. package/dist/runtime/bootstrap-BISQSn_O.js.map +0 -1
  112. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +0 -269
  113. package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +0 -1
  114. package/dist/runtime/id--1m0NuUL.js.map +0 -1
  115. package/dist/runtime/id-CBOt2kDo.d.ts +0 -1
  116. package/dist/runtime/identity-QjtJRxBD.d.ts +0 -2
  117. package/dist/runtime/image-Djtjy4Z8.js.map +0 -1
  118. package/dist/runtime/internal/api.js.map +0 -1
  119. package/dist/runtime/pagination-CBOt2kDo.d.ts +0 -1
  120. package/dist/runtime/router-BSBI1oN1.js.map +0 -1
  121. package/dist/runtime/routes-Ce8TVP-T.js +0 -690
  122. package/dist/runtime/routes-Ce8TVP-T.js.map +0 -1
  123. package/dist/runtime/schema-D1wOqiNH.js.map +0 -1
  124. package/dist/runtime/sync-DzUwA8W9.js.map +0 -1
  125. package/src/cli/managed-release-bin.ts +0 -13
  126. package/src/cli/managed-release.ts +0 -260
@@ -1,324 +1 @@
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
- }
1
+ export * from "@ignotum/platform/versions/1/browser/router";
@@ -1,28 +1 @@
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 };
1
+ export * from "@ignotum/platform/versions/1/browser/routes";
@@ -1,286 +1 @@
1
- import { BrowserSession } from "./id.js";
2
- import { ManagedBrowserClient } from "./managed-client.js";
3
- import type { ExecutionSelection } from "@ignotum/contracts/platform";
4
- import { Context, Effect, Layer, ManagedRuntime, Schema } from "effect";
5
-
6
- import { encodeCanonicalJson } from "@ignotum/contracts/json";
7
- import { type AppStateRevision } from "@ignotum/contracts/runtime/hosted";
8
- import { IdGenerator } from "@ignotum/shared/id";
9
- import type { ManagedClientMessage, ManagedServerMessage } from "@ignotum/contracts/managed-sync";
10
-
11
- import type {
12
- ErrorValue,
13
- QueryResult,
14
- Result as IgnotumResult,
15
- } from "@ignotum/contracts/runtime/result";
16
- import { failureFromWire, pending, Result } from "@ignotum/contracts/runtime/result";
17
- import {
18
- SubscriptionId,
19
- decodeTransportValue,
20
- type FunctionAddress,
21
- type WireResult,
22
- } from "@ignotum/contracts/runtime/sync";
23
- import {
24
- ClientInfrastructureError,
25
- ConnectionUnavailable,
26
- InvalidServerMessage,
27
- clientErrorReporterLayer,
28
- } from "./errors.js";
29
- import { applyFileGrants } from "./files.js";
30
-
31
- type Listener = () => void;
32
-
33
- interface QueryEntry {
34
- readonly args: Schema.Json;
35
- readonly function: FunctionAddress;
36
- readonly listeners: Set<Listener>;
37
- readonly subscriptionId: SubscriptionId;
38
- generation: number;
39
- dependsOnId?: boolean;
40
- revision: AppStateRevision | undefined;
41
- references: number;
42
- result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
43
- }
44
-
45
- export interface QueryObserver {
46
- readonly getSnapshot: () => QueryResult<unknown, ErrorValue> | ClientInfrastructureError;
47
- readonly release: Effect.Effect<void>;
48
- readonly subscribe: (listener: Listener) => () => void;
49
- }
50
-
51
- type QueryLifecycleMessage = Extract<
52
- ManagedClientMessage,
53
- { readonly type: "Subscribe" | "Unsubscribe" }
54
- >;
55
-
56
- interface QueryCache {
57
- readonly invalidate: (ids?: ReadonlyArray<SubscriptionId>) => void;
58
- readonly invalidatePrivate: () => void;
59
- readonly entries: () => Iterable<QueryEntry>;
60
- readonly getById: (id: SubscriptionId) => QueryEntry | undefined;
61
- readonly setResult: (
62
- id: SubscriptionId,
63
- result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError,
64
- revision?: AppStateRevision,
65
- ) => void;
66
- readonly observe: (
67
- functionAddress: FunctionAddress,
68
- args: Schema.Json,
69
- ) => Effect.Effect<QueryObserver>;
70
- }
71
-
72
- const queryKey = (functionAddress: FunctionAddress, args: Schema.Json): string =>
73
- encodeCanonicalJson([functionAddress, args]);
74
-
75
- const subscribeMessage = (entry: QueryEntry): QueryLifecycleMessage => ({
76
- type: "Subscribe",
77
- id: entry.subscriptionId,
78
- function: entry.function,
79
- args: entry.args,
80
- });
81
-
82
- const makeQueryCache = (
83
- allocateId: Effect.Effect<SubscriptionId>,
84
- sendLifecycleMessage: (message: QueryLifecycleMessage) => Effect.Effect<void>,
85
- ) => {
86
- const queries = new Map<string, QueryEntry>();
87
- const queriesById = new Map<SubscriptionId, QueryEntry>();
88
-
89
- const observe = Effect.fn("SyncClient.QueryCache.observe")(function* (
90
- functionAddress: FunctionAddress,
91
- args: Schema.Json,
92
- ) {
93
- const key = queryKey(functionAddress, args);
94
- let entry = queries.get(key);
95
-
96
- if (entry === undefined) {
97
- const created: QueryEntry = {
98
- args,
99
- function: functionAddress,
100
- generation: 0,
101
- listeners: new Set(),
102
- revision: undefined,
103
- references: 0,
104
- result: pending(),
105
- subscriptionId: yield* allocateId,
106
- };
107
- queries.set(key, created);
108
- queriesById.set(created.subscriptionId, created);
109
- yield* sendLifecycleMessage(subscribeMessage(created));
110
- entry = created;
111
- }
112
-
113
- const observed = entry;
114
- observed.references += 1;
115
- observed.generation += 1;
116
- let released = false;
117
- return {
118
- getSnapshot: () => observed.result,
119
- subscribe: (listener: Listener) => {
120
- observed.listeners.add(listener);
121
- return () => observed.listeners.delete(listener);
122
- },
123
- release: Effect.gen(function* () {
124
- if (released) return;
125
- released = true;
126
- observed.references -= 1;
127
- if (observed.references !== 0) return;
128
- const generation = ++observed.generation;
129
-
130
- // Preact and HMR may release and reacquire an observer during one render handoff.
131
- // Keep the server subscription alive long enough for that handoff to reuse it.
132
- yield* Effect.sleep("25 millis");
133
- if (
134
- observed.references !== 0 ||
135
- observed.generation !== generation ||
136
- queries.get(key) !== observed
137
- ) {
138
- return;
139
- }
140
-
141
- queries.delete(key);
142
- queriesById.delete(observed.subscriptionId);
143
- yield* sendLifecycleMessage({
144
- type: "Unsubscribe",
145
- id: observed.subscriptionId,
146
- });
147
- }),
148
- } satisfies QueryObserver;
149
- });
150
-
151
- const clear = (entry: QueryEntry) => {
152
- entry.result = pending();
153
- entry.revision = undefined;
154
- for (const listener of entry.listeners) listener();
155
- };
156
- return {
157
- invalidate: (ids) => {
158
- for (const entry of queries.values())
159
- if (ids === undefined || ids.includes(entry.subscriptionId)) clear(entry);
160
- },
161
- invalidatePrivate: () => {
162
- for (const entry of queries.values()) if (entry.dependsOnId !== false) clear(entry);
163
- },
164
- entries: () => queries.values(),
165
- getById: (id: SubscriptionId) => queriesById.get(id),
166
- observe,
167
- setResult: (id, result, revision?: AppStateRevision) => {
168
- const entry = queriesById.get(id);
169
- if (entry === undefined) return;
170
- if (revision !== undefined) {
171
- if (entry.revision !== undefined && revision < entry.revision) return;
172
- entry.revision = revision;
173
- }
174
- entry.result = result;
175
- for (const listener of entry.listeners) {
176
- listener();
177
- }
178
- },
179
- } satisfies QueryCache;
180
- };
181
-
182
- type SnapshotFiles = NonNullable<
183
- Extract<ManagedServerMessage, { readonly type: "QuerySnapshot" }>["files"]
184
- >;
185
-
186
- const resultFromWire = Effect.fn("SyncClient.resultFromWire")(
187
- (wire: WireResult, files: SnapshotFiles = []) =>
188
- Effect.try({
189
- try: () =>
190
- wire.type === "Success"
191
- ? Result.succeed(
192
- wire.value === undefined
193
- ? undefined
194
- : applyFileGrants(decodeTransportValue(wire.value, wire.dates ?? []), files),
195
- )
196
- : failureFromWire(wire.error, wire.dates),
197
- catch: (cause) =>
198
- InvalidServerMessage.make({
199
- cause,
200
- message: "The server returned an invalid function result.",
201
- }),
202
- }),
203
- );
204
-
205
- export const syncClientInternals = {
206
- makeQueryCache,
207
- queryIdentity: queryKey,
208
- resultFromWire,
209
- };
210
-
211
- export interface SyncClientService {
212
- readonly mutate: (
213
- functionAddress: FunctionAddress,
214
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Generated reference argument types are deliberately erased at the client transport boundary.
215
- args: unknown,
216
- ) => Effect.Effect<IgnotumResult<unknown, ErrorValue>, ClientInfrastructureError>;
217
- readonly observe: (
218
- functionAddress: FunctionAddress,
219
- args: Schema.Json,
220
- ) => Effect.Effect<QueryObserver, ClientInfrastructureError>;
221
- }
222
-
223
- export class SyncClient extends Context.Service<SyncClient, SyncClientService>()(
224
- "ignotum/client/sync/SyncClient",
225
- ) {}
226
-
227
- const makeUnmountedSyncRuntime = () =>
228
- ManagedRuntime.make(
229
- Layer.succeed(SyncClient)({
230
- observe: () =>
231
- Effect.fail(ConnectionUnavailable.make({ message: "The application has not mounted." })),
232
- mutate: () =>
233
- Effect.fail(ConnectionUnavailable.make({ message: "The application has not mounted." })),
234
- }).pipe(
235
- Layer.provideMerge(BrowserSession.layer),
236
- Layer.provideMerge(clientErrorReporterLayer),
237
- Layer.provideMerge(IdGenerator.layer),
238
- ),
239
- );
240
- export let syncRuntime = makeUnmountedSyncRuntime();
241
-
242
- export const mountManagedSync = (
243
- selection: ExecutionSelection,
244
- browser: Window,
245
- callbacks: { readonly stopAdmission: () => void; readonly reload: () => void },
246
- ) => {
247
- const client = new ManagedBrowserClient(selection, browser, syncClientInternals, callbacks);
248
- const previous = syncRuntime;
249
- void previous.dispose();
250
- syncRuntime = ManagedRuntime.make(
251
- Layer.effect(
252
- SyncClient,
253
- Effect.gen(function* () {
254
- client.attachSession(yield* BrowserSession, yield* IdGenerator);
255
- return SyncClient.of({ observe: client.observe, mutate: client.mutate });
256
- }),
257
- ).pipe(
258
- Layer.provideMerge(BrowserSession.layer),
259
- Layer.provideMerge(clientErrorReporterLayer),
260
- Layer.provideMerge(IdGenerator.layer),
261
- ),
262
- );
263
- const runtime = syncRuntime;
264
- void runtime.runPromise(SyncClient).then(
265
- () => client.start(),
266
- () => client.dispose(),
267
- );
268
- return {
269
- client,
270
- dispose: () => {
271
- client.dispose();
272
- void runtime.dispose();
273
- if (syncRuntime === runtime) syncRuntime = makeUnmountedSyncRuntime();
274
- },
275
- };
276
- };
277
-
278
- // SAFETY: Vite adds this optional property to browser development modules.
279
- const hot = (
280
- import.meta as ImportMeta & {
281
- readonly hot?: { readonly dispose: (cleanup: () => void) => void };
282
- }
283
- ).hot;
284
- if (hot !== undefined) {
285
- hot.dispose(() => void syncRuntime.dispose());
286
- }
1
+ export * from "@ignotum/platform/versions/1/browser/sync";
@@ -28,7 +28,7 @@ const ProfilesV1 = Schema.Struct({
28
28
  const Profiles = Versioned.initial(ProfilesV1);
29
29
  const ProfilesJson = Schema.fromJsonString(Profiles);
30
30
 
31
- export const localIdPath = "/_ignotum/v1/id";
31
+ export const localIdPath = "/_ignotum/v1.0/id";
32
32
  export const LocalCompletion = Schema.Struct({
33
33
  request: SessionRequest,
34
34
  username: DevelopmentUsername,