@govela/app 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/client.d.ts +33 -8
  2. package/client.js +11 -4
  3. package/package.json +8 -8
package/client.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- // Types of @govela/app/client for editors (docs/APP.md).
1
+ // Types of @govela/app/client (docs/APP.md).
2
+
3
+ import type { Component, ComponentInstance } from '@govela/runtime';
4
+ import type { MismatchReport } from '@govela/runtime/hydrate';
2
5
 
3
6
  /** The route table of src/_app/routes.js, which vela-app generates. */
4
7
  export interface RouteTable {
@@ -7,12 +10,15 @@ export interface RouteTable {
7
10
  match(path: string): { route: { id: string }; params: Record<string, string> } | null;
8
11
  }
9
12
 
10
- /** The route prop of route components (R-1). */
11
- export interface Route {
13
+ /**
14
+ * The route prop of route components (R-1). P are the parameters of the
15
+ * path: `Route<Params>` of the route's `$types` (docs/TYPESCRIPT.md TS-8.2).
16
+ */
17
+ export interface Route<P extends Record<string, string | undefined> = Record<string, string>> {
12
18
  id: string;
13
19
  url: string;
14
20
  path: string;
15
- params: Record<string, string>;
21
+ params: P;
16
22
  query: Record<string, string>;
17
23
  }
18
24
 
@@ -37,19 +43,19 @@ export interface BeforeNavigation extends Navigation {
37
43
 
38
44
  export interface StartOptions {
39
45
  /** The generated app root, src/_app/VelaApp.vela. */
40
- VelaApp: Function;
46
+ VelaApp: Component<any, any, any>;
41
47
  /** The generated route table, src/_app/routes.js. Without it links load documents. */
42
48
  routes?: RouteTable;
43
49
  /** The element that holds the app root, or its id. */
44
50
  root?: string | Element;
45
51
  /** Receives hydration mismatch reports. */
46
- onMismatch?: (report: unknown) => void;
52
+ onMismatch?: (report: MismatchReport) => void;
47
53
  /** The default prefetch of links; data-vela-preload on a link or an ancestor overrides it. */
48
54
  preload?: 'hover' | 'viewport' | 'off';
49
55
  }
50
56
 
51
57
  /** Hydrates the server document and installs the router. */
52
- export function start(options: StartOptions): Promise<{ app: unknown; routes?: RouteTable }>;
58
+ export function start(options: StartOptions): Promise<{ app: ComponentInstance; routes?: RouteTable }>;
53
59
 
54
60
  export interface GotoOptions {
55
61
  /** Replace the current history entry instead of adding one. */
@@ -81,4 +87,23 @@ export function afterNavigate(fn: (navigation: Navigation) => void): () => void;
81
87
  export const protocolVersion: 1;
82
88
 
83
89
  /** The request headers of a navigation (N-2). */
84
- export const headers: Readonly<{ navigate: string; have: string; build: string }>;
90
+ export const headers: Readonly<{ navigate: string; have: string; build: string; purpose: string }>;
91
+
92
+ /** The detail of `vela:form`: a failed form action (`app.Fail`). */
93
+ export interface FormFailure {
94
+ /** The HTTP status of the failure, 400–499. */
95
+ status: number;
96
+ /** The value the action failed with: the page's `form` prop. */
97
+ form: unknown;
98
+ }
99
+
100
+ declare global {
101
+ interface HTMLElementEventMap {
102
+ /** The app root hydrated (start()). */
103
+ 'vela:hydrated': CustomEvent<{ app: ComponentInstance }>;
104
+ /** A navigation of the router ended. */
105
+ 'vela:navigated': CustomEvent<Navigation>;
106
+ /** A form action failed; the page's `form` prop changed. */
107
+ 'vela:form': CustomEvent<FormFailure>;
108
+ }
109
+ }
package/client.js CHANGED
@@ -12,6 +12,8 @@ export const headers = Object.freeze({
12
12
  navigate: 'X-Vela-Navigate',
13
13
  have: 'X-Vela-Have',
14
14
  build: 'X-Vela-Build',
15
+ // "prefetch" on a prefetch (G-4): the answer sets no cookies.
16
+ purpose: 'X-Vela-Purpose',
15
17
  });
16
18
 
17
19
  // A chain of redirects longer than this loads the document (N-5).
@@ -210,11 +212,13 @@ function createRouter({ VelaApp, app, routes, target, route, chain, preload }) {
210
212
  // fetchNavigation asks the server for the navigation to url (N-2). It
211
213
  // resolves with the JSON body, or { redirected } when something other
212
214
  // than the app redirected the request.
213
- async function fetchNavigation(url, have, signal) {
215
+ async function fetchNavigation(url, have, signal, purpose) {
216
+ const requestHeaders = { [headers.navigate]: `auto;v=${protocolVersion}`, [headers.have]: have, [headers.build]: build };
217
+ if (purpose) requestHeaders[headers.purpose] = purpose;
214
218
  const response = await fetch(url.href, {
215
219
  credentials: 'same-origin',
216
220
  signal,
217
- headers: { [headers.navigate]: `auto;v=${protocolVersion}`, [headers.have]: have, [headers.build]: build },
221
+ headers: requestHeaders,
218
222
  });
219
223
  if (response.redirected) return { redirected: response.url };
220
224
  return response.json();
@@ -303,7 +307,10 @@ function createRouter({ VelaApp, app, routes, target, route, chain, preload }) {
303
307
  // or when it enters the viewport, as data-vela-preload on it or an
304
308
  // ancestor says ("hover", "viewport" or "off"; start({ preload }) is the
305
309
  // default). The answer serves one navigation within 30 s, for the same
306
- // URL and X-Vela-Have.
310
+ // URL and X-Vela-Have; an answer marked cache: "once" (a user-dependent
311
+ // one, G-4) never serves more. The request says X-Vela-Purpose: prefetch,
312
+ // so the server sets no cookies on it. Every action and invalidate()
313
+ // clear the cache: an answer prefetched before a logout is not shown.
307
314
  const prefetched = new Map();
308
315
  const prefetchTTL = 30000;
309
316
 
@@ -334,7 +341,7 @@ function createRouter({ VelaApp, app, routes, target, route, chain, preload }) {
334
341
  if (match.route.render === 'data') {
335
342
  for (const level of match.route.chain) components(routes, level.id)?.load().catch(() => {});
336
343
  }
337
- const body = fetchNavigation(url, have, undefined);
344
+ const body = fetchNavigation(url, have, undefined, 'prefetch');
338
345
  prefetched.set(key, { at: now, body });
339
346
  // What the answer needs loads too; a failure only drops the entry.
340
347
  body.then((answer) => (answer && answer.v === protocolVersion && (answer.type === 'data' || answer.type === 'fragment') ? prepareModules(answer) : null))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@govela/app",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Vela App (docs/APP.md): the client router (hydration, data navigation, route matching) and the vela-app command.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,20 +32,20 @@
32
32
  "LICENSE"
33
33
  ],
34
34
  "govela": {
35
- "runtimeContract": 4,
35
+ "runtimeContract": 5,
36
36
  "protocol": 1
37
37
  },
38
38
  "peerDependencies": {
39
- "@govela/runtime": ">=0.4.8"
39
+ "@govela/runtime": ">=0.7.0"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=18"
43
43
  },
44
44
  "optionalDependencies": {
45
- "@govela/app-darwin-arm64": "0.1.2",
46
- "@govela/app-darwin-x64": "0.1.2",
47
- "@govela/app-linux-arm64": "0.1.2",
48
- "@govela/app-linux-x64": "0.1.2",
49
- "@govela/app-win32-x64": "0.1.2"
45
+ "@govela/app-darwin-arm64": "0.3.0",
46
+ "@govela/app-darwin-x64": "0.3.0",
47
+ "@govela/app-linux-arm64": "0.3.0",
48
+ "@govela/app-linux-x64": "0.3.0",
49
+ "@govela/app-win32-x64": "0.3.0"
50
50
  }
51
51
  }