@owlmeans/client 0.1.18-rc.40 → 0.1.18-rc.41
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client/SKILL.md +91 -3
- package/build/consts.d.ts +8 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +8 -0
- package/build/consts.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/lazy-retry.d.ts +47 -0
- package/build/lazy-retry.d.ts.map +1 -0
- package/build/lazy-retry.js +128 -0
- package/build/lazy-retry.js.map +1 -0
- package/build/lazy.d.ts +32 -0
- package/build/lazy.d.ts.map +1 -0
- package/build/lazy.js +114 -0
- package/build/lazy.js.map +1 -0
- package/build/types.d.ts +69 -2
- package/build/types.d.ts.map +1 -1
- package/package.json +19 -12
- package/src/consts.ts +10 -0
- package/src/index.ts +2 -0
- package/src/lazy-retry.ts +141 -0
- package/src/lazy.tsx +161 -0
- package/src/types.ts +77 -2
- package/tests/context.ts +33 -0
- package/tests/harness/index.html +11 -0
- package/tests/harness/mount.tsx +86 -0
- package/tests/harness/piece.tsx +4 -0
- package/tests/lazy-retry.spec.ts +199 -0
- package/tests/lazy.spec.ts +145 -0
- package/tsconfig.json +1 -1
package/build/types.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { PropsWithChildren, FC, DependencyList } from 'react';
|
|
1
|
+
import type { PropsWithChildren, FC, DependencyList, ComponentType, ReactNode } from 'react';
|
|
2
2
|
import type { AbstractRequest } from '@owlmeans/entrypoint';
|
|
3
3
|
import type { ClientConfig, ClientContext as BasicClientContext } from '@owlmeans/client-context';
|
|
4
4
|
import type { Criteria, ResourceRecord, Sort } from '@owlmeans/resource';
|
|
5
5
|
import type { StateResourceAppend } from '@owlmeans/state';
|
|
6
|
-
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint';
|
|
6
|
+
import type { ClientEntrypoint, RefedEntrypointHandler } from '@owlmeans/client-entrypoint';
|
|
7
7
|
import type { DebugServiceAppend, ModalServiceAppend } from './components/types.js';
|
|
8
8
|
import type { ConfigResourceAppend } from '@owlmeans/config';
|
|
9
9
|
import type { ConfigRecord, EntrypointReference } from '@owlmeans/context';
|
|
@@ -73,4 +73,71 @@ export interface UseStoreListOptions<T extends ResourceRecord = ResourceRecord>
|
|
|
73
73
|
/** Which state resource to read; the context's default one when omitted. */
|
|
74
74
|
resource?: string;
|
|
75
75
|
}
|
|
76
|
+
/** How `retryImport` retries a rejected dynamic import. */
|
|
77
|
+
export interface RetryImportOptions {
|
|
78
|
+
/** Loads after the first failure, at most; `0` loads once. Default 2. */
|
|
79
|
+
attempts?: number;
|
|
80
|
+
/** The pause before each retry, in order; the last one repeats. Default 500 ms, then 1500 ms. */
|
|
81
|
+
delaysMs?: readonly number[];
|
|
82
|
+
/**
|
|
83
|
+
* Which rejections are retried. Default `isChunkLoadError` — a module that could not be fetched,
|
|
84
|
+
* never one that was fetched and then failed.
|
|
85
|
+
*/
|
|
86
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Retry a failed chunk from its own URL with a cache-busting `t` parameter when the browser's
|
|
89
|
+
* error names it (Chromium, Firefox). Default `true`: a browser that remembers a failed module
|
|
90
|
+
* fetch rejects every later `import()` of the same URL at once, so only a new URL is fetched
|
|
91
|
+
* again. `false` repeats `load` as is.
|
|
92
|
+
*/
|
|
93
|
+
bustCache?: boolean;
|
|
94
|
+
/** How a cache-busted URL is imported; a seam for tests. Default: a dynamic `import()`. */
|
|
95
|
+
importUrl?: (url: string) => Promise<unknown>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* What a lazily-loaded piece renders once it failed: the props it was given, the error, and
|
|
99
|
+
* `retry`, which resets the piece's boundary so its chunk loads again.
|
|
100
|
+
*/
|
|
101
|
+
export type LazyErrorRenderer = (props: any, error: unknown, retry: () => void) => ReactNode;
|
|
102
|
+
/** How a lazily-loaded component behaves while its chunk loads and when the load fails. */
|
|
103
|
+
export interface LazyComponentOptions {
|
|
104
|
+
/** Shown while the chunk loads: a node, or a function of the props the component was given. */
|
|
105
|
+
fallback?: ReactNode | ((props: any) => ReactNode);
|
|
106
|
+
/**
|
|
107
|
+
* Rendered in place of the component when its chunk fails to load, or it throws: a node, or a
|
|
108
|
+
* `LazyErrorRenderer`. Omitted, a chunk failure starts the guarded reload
|
|
109
|
+
* (`recoverFromChunkError`) and leaves `fallback` in place, and any other error propagates to
|
|
110
|
+
* the nearest boundary above.
|
|
111
|
+
*/
|
|
112
|
+
error?: ReactNode | LazyErrorRenderer;
|
|
113
|
+
/**
|
|
114
|
+
* How a rejected load is retried before it counts as failed — `retryImport`'s options, its
|
|
115
|
+
* defaults when omitted. `false` loads once.
|
|
116
|
+
*/
|
|
117
|
+
retry?: false | RetryImportOptions;
|
|
118
|
+
/**
|
|
119
|
+
* When a chunk still fails after its retries, start the guarded page reload
|
|
120
|
+
* (`recoverFromChunkError`, once a minute per tab) before showing `error`; the fallback stays up
|
|
121
|
+
* while the reload starts, and `error` shows when the guard refuses. Without `error` a chunk
|
|
122
|
+
* failure always reloads. Default `false` for `lazyComponent` (a piece degrades in place), `true`
|
|
123
|
+
* for `lazyHandler` (a whole screen). A dev-served app needs it: its modules import themselves by
|
|
124
|
+
* their own URL (React Fast Refresh), so a browser that remembers the failed fetch can load that
|
|
125
|
+
* module again only in a new document.
|
|
126
|
+
*/
|
|
127
|
+
reload?: boolean;
|
|
128
|
+
}
|
|
129
|
+
export interface LazyPreload<P = {}> {
|
|
130
|
+
/** Start (or join) the chunk load; resolves to the component once it is ready. */
|
|
131
|
+
preload: () => Promise<ComponentType<P>>;
|
|
132
|
+
}
|
|
133
|
+
/** A lazily-loaded component: renders like the real one, and can be preloaded ahead of use. */
|
|
134
|
+
export type LazyComponent<P = {}> = ComponentType<P> & LazyPreload<P>;
|
|
135
|
+
/** An entrypoint handler over a lazily-loaded component, carrying its `.preload()`. */
|
|
136
|
+
export type LazyHandler<P = {}> = RefedEntrypointHandler<P> & LazyPreload<P>;
|
|
137
|
+
/** Names of a module's exports that are React components. */
|
|
138
|
+
export type ComponentExport<M> = {
|
|
139
|
+
[K in keyof M]-?: M[K] extends ComponentType<any> ? K : never;
|
|
140
|
+
}[keyof M] & string;
|
|
141
|
+
/** The props of a module's component export. */
|
|
142
|
+
export type ExportProps<M, K extends keyof M> = M[K] extends ComponentType<infer P> ? P : never;
|
|
76
143
|
//# sourceMappingURL=types.d.ts.map
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,EAAE,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/client",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.41",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc -b",
|
|
8
8
|
"dev": "sleep 54 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
-
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
10
|
+
"test": "bun test ./tests"
|
|
10
11
|
},
|
|
11
12
|
"main": "build/index.js",
|
|
12
13
|
"module": "build/index.js",
|
|
@@ -28,17 +29,17 @@
|
|
|
28
29
|
}
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
32
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
33
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
34
|
-
"@owlmeans/client-resource": "^0.1.18-rc.
|
|
35
|
-
"@owlmeans/config": "^0.1.18-rc.
|
|
32
|
+
"@owlmeans/auth": "^0.1.18-rc.36",
|
|
33
|
+
"@owlmeans/client-context": "^0.1.18-rc.39",
|
|
34
|
+
"@owlmeans/client-entrypoint": "^0.1.18-rc.38",
|
|
35
|
+
"@owlmeans/client-resource": "^0.1.18-rc.38",
|
|
36
|
+
"@owlmeans/config": "^0.1.18-rc.38",
|
|
36
37
|
"@owlmeans/context": "^0.1.18-rc.32",
|
|
37
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
39
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
38
|
+
"@owlmeans/error": "^0.1.18-rc.36",
|
|
39
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.37",
|
|
40
|
+
"@owlmeans/resource": "^0.1.18-rc.36",
|
|
40
41
|
"@owlmeans/router": "^0.1.18-rc.31",
|
|
41
|
-
"@owlmeans/state": "^0.1.18-rc.
|
|
42
|
+
"@owlmeans/state": "^0.1.18-rc.35"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"@remix-run/router": "*",
|
|
@@ -51,9 +52,15 @@
|
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@owlmeans/dep-config": "workspace:*",
|
|
55
|
+
"@owlmeans/test-ui": "^0.1.18-rc.39",
|
|
54
56
|
"@types/react": "^19.2.17",
|
|
57
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
55
58
|
"nodemon": "^3.1.14",
|
|
56
|
-
"
|
|
59
|
+
"playwright": "^1.61.1",
|
|
60
|
+
"react": "^19.2.7",
|
|
61
|
+
"react-dom": "^19.2.7",
|
|
62
|
+
"typescript": "^7.0.2",
|
|
63
|
+
"vite": "^8.1.2"
|
|
57
64
|
},
|
|
58
65
|
"publishConfig": {
|
|
59
66
|
"access": "public"
|
package/src/consts.ts
CHANGED
|
@@ -5,3 +5,13 @@ export const DEF_DEBUG_ALIAS = 'debug'
|
|
|
5
5
|
export const DEBUGGER_FLAG = 'debugger'
|
|
6
6
|
export const DEBUG_CONFIG_KEY = 'debugger'
|
|
7
7
|
|
|
8
|
+
/** How many times `retryImport` loads a chunk again after its first failure. */
|
|
9
|
+
export const DEF_IMPORT_RETRY_ATTEMPTS = 2
|
|
10
|
+
/** The pause before each retry, in order; the last one repeats. */
|
|
11
|
+
export const DEF_IMPORT_RETRY_DELAYS_MS: readonly number[] = [500, 1500]
|
|
12
|
+
|
|
13
|
+
/** The `sessionStorage` key `recoverFromChunkError` keeps the time of its last reload under. */
|
|
14
|
+
export const CHUNK_RELOAD_KEY = 'owlmeans:chunk-reload'
|
|
15
|
+
/** At most one chunk-recovery reload per this window, per tab. */
|
|
16
|
+
export const CHUNK_RELOAD_WINDOW_MS = 60_000
|
|
17
|
+
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ export * from './context.js'
|
|
|
3
3
|
export * from './components/index.js'
|
|
4
4
|
export * from './services/index.js'
|
|
5
5
|
export * from './helper.js'
|
|
6
|
+
export * from './lazy.js'
|
|
7
|
+
export * from './lazy-retry.js'
|
|
6
8
|
export * from './navigate.js'
|
|
7
9
|
export * from './entrypoint.js'
|
|
8
10
|
export * from './router.js'
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHUNK_RELOAD_KEY, CHUNK_RELOAD_WINDOW_MS, DEF_IMPORT_RETRY_ATTEMPTS, DEF_IMPORT_RETRY_DELAYS_MS
|
|
3
|
+
} from './consts.js'
|
|
4
|
+
import type { RetryImportOptions } from './types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a browser says when a dynamic `import()` could not fetch its module — Chromium, Safari and
|
|
8
|
+
* Firefox, in that order — and what Vite's preload helper says when a chunk's stylesheet could not.
|
|
9
|
+
*/
|
|
10
|
+
const CHUNK_FAILURE = /failed to fetch dynamically imported module|importing a module script failed|error loading dynamically imported module|unable to preload css/i
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether `error` is a chunk that could not be FETCHED: a browser's failed dynamic import, Vite's
|
|
14
|
+
* stylesheet preload failure, webpack's `ChunkLoadError`, Vite's `vite:preloadError` event, or
|
|
15
|
+
* the `error` event of the element a loader fetched through. A module that was fetched and then
|
|
16
|
+
* failed — a missing export, a throw while it evaluated — is not one: loading it again changes
|
|
17
|
+
* nothing.
|
|
18
|
+
*/
|
|
19
|
+
export const isChunkLoadError = (error: unknown): boolean => {
|
|
20
|
+
if (error == null || typeof error !== 'object') {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
if (typeof Event !== 'undefined' && error instanceof Event) {
|
|
24
|
+
return error.type === 'vite:preloadError' || error.type === 'error'
|
|
25
|
+
}
|
|
26
|
+
const { name, message } = error as { name?: unknown, message?: unknown }
|
|
27
|
+
|
|
28
|
+
return name === 'ChunkLoadError' || (typeof message === 'string' && CHUNK_FAILURE.test(message))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const pause = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms))
|
|
32
|
+
|
|
33
|
+
/** The module URL a Chromium or Firefox import failure names; Safari's message names none. */
|
|
34
|
+
const FAILED_URL = /dynamically imported module:?\s+(\S+)/i
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The URL of the chunk a failed dynamic import tried to fetch, when the browser's error names it
|
|
38
|
+
* and it is an http(s) URL of this page's origin — never an arbitrary URL out of a message. `null`
|
|
39
|
+
* otherwise (Safari, a non-browser platform, another origin).
|
|
40
|
+
*/
|
|
41
|
+
export const chunkUrlOf = (error: unknown): string | null => {
|
|
42
|
+
const message = (error as { message?: unknown } | null)?.message
|
|
43
|
+
const found = typeof message === 'string' ? FAILED_URL.exec(message)?.[1] : undefined
|
|
44
|
+
if (found == null) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(found)
|
|
49
|
+
const origin = typeof location !== 'undefined' ? location.origin : undefined
|
|
50
|
+
if ((url.protocol !== 'https:' && url.protocol !== 'http:') || (origin != null && url.origin !== origin)) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
return url.href
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `href` with a fresh `t` parameter: a URL the browser has not seen, so it fetches the module
|
|
61
|
+
* again. `t` is the parameter Vite's dev server already gives module URLs; a static host ignores it.
|
|
62
|
+
*/
|
|
63
|
+
export const cacheBustedUrl = (href: string, now: number = Date.now()): string => {
|
|
64
|
+
const url = new URL(href)
|
|
65
|
+
url.searchParams.set('t', String(now))
|
|
66
|
+
return url.href
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const importUrl = (url: string): Promise<unknown> =>
|
|
70
|
+
import(/* @vite-ignore */ /* webpackIgnore: true */ url)
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run `load` — a dynamic `import()` — and, while it rejects with a chunk-load failure, run it again
|
|
74
|
+
* after a pause, `attempts` more times at most. Any other rejection is rethrown at once.
|
|
75
|
+
*
|
|
76
|
+
* This covers a TRANSIENT failure — a network blip, an edge answering 404 or 5xx for a moment.
|
|
77
|
+
* A browser remembers a failed module fetch for the document's lifetime (Chromium does) and
|
|
78
|
+
* rejects every later `import()` of that URL at once, so a retry imports the URL the error names
|
|
79
|
+
* with a cache-busting parameter instead (`bustCache`, default on): a new URL, fetched again. Where
|
|
80
|
+
* the error names no URL (Safari), `load` runs again as is; what always recovers is a new
|
|
81
|
+
* document, which `recoverFromChunkError` starts, once and guarded.
|
|
82
|
+
*/
|
|
83
|
+
export const retryImport = async <M>(load: () => Promise<M>, opts?: RetryImportOptions): Promise<M> => {
|
|
84
|
+
const attempts = Math.max(0, opts?.attempts ?? DEF_IMPORT_RETRY_ATTEMPTS)
|
|
85
|
+
const delays = opts?.delaysMs ?? DEF_IMPORT_RETRY_DELAYS_MS
|
|
86
|
+
const shouldRetry = opts?.shouldRetry ?? isChunkLoadError
|
|
87
|
+
const bust = opts?.bustCache !== false
|
|
88
|
+
const importBusted = opts?.importUrl ?? importUrl
|
|
89
|
+
|
|
90
|
+
let next: () => Promise<M> = load
|
|
91
|
+
for (let attempt = 0; ; attempt++) {
|
|
92
|
+
try {
|
|
93
|
+
return await next()
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (attempt >= attempts || !shouldRetry(error)) {
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
const url = bust ? chunkUrlOf(error) : null
|
|
99
|
+
if (url != null) {
|
|
100
|
+
next = async () => await importBusted(cacheBustedUrl(url)) as M
|
|
101
|
+
}
|
|
102
|
+
await pause(delays[Math.min(attempt, delays.length - 1)] ?? 0)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reload the page, at most once per `windowMs` in this tab — the time of the last reload is kept
|
|
109
|
+
* in `sessionStorage` under `key`. Never while offline (the reload would land on the browser's own
|
|
110
|
+
* error page), and never without storage: with nowhere to keep the guard, a failure that survives
|
|
111
|
+
* the reload would reload forever. A platform with no page to reload does nothing. Answers whether
|
|
112
|
+
* a reload was started.
|
|
113
|
+
*/
|
|
114
|
+
export const reloadOnce = (key: string, windowMs: number): boolean => {
|
|
115
|
+
const location = typeof window !== 'undefined' ? window.location : undefined
|
|
116
|
+
if (typeof location?.reload !== 'function') {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const now = Date.now()
|
|
124
|
+
if (now - Number(window.sessionStorage.getItem(key) ?? 0) < windowMs) {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
window.sessionStorage.setItem(key, String(now))
|
|
128
|
+
} catch {
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
location.reload()
|
|
132
|
+
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The guarded reload a chunk that failed for good ends in: `reloadOnce` under one key every caller
|
|
138
|
+
* in the tab shares, so a lazy boundary and an application's own `vite:preloadError` listener
|
|
139
|
+
* never reload twice for one failure.
|
|
140
|
+
*/
|
|
141
|
+
export const recoverFromChunkError = (): boolean => reloadOnce(CHUNK_RELOAD_KEY, CHUNK_RELOAD_WINDOW_MS)
|
package/src/lazy.tsx
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { lazy, Suspense, Component, useState } from 'react'
|
|
2
|
+
import type { ComponentType, ReactNode } from 'react'
|
|
3
|
+
import { handler } from './helper.js'
|
|
4
|
+
import { isChunkLoadError, recoverFromChunkError, retryImport } from './lazy-retry.js'
|
|
5
|
+
import type { HandledRenderer } from './utils/route.js'
|
|
6
|
+
import type {
|
|
7
|
+
ComponentExport, ExportProps, LazyComponent, LazyComponentOptions, LazyHandler
|
|
8
|
+
} from './types.js'
|
|
9
|
+
|
|
10
|
+
type Loaded = ComponentType<any>
|
|
11
|
+
type LazyModule = { default: Loaded }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrap an async module import as a stable lazily-loaded component with a `.preload()`.
|
|
15
|
+
*
|
|
16
|
+
* MUST be called at module scope, never inside a render, a hook body or an entrypoint handler
|
|
17
|
+
* factory — the OwlMeans route renderer (`utils/route.tsx`) remounts the whole route subtree on
|
|
18
|
+
* every navigation (a fresh component identity per render), so a lazy object created during
|
|
19
|
+
* render would re-suspend on every navigation instead of rendering instantly once loaded.
|
|
20
|
+
*
|
|
21
|
+
* The `Suspense` boundary sits inside the returned component, so the fallback replaces only this
|
|
22
|
+
* component — the layout around it stays mounted while the chunk loads.
|
|
23
|
+
*
|
|
24
|
+
* Once loaded, later renders resolve synchronously (no re-suspend), so a component whose chunk
|
|
25
|
+
* was already preloaded never shows its fallback. A load that fails to fetch is retried in place
|
|
26
|
+
* (`retryImport`, tuned or turned off by `opts.retry`); one that still fails is never cached: the
|
|
27
|
+
* internal `React.lazy` is recreated, so the next mount, `retry()` or `preload()` loads for real.
|
|
28
|
+
*
|
|
29
|
+
* The component always carries its own error boundary, so a chunk that failed for good never
|
|
30
|
+
* unmounts what is around it: `opts.error` renders in its place, or — without one — the guarded
|
|
31
|
+
* reload (`recoverFromChunkError`) starts and the fallback stays. Any other error without
|
|
32
|
+
* `opts.error` goes on to the nearest boundary above, as if there were none.
|
|
33
|
+
*/
|
|
34
|
+
export const lazyComponent = <M, K extends ComponentExport<M>>(
|
|
35
|
+
load: () => Promise<M>, exportName: K, opts?: LazyComponentOptions
|
|
36
|
+
): LazyComponent<ExportProps<M, K>> => {
|
|
37
|
+
let loaded: Loaded | undefined
|
|
38
|
+
let loading: Promise<Loaded> | undefined
|
|
39
|
+
|
|
40
|
+
const fetchModule = (): Promise<M> => opts?.retry === false ? load() : retryImport(load, opts?.retry)
|
|
41
|
+
|
|
42
|
+
const runLoad = (): Promise<Loaded> => loading ??= fetchModule().then(module => {
|
|
43
|
+
const Comp = (module as unknown as Record<string, Loaded | undefined>)[exportName]
|
|
44
|
+
if (Comp == null) {
|
|
45
|
+
throw new SyntaxError(`Lazy module has no component export "${exportName}"`)
|
|
46
|
+
}
|
|
47
|
+
return loaded = Comp
|
|
48
|
+
}).catch((error: unknown) => {
|
|
49
|
+
loading = undefined
|
|
50
|
+
current = createLazy()
|
|
51
|
+
throw error
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const preload = (): Promise<Loaded> => loaded != null ? Promise.resolve(loaded) : runLoad()
|
|
55
|
+
|
|
56
|
+
const createLazy = () => lazy<Loaded>(() => {
|
|
57
|
+
const ready = loaded
|
|
58
|
+
if (ready != null) {
|
|
59
|
+
// Already resolved: hand React a thenable that settles synchronously, so its lazy
|
|
60
|
+
// initializer reads the module in the same tick and never suspends.
|
|
61
|
+
const settled = { then: (resolve: (module: LazyModule) => void) => resolve({ default: ready }) }
|
|
62
|
+
return settled as unknown as Promise<LazyModule>
|
|
63
|
+
}
|
|
64
|
+
return runLoad().then(Comp => ({ default: Comp }))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
let current = createLazy()
|
|
68
|
+
|
|
69
|
+
const Lazy = (props: Record<string, unknown>) => {
|
|
70
|
+
// The lazy THIS instance renders, taken when it mounts: a failed load stays failed for it
|
|
71
|
+
// while React re-renders it to recover from the error, until `retry` swaps in the recreated
|
|
72
|
+
// lazy. A new mount — a navigation back, another place — takes the recreated one at once.
|
|
73
|
+
const [Current, setCurrent] = useState(() => current)
|
|
74
|
+
const fallback = (typeof opts?.fallback === 'function' ? opts.fallback(props) : opts?.fallback) ?? null
|
|
75
|
+
|
|
76
|
+
return <LazyErrorBoundary error={opts?.error} reload={opts?.reload === true} fallback={fallback} props={props}
|
|
77
|
+
onRetry={() => setCurrent(() => current)}>
|
|
78
|
+
<Suspense fallback={fallback}><Current {...props} /></Suspense>
|
|
79
|
+
</LazyErrorBoundary>
|
|
80
|
+
}
|
|
81
|
+
Lazy.displayName = `Lazy(${exportName})`
|
|
82
|
+
|
|
83
|
+
return Object.assign(Lazy, { preload }) as unknown as LazyComponent<ExportProps<M, K>>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface LazyErrorBoundaryProps {
|
|
87
|
+
error: LazyComponentOptions['error']
|
|
88
|
+
/** Start the guarded reload for a chunk failure even though `error` is given. */
|
|
89
|
+
reload: boolean
|
|
90
|
+
fallback: ReactNode
|
|
91
|
+
props: Record<string, unknown>
|
|
92
|
+
/** Swap the failed lazy for the recreated one — batched with the boundary's own reset. */
|
|
93
|
+
onRetry: () => void
|
|
94
|
+
children: ReactNode
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface LazyErrorBoundaryState {
|
|
98
|
+
failed: boolean
|
|
99
|
+
error: unknown
|
|
100
|
+
/** A guarded reload has started: keep the fallback up until the new document replaces this one. */
|
|
101
|
+
reloading?: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class LazyErrorBoundary extends Component<LazyErrorBoundaryProps, LazyErrorBoundaryState> {
|
|
105
|
+
override state: LazyErrorBoundaryState = { failed: false, error: undefined }
|
|
106
|
+
|
|
107
|
+
static getDerivedStateFromError(error: unknown): LazyErrorBoundaryState {
|
|
108
|
+
return { failed: true, error }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override componentDidCatch(error: unknown): void {
|
|
112
|
+
// A chunk that failed for good is recovered by a new document: always without a surface of its
|
|
113
|
+
// own, and with one when asked (`reload`) — a whole screen rather than a piece that degrades.
|
|
114
|
+
// The reload is guarded (once a minute per tab); when the guard refuses, the surface stays.
|
|
115
|
+
if (isChunkLoadError(error) && (this.props.error === undefined || this.props.reload)) {
|
|
116
|
+
if (recoverFromChunkError()) {
|
|
117
|
+
this.setState({ reloading: true })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Render the children again, over the recreated lazy, which loads anew. */
|
|
123
|
+
readonly retry = (): void => {
|
|
124
|
+
this.setState({ failed: false, error: undefined, reloading: false })
|
|
125
|
+
this.props.onRetry()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
override render() {
|
|
129
|
+
if (!this.state.failed) {
|
|
130
|
+
return this.props.children
|
|
131
|
+
}
|
|
132
|
+
const { error, fallback, props } = this.props
|
|
133
|
+
if (this.state.reloading === true) {
|
|
134
|
+
return fallback
|
|
135
|
+
}
|
|
136
|
+
if (error !== undefined) {
|
|
137
|
+
return typeof error === 'function' ? error(props, this.state.error, this.retry) : error
|
|
138
|
+
}
|
|
139
|
+
if (isChunkLoadError(this.state.error)) {
|
|
140
|
+
return fallback
|
|
141
|
+
}
|
|
142
|
+
// Not a chunk failure and no surface for it: the application's own boundary decides.
|
|
143
|
+
throw this.state.error
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* `handler(lazyComponent(...))`, carrying `.preload()` through — bind it to a screen exactly like
|
|
149
|
+
* `handler(Component)`. The same module-scope rule applies: call it where the entrypoints are
|
|
150
|
+
* declared, never inside a render. A screen whose chunk still fails after its retries starts the
|
|
151
|
+
* guarded reload before showing its `error` (`reload` defaults to `true` here): a whole screen has
|
|
152
|
+
* nothing to degrade to, and in a dev-served app only a new document can load it again.
|
|
153
|
+
*/
|
|
154
|
+
export const lazyHandler = <M, K extends ComponentExport<M>>(
|
|
155
|
+
load: () => Promise<M>, exportName: K, opts?: LazyComponentOptions
|
|
156
|
+
): LazyHandler<ExportProps<M, K>> => {
|
|
157
|
+
const LazyComp = lazyComponent(load, exportName, { ...opts, reload: opts?.reload ?? true })
|
|
158
|
+
const refed = handler(LazyComp as unknown as HandledRenderer<{}>)
|
|
159
|
+
|
|
160
|
+
return Object.assign(refed, { preload: LazyComp.preload }) as unknown as LazyHandler<ExportProps<M, K>>
|
|
161
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { PropsWithChildren, FC, DependencyList } from 'react'
|
|
1
|
+
import type { PropsWithChildren, FC, DependencyList, ComponentType, ReactNode } from 'react'
|
|
2
2
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
3
3
|
import type { ClientConfig, ClientContext as BasicClientContext } from '@owlmeans/client-context'
|
|
4
4
|
import type { Criteria, ResourceRecord, Sort } from '@owlmeans/resource'
|
|
5
5
|
import type { StateResourceAppend } from '@owlmeans/state'
|
|
6
|
-
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
6
|
+
import type { ClientEntrypoint, RefedEntrypointHandler } from '@owlmeans/client-entrypoint'
|
|
7
7
|
import type { DebugServiceAppend, ModalServiceAppend } from './components/types.js'
|
|
8
8
|
import type { ConfigResourceAppend } from '@owlmeans/config'
|
|
9
9
|
import type { ConfigRecord, EntrypointReference } from '@owlmeans/context'
|
|
@@ -92,3 +92,78 @@ export interface UseStoreListOptions<T extends ResourceRecord = ResourceRecord>
|
|
|
92
92
|
/** Which state resource to read; the context's default one when omitted. */
|
|
93
93
|
resource?: string
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
/** How `retryImport` retries a rejected dynamic import. */
|
|
97
|
+
export interface RetryImportOptions {
|
|
98
|
+
/** Loads after the first failure, at most; `0` loads once. Default 2. */
|
|
99
|
+
attempts?: number
|
|
100
|
+
/** The pause before each retry, in order; the last one repeats. Default 500 ms, then 1500 ms. */
|
|
101
|
+
delaysMs?: readonly number[]
|
|
102
|
+
/**
|
|
103
|
+
* Which rejections are retried. Default `isChunkLoadError` — a module that could not be fetched,
|
|
104
|
+
* never one that was fetched and then failed.
|
|
105
|
+
*/
|
|
106
|
+
shouldRetry?: (error: unknown) => boolean
|
|
107
|
+
/**
|
|
108
|
+
* Retry a failed chunk from its own URL with a cache-busting `t` parameter when the browser's
|
|
109
|
+
* error names it (Chromium, Firefox). Default `true`: a browser that remembers a failed module
|
|
110
|
+
* fetch rejects every later `import()` of the same URL at once, so only a new URL is fetched
|
|
111
|
+
* again. `false` repeats `load` as is.
|
|
112
|
+
*/
|
|
113
|
+
bustCache?: boolean
|
|
114
|
+
/** How a cache-busted URL is imported; a seam for tests. Default: a dynamic `import()`. */
|
|
115
|
+
importUrl?: (url: string) => Promise<unknown>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What a lazily-loaded piece renders once it failed: the props it was given, the error, and
|
|
120
|
+
* `retry`, which resets the piece's boundary so its chunk loads again.
|
|
121
|
+
*/
|
|
122
|
+
export type LazyErrorRenderer = (props: any, error: unknown, retry: () => void) => ReactNode
|
|
123
|
+
|
|
124
|
+
/** How a lazily-loaded component behaves while its chunk loads and when the load fails. */
|
|
125
|
+
export interface LazyComponentOptions {
|
|
126
|
+
/** Shown while the chunk loads: a node, or a function of the props the component was given. */
|
|
127
|
+
fallback?: ReactNode | ((props: any) => ReactNode)
|
|
128
|
+
/**
|
|
129
|
+
* Rendered in place of the component when its chunk fails to load, or it throws: a node, or a
|
|
130
|
+
* `LazyErrorRenderer`. Omitted, a chunk failure starts the guarded reload
|
|
131
|
+
* (`recoverFromChunkError`) and leaves `fallback` in place, and any other error propagates to
|
|
132
|
+
* the nearest boundary above.
|
|
133
|
+
*/
|
|
134
|
+
error?: ReactNode | LazyErrorRenderer
|
|
135
|
+
/**
|
|
136
|
+
* How a rejected load is retried before it counts as failed — `retryImport`'s options, its
|
|
137
|
+
* defaults when omitted. `false` loads once.
|
|
138
|
+
*/
|
|
139
|
+
retry?: false | RetryImportOptions
|
|
140
|
+
/**
|
|
141
|
+
* When a chunk still fails after its retries, start the guarded page reload
|
|
142
|
+
* (`recoverFromChunkError`, once a minute per tab) before showing `error`; the fallback stays up
|
|
143
|
+
* while the reload starts, and `error` shows when the guard refuses. Without `error` a chunk
|
|
144
|
+
* failure always reloads. Default `false` for `lazyComponent` (a piece degrades in place), `true`
|
|
145
|
+
* for `lazyHandler` (a whole screen). A dev-served app needs it: its modules import themselves by
|
|
146
|
+
* their own URL (React Fast Refresh), so a browser that remembers the failed fetch can load that
|
|
147
|
+
* module again only in a new document.
|
|
148
|
+
*/
|
|
149
|
+
reload?: boolean
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface LazyPreload<P = {}> {
|
|
153
|
+
/** Start (or join) the chunk load; resolves to the component once it is ready. */
|
|
154
|
+
preload: () => Promise<ComponentType<P>>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A lazily-loaded component: renders like the real one, and can be preloaded ahead of use. */
|
|
158
|
+
export type LazyComponent<P = {}> = ComponentType<P> & LazyPreload<P>
|
|
159
|
+
|
|
160
|
+
/** An entrypoint handler over a lazily-loaded component, carrying its `.preload()`. */
|
|
161
|
+
export type LazyHandler<P = {}> = RefedEntrypointHandler<P> & LazyPreload<P>
|
|
162
|
+
|
|
163
|
+
/** Names of a module's exports that are React components. */
|
|
164
|
+
export type ComponentExport<M> = {
|
|
165
|
+
[K in keyof M]-?: M[K] extends ComponentType<any> ? K : never
|
|
166
|
+
}[keyof M] & string
|
|
167
|
+
|
|
168
|
+
/** The props of a module's component export. */
|
|
169
|
+
export type ExportProps<M, K extends keyof M> = M[K] extends ComponentType<infer P> ? P : never
|
package/tests/context.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createServer } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
let url: string | null = null
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Boot a Vite dev server serving `tests/harness/` — a real HTTP origin, because a lazily-loaded
|
|
12
|
+
* piece is a module request of its own and a failed one ends in a real page reload, neither of
|
|
13
|
+
* which a `data:` URL can host. One server per `bun test` process.
|
|
14
|
+
*/
|
|
15
|
+
export const getHarnessUrl = async (): Promise<string> => {
|
|
16
|
+
if (url != null) return url
|
|
17
|
+
const server = await createServer({
|
|
18
|
+
configFile: false,
|
|
19
|
+
root: resolve(here, './harness'),
|
|
20
|
+
plugins: [react()],
|
|
21
|
+
// Single React instance so hooks work across the workspace-linked packages.
|
|
22
|
+
resolve: { dedupe: ['react', 'react-dom'] },
|
|
23
|
+
server: { port: 0 },
|
|
24
|
+
logLevel: 'warn'
|
|
25
|
+
})
|
|
26
|
+
await server.listen()
|
|
27
|
+
const local = server.resolvedUrls?.local?.[0]
|
|
28
|
+
if (local == null) throw new Error('vite did not expose a local URL')
|
|
29
|
+
url = local
|
|
30
|
+
return url
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const HARNESS_URL = await getHarnessUrl()
|