hadars 0.1.30 → 0.1.32
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 +10 -14
- package/dist/cli.js +3 -1
- package/dist/index.cjs +1 -30
- package/dist/index.d.ts +4 -15
- package/dist/index.js +1 -30
- package/dist/slim-react/index.cjs +3 -1
- package/dist/slim-react/index.js +3 -1
- package/dist/ssr-render-worker.js +3 -1
- package/dist/utils/Head.tsx +6 -51
- package/package.json +1 -1
- package/src/slim-react/render.ts +2 -1
- package/src/types/hadars.ts +0 -3
- package/src/utils/Head.tsx +6 -51
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@ hadars is an alternative to Next.js for apps that just need SSR.
|
|
|
16
16
|
|
|
17
17
|
Bring your own router (or none), keep your components as plain React, and get SSR, HMR, and a production build from a single config file.
|
|
18
18
|
|
|
19
|
+
## Benchmarks
|
|
20
|
+
|
|
21
|
+
Benchmarks against an equivalent Next.js app show significantly faster server throughput (requests/second) and meaningfully better page load metrics (TTFB, FCP, DOMContentLoaded). Build times are also much lower due to rspack.
|
|
22
|
+
|
|
19
23
|
## Quick start
|
|
20
24
|
|
|
21
25
|
Scaffold a new project in seconds:
|
|
@@ -114,7 +118,6 @@ const UserCard = ({ userId }: { userId: string }) => {
|
|
|
114
118
|
- **`key`** - string or string array; must be stable and unique within the page
|
|
115
119
|
- **Server** - calls `fn()`, awaits the result across render iterations, returns `undefined` until resolved
|
|
116
120
|
- **Client** - reads the pre-resolved value from the hydration cache serialised by the server; `fn()` is never called in the browser
|
|
117
|
-
- **Suspense libraries** - also works when `fn()` throws a thenable (e.g. Relay `useLazyLoadQuery` with `suspense: true`); the thrown promise is awaited and the next render re-calls `fn()` synchronously
|
|
118
121
|
|
|
119
122
|
## Data lifecycle hooks
|
|
120
123
|
|
|
@@ -182,21 +185,14 @@ export default config;
|
|
|
182
185
|
|
|
183
186
|
## slim-react
|
|
184
187
|
|
|
185
|
-
hadars ships its own lightweight React-compatible SSR renderer called **slim-react** (`src/slim-react/`). It replaces `react-dom/server`
|
|
186
|
-
|
|
187
|
-
For server builds, rspack aliases `react` and `react/jsx-runtime` to slim-react, so your components and any libraries they import render through it automatically without any code changes.
|
|
188
|
-
|
|
189
|
-
**What it does:**
|
|
190
|
-
|
|
191
|
-
- Renders the full component tree to an HTML string with native `async/await` support - async components and hooks that return Promises are awaited directly without streaming workarounds
|
|
192
|
-
- Implements the React Suspense protocol: when a component throws a Promise (e.g. from `useServerData` or a Suspense-enabled data library), slim-react awaits it and retries the tree automatically
|
|
193
|
-
- Emits React-compatible hydration markers - `<!--$-->…<!--/$-->` for resolved Suspense boundaries, `<!-- -->` separators between adjacent text nodes - so `hydrateRoot` on the client works without mismatches
|
|
194
|
-
- Supports `React.memo`, `React.forwardRef`, `React.lazy`, `Context.Provider`, `Context.Consumer`, and the React 18/19 element wire formats
|
|
195
|
-
- Covers the full hook surface needed for SSR: `useState`, `useReducer`, `useContext`, `useRef`, `useMemo`, `useCallback`, `useId`, `useSyncExternalStore`, `use`, and more - all as lightweight SSR stubs
|
|
188
|
+
hadars ships its own lightweight React-compatible SSR renderer called **slim-react** (`src/slim-react/`). It replaces `react-dom/server` on the server side entirely.
|
|
196
189
|
|
|
197
|
-
|
|
190
|
+
For server builds, rspack aliases `react` and `react/jsx-runtime` to slim-react, so your components and any libraries they import render through it automatically without code changes.
|
|
198
191
|
|
|
199
|
-
|
|
192
|
+
- Renders the full component tree to an HTML string with native `async/await` — async components are awaited directly
|
|
193
|
+
- Implements the React Suspense protocol: thrown Promises (e.g. from `useSuspenseQuery`) are awaited and the component retried automatically
|
|
194
|
+
- Compatible with `hydrateRoot` — output matches what React expects on the client
|
|
195
|
+
- Supports `React.memo`, `React.forwardRef`, `React.lazy`, `Context.Provider`, `Context.Consumer`, and the React 19 element format
|
|
200
196
|
|
|
201
197
|
## License
|
|
202
198
|
|
package/dist/cli.js
CHANGED
|
@@ -558,7 +558,9 @@ function renderAttributes(props, isSvg) {
|
|
|
558
558
|
continue;
|
|
559
559
|
}
|
|
560
560
|
if (key === "style" && typeof value === "object") {
|
|
561
|
-
|
|
561
|
+
const styleStr = styleObjectToString(value);
|
|
562
|
+
if (styleStr)
|
|
563
|
+
attrs += ` style="${escapeAttr(styleStr)}"`;
|
|
562
564
|
continue;
|
|
563
565
|
}
|
|
564
566
|
attrs += ` ${attrName}="${escapeAttr(String(value))}"`;
|
package/dist/index.cjs
CHANGED
|
@@ -189,37 +189,8 @@ function useServerData(key, fn) {
|
|
|
189
189
|
if (!unsuspend)
|
|
190
190
|
return void 0;
|
|
191
191
|
const existing = unsuspend.cache.get(cacheKey);
|
|
192
|
-
if (existing?.status === "suspense-resolved") {
|
|
193
|
-
try {
|
|
194
|
-
const value = fn();
|
|
195
|
-
unsuspend.cache.set(cacheKey, { status: "suspense-cached", value });
|
|
196
|
-
return value;
|
|
197
|
-
} catch {
|
|
198
|
-
return void 0;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (existing?.status === "suspense-cached") {
|
|
202
|
-
return existing.value;
|
|
203
|
-
}
|
|
204
192
|
if (!existing) {
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
result = fn();
|
|
208
|
-
} catch (thrown) {
|
|
209
|
-
if (thrown !== null && typeof thrown === "object" && typeof thrown.then === "function") {
|
|
210
|
-
const suspensePromise = Promise.resolve(thrown).then(
|
|
211
|
-
() => {
|
|
212
|
-
unsuspend.cache.set(cacheKey, { status: "suspense-resolved" });
|
|
213
|
-
},
|
|
214
|
-
() => {
|
|
215
|
-
unsuspend.cache.set(cacheKey, { status: "suspense-resolved" });
|
|
216
|
-
}
|
|
217
|
-
);
|
|
218
|
-
unsuspend.cache.set(cacheKey, { status: "pending", promise: suspensePromise });
|
|
219
|
-
throw suspensePromise;
|
|
220
|
-
}
|
|
221
|
-
throw thrown;
|
|
222
|
-
}
|
|
193
|
+
const result = fn();
|
|
223
194
|
const isThenable = result !== null && typeof result === "object" && typeof result.then === "function";
|
|
224
195
|
if (!isThenable) {
|
|
225
196
|
const value = result;
|
package/dist/index.d.ts
CHANGED
|
@@ -28,11 +28,6 @@ type UnsuspendEntry = {
|
|
|
28
28
|
} | {
|
|
29
29
|
status: 'fulfilled';
|
|
30
30
|
value: unknown;
|
|
31
|
-
} | {
|
|
32
|
-
status: 'suspense-resolved';
|
|
33
|
-
} | {
|
|
34
|
-
status: 'suspense-cached';
|
|
35
|
-
value: unknown;
|
|
36
31
|
} | {
|
|
37
32
|
status: 'rejected';
|
|
38
33
|
reason: unknown;
|
|
@@ -40,7 +35,6 @@ type UnsuspendEntry = {
|
|
|
40
35
|
/** @internal Populated by the framework's render loop — use useServerData() instead. */
|
|
41
36
|
interface AppUnsuspend {
|
|
42
37
|
cache: Map<string, UnsuspendEntry>;
|
|
43
|
-
hasPending: boolean;
|
|
44
38
|
}
|
|
45
39
|
interface AppContext {
|
|
46
40
|
path?: string;
|
|
@@ -197,16 +191,11 @@ declare function initServerDataCache(data: Record<string, unknown>): void;
|
|
|
197
191
|
* across all SSR render passes and client hydration — it must be stable and
|
|
198
192
|
* unique within the page.
|
|
199
193
|
*
|
|
200
|
-
* `fn` may return a `Promise<T>` (
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* - Synchronous return: value stored immediately, returned on the same pass.
|
|
204
|
-
* - Thrown thenable (e.g. `useQuery({ suspense: true })`): the thrown promise is
|
|
205
|
-
* awaited, the cache entry is then cleared so that the next render re-calls
|
|
206
|
-
* `fn()` — at that point the Suspense hook returns synchronously.
|
|
194
|
+
* `fn` may return a `Promise<T>` (async usage) or return `T` synchronously.
|
|
195
|
+
* The resolved value is serialised into `__serverData` and returned from cache
|
|
196
|
+
* during hydration.
|
|
207
197
|
*
|
|
208
|
-
* `fn` is **server-only**: it is never called in the browser.
|
|
209
|
-
* is serialised into `__serverData` and returned from cache during hydration.
|
|
198
|
+
* `fn` is **server-only**: it is never called in the browser.
|
|
210
199
|
*
|
|
211
200
|
* @example
|
|
212
201
|
* const user = useServerData('current_user', () => db.getUser(id));
|
package/dist/index.js
CHANGED
|
@@ -146,37 +146,8 @@ function useServerData(key, fn) {
|
|
|
146
146
|
if (!unsuspend)
|
|
147
147
|
return void 0;
|
|
148
148
|
const existing = unsuspend.cache.get(cacheKey);
|
|
149
|
-
if (existing?.status === "suspense-resolved") {
|
|
150
|
-
try {
|
|
151
|
-
const value = fn();
|
|
152
|
-
unsuspend.cache.set(cacheKey, { status: "suspense-cached", value });
|
|
153
|
-
return value;
|
|
154
|
-
} catch {
|
|
155
|
-
return void 0;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (existing?.status === "suspense-cached") {
|
|
159
|
-
return existing.value;
|
|
160
|
-
}
|
|
161
149
|
if (!existing) {
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
result = fn();
|
|
165
|
-
} catch (thrown) {
|
|
166
|
-
if (thrown !== null && typeof thrown === "object" && typeof thrown.then === "function") {
|
|
167
|
-
const suspensePromise = Promise.resolve(thrown).then(
|
|
168
|
-
() => {
|
|
169
|
-
unsuspend.cache.set(cacheKey, { status: "suspense-resolved" });
|
|
170
|
-
},
|
|
171
|
-
() => {
|
|
172
|
-
unsuspend.cache.set(cacheKey, { status: "suspense-resolved" });
|
|
173
|
-
}
|
|
174
|
-
);
|
|
175
|
-
unsuspend.cache.set(cacheKey, { status: "pending", promise: suspensePromise });
|
|
176
|
-
throw suspensePromise;
|
|
177
|
-
}
|
|
178
|
-
throw thrown;
|
|
179
|
-
}
|
|
150
|
+
const result = fn();
|
|
180
151
|
const isThenable = result !== null && typeof result === "object" && typeof result.then === "function";
|
|
181
152
|
if (!isThenable) {
|
|
182
153
|
const value = result;
|
|
@@ -541,7 +541,9 @@ function renderAttributes(props, isSvg) {
|
|
|
541
541
|
continue;
|
|
542
542
|
}
|
|
543
543
|
if (key === "style" && typeof value === "object") {
|
|
544
|
-
|
|
544
|
+
const styleStr = styleObjectToString(value);
|
|
545
|
+
if (styleStr)
|
|
546
|
+
attrs += ` style="${escapeAttr(styleStr)}"`;
|
|
545
547
|
continue;
|
|
546
548
|
}
|
|
547
549
|
attrs += ` ${attrName}="${escapeAttr(String(value))}"`;
|
package/dist/slim-react/index.js
CHANGED
|
@@ -440,7 +440,9 @@ function renderAttributes(props, isSvg) {
|
|
|
440
440
|
continue;
|
|
441
441
|
}
|
|
442
442
|
if (key === "style" && typeof value === "object") {
|
|
443
|
-
|
|
443
|
+
const styleStr = styleObjectToString(value);
|
|
444
|
+
if (styleStr)
|
|
445
|
+
attrs += ` style="${escapeAttr(styleStr)}"`;
|
|
444
446
|
continue;
|
|
445
447
|
}
|
|
446
448
|
attrs += ` ${attrName}="${escapeAttr(String(value))}"`;
|
|
@@ -465,7 +465,9 @@ function renderAttributes(props, isSvg) {
|
|
|
465
465
|
continue;
|
|
466
466
|
}
|
|
467
467
|
if (key === "style" && typeof value === "object") {
|
|
468
|
-
|
|
468
|
+
const styleStr = styleObjectToString(value);
|
|
469
|
+
if (styleStr)
|
|
470
|
+
attrs += ` style="${escapeAttr(styleStr)}"`;
|
|
469
471
|
continue;
|
|
470
472
|
}
|
|
471
473
|
attrs += ` ${attrName}="${escapeAttr(String(value))}"`;
|
package/dist/utils/Head.tsx
CHANGED
|
@@ -196,16 +196,11 @@ export function initServerDataCache(data: Record<string, unknown>) {
|
|
|
196
196
|
* across all SSR render passes and client hydration — it must be stable and
|
|
197
197
|
* unique within the page.
|
|
198
198
|
*
|
|
199
|
-
* `fn` may return a `Promise<T>` (
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* - Synchronous return: value stored immediately, returned on the same pass.
|
|
203
|
-
* - Thrown thenable (e.g. `useQuery({ suspense: true })`): the thrown promise is
|
|
204
|
-
* awaited, the cache entry is then cleared so that the next render re-calls
|
|
205
|
-
* `fn()` — at that point the Suspense hook returns synchronously.
|
|
199
|
+
* `fn` may return a `Promise<T>` (async usage) or return `T` synchronously.
|
|
200
|
+
* The resolved value is serialised into `__serverData` and returned from cache
|
|
201
|
+
* during hydration.
|
|
206
202
|
*
|
|
207
|
-
* `fn` is **server-only**: it is never called in the browser.
|
|
208
|
-
* is serialised into `__serverData` and returned from cache during hydration.
|
|
203
|
+
* `fn` is **server-only**: it is never called in the browser.
|
|
209
204
|
*
|
|
210
205
|
* @example
|
|
211
206
|
* const user = useServerData('current_user', () => db.getUser(id));
|
|
@@ -230,51 +225,11 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
230
225
|
|
|
231
226
|
const existing = unsuspend.cache.get(cacheKey);
|
|
232
227
|
|
|
233
|
-
// Suspense promise has resolved — re-call fn() so the hook returns its value
|
|
234
|
-
// synchronously from its own internal cache. Cache the result as
|
|
235
|
-
// 'suspense-cached' so later renders (e.g. the final renderToString in
|
|
236
|
-
// buildSsrResponse, which runs after getFinalProps may have cleared the
|
|
237
|
-
// user's QueryClient) can return the value without calling fn() again.
|
|
238
|
-
// NOT stored as 'fulfilled' so it is never included in serverData sent to
|
|
239
|
-
// the client — the Suspense library owns its own hydration.
|
|
240
|
-
if (existing?.status === 'suspense-resolved') {
|
|
241
|
-
try {
|
|
242
|
-
const value = fn() as T;
|
|
243
|
-
unsuspend.cache.set(cacheKey, { status: 'suspense-cached', value });
|
|
244
|
-
return value;
|
|
245
|
-
} catch {
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Return the cached Suspense value on all subsequent renders.
|
|
251
|
-
if (existing?.status === 'suspense-cached') {
|
|
252
|
-
return existing.value as T;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
228
|
if (!existing) {
|
|
256
229
|
// First encounter — call fn(), which may:
|
|
257
|
-
// (a) return a Promise<T> —
|
|
230
|
+
// (a) return a Promise<T> — async usage (serialised for the client)
|
|
258
231
|
// (b) return T synchronously — e.g. a sync data source
|
|
259
|
-
|
|
260
|
-
let result: Promise<T> | T;
|
|
261
|
-
try {
|
|
262
|
-
result = fn();
|
|
263
|
-
} catch (thrown) {
|
|
264
|
-
// (c) Suspense protocol: fn() threw a thenable. Await it, then mark the
|
|
265
|
-
// entry as 'suspense-resolved' so the next render re-calls fn() to get
|
|
266
|
-
// the synchronously available value. Not stored as 'fulfilled' → not
|
|
267
|
-
// serialised to the client (the Suspense library handles its own hydration).
|
|
268
|
-
if (thrown !== null && typeof thrown === 'object' && typeof (thrown as any).then === 'function') {
|
|
269
|
-
const suspensePromise = Promise.resolve(thrown as Promise<unknown>).then(
|
|
270
|
-
() => { unsuspend.cache.set(cacheKey, { status: 'suspense-resolved' }); },
|
|
271
|
-
() => { unsuspend.cache.set(cacheKey, { status: 'suspense-resolved' }); },
|
|
272
|
-
);
|
|
273
|
-
unsuspend.cache.set(cacheKey, { status: 'pending', promise: suspensePromise });
|
|
274
|
-
throw suspensePromise; // slim-react will await and retry
|
|
275
|
-
}
|
|
276
|
-
throw thrown;
|
|
277
|
-
}
|
|
232
|
+
const result = fn();
|
|
278
233
|
|
|
279
234
|
const isThenable = result !== null && typeof result === 'object' && typeof (result as any).then === 'function';
|
|
280
235
|
|
package/package.json
CHANGED
package/src/slim-react/render.ts
CHANGED
|
@@ -300,7 +300,8 @@ function renderAttributes(props: Record<string, any>, isSvg: boolean): string {
|
|
|
300
300
|
continue;
|
|
301
301
|
}
|
|
302
302
|
if (key === "style" && typeof value === "object") {
|
|
303
|
-
|
|
303
|
+
const styleStr = styleObjectToString(value);
|
|
304
|
+
if (styleStr) attrs += ` style="${escapeAttr(styleStr)}"`;
|
|
304
305
|
continue;
|
|
305
306
|
}
|
|
306
307
|
attrs += ` ${attrName}="${escapeAttr(String(value))}"`;
|
package/src/types/hadars.ts
CHANGED
|
@@ -26,14 +26,11 @@ export interface AppHead {
|
|
|
26
26
|
export type UnsuspendEntry =
|
|
27
27
|
| { status: 'pending'; promise: Promise<unknown> }
|
|
28
28
|
| { status: 'fulfilled'; value: unknown }
|
|
29
|
-
| { status: 'suspense-resolved' }
|
|
30
|
-
| { status: 'suspense-cached'; value: unknown }
|
|
31
29
|
| { status: 'rejected'; reason: unknown };
|
|
32
30
|
|
|
33
31
|
/** @internal Populated by the framework's render loop — use useServerData() instead. */
|
|
34
32
|
export interface AppUnsuspend {
|
|
35
33
|
cache: Map<string, UnsuspendEntry>;
|
|
36
|
-
hasPending: boolean;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
export interface AppContext {
|
package/src/utils/Head.tsx
CHANGED
|
@@ -196,16 +196,11 @@ export function initServerDataCache(data: Record<string, unknown>) {
|
|
|
196
196
|
* across all SSR render passes and client hydration — it must be stable and
|
|
197
197
|
* unique within the page.
|
|
198
198
|
*
|
|
199
|
-
* `fn` may return a `Promise<T>` (
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* - Synchronous return: value stored immediately, returned on the same pass.
|
|
203
|
-
* - Thrown thenable (e.g. `useQuery({ suspense: true })`): the thrown promise is
|
|
204
|
-
* awaited, the cache entry is then cleared so that the next render re-calls
|
|
205
|
-
* `fn()` — at that point the Suspense hook returns synchronously.
|
|
199
|
+
* `fn` may return a `Promise<T>` (async usage) or return `T` synchronously.
|
|
200
|
+
* The resolved value is serialised into `__serverData` and returned from cache
|
|
201
|
+
* during hydration.
|
|
206
202
|
*
|
|
207
|
-
* `fn` is **server-only**: it is never called in the browser.
|
|
208
|
-
* is serialised into `__serverData` and returned from cache during hydration.
|
|
203
|
+
* `fn` is **server-only**: it is never called in the browser.
|
|
209
204
|
*
|
|
210
205
|
* @example
|
|
211
206
|
* const user = useServerData('current_user', () => db.getUser(id));
|
|
@@ -230,51 +225,11 @@ export function useServerData<T>(key: string | string[], fn: () => Promise<T> |
|
|
|
230
225
|
|
|
231
226
|
const existing = unsuspend.cache.get(cacheKey);
|
|
232
227
|
|
|
233
|
-
// Suspense promise has resolved — re-call fn() so the hook returns its value
|
|
234
|
-
// synchronously from its own internal cache. Cache the result as
|
|
235
|
-
// 'suspense-cached' so later renders (e.g. the final renderToString in
|
|
236
|
-
// buildSsrResponse, which runs after getFinalProps may have cleared the
|
|
237
|
-
// user's QueryClient) can return the value without calling fn() again.
|
|
238
|
-
// NOT stored as 'fulfilled' so it is never included in serverData sent to
|
|
239
|
-
// the client — the Suspense library owns its own hydration.
|
|
240
|
-
if (existing?.status === 'suspense-resolved') {
|
|
241
|
-
try {
|
|
242
|
-
const value = fn() as T;
|
|
243
|
-
unsuspend.cache.set(cacheKey, { status: 'suspense-cached', value });
|
|
244
|
-
return value;
|
|
245
|
-
} catch {
|
|
246
|
-
return undefined;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Return the cached Suspense value on all subsequent renders.
|
|
251
|
-
if (existing?.status === 'suspense-cached') {
|
|
252
|
-
return existing.value as T;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
228
|
if (!existing) {
|
|
256
229
|
// First encounter — call fn(), which may:
|
|
257
|
-
// (a) return a Promise<T> —
|
|
230
|
+
// (a) return a Promise<T> — async usage (serialised for the client)
|
|
258
231
|
// (b) return T synchronously — e.g. a sync data source
|
|
259
|
-
|
|
260
|
-
let result: Promise<T> | T;
|
|
261
|
-
try {
|
|
262
|
-
result = fn();
|
|
263
|
-
} catch (thrown) {
|
|
264
|
-
// (c) Suspense protocol: fn() threw a thenable. Await it, then mark the
|
|
265
|
-
// entry as 'suspense-resolved' so the next render re-calls fn() to get
|
|
266
|
-
// the synchronously available value. Not stored as 'fulfilled' → not
|
|
267
|
-
// serialised to the client (the Suspense library handles its own hydration).
|
|
268
|
-
if (thrown !== null && typeof thrown === 'object' && typeof (thrown as any).then === 'function') {
|
|
269
|
-
const suspensePromise = Promise.resolve(thrown as Promise<unknown>).then(
|
|
270
|
-
() => { unsuspend.cache.set(cacheKey, { status: 'suspense-resolved' }); },
|
|
271
|
-
() => { unsuspend.cache.set(cacheKey, { status: 'suspense-resolved' }); },
|
|
272
|
-
);
|
|
273
|
-
unsuspend.cache.set(cacheKey, { status: 'pending', promise: suspensePromise });
|
|
274
|
-
throw suspensePromise; // slim-react will await and retry
|
|
275
|
-
}
|
|
276
|
-
throw thrown;
|
|
277
|
-
}
|
|
232
|
+
const result = fn();
|
|
278
233
|
|
|
279
234
|
const isThenable = result !== null && typeof result === 'object' && typeof (result as any).then === 'function';
|
|
280
235
|
|