@barefootjs/hono 0.6.1 → 0.8.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.
@@ -1 +1 @@
1
- {"version":3,"file":"hono-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/hono-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hono-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/hono-adapter.ts"],"names":[],"mappings":"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"}
@@ -100,8 +100,11 @@ export default ${this.componentName}` : "";
100
100
  if (utilImports.length > 0) {
101
101
  lines.push(`import { ${utilImports.join(", ")} } from '@barefootjs/hono/utils'`);
102
102
  }
103
- if (componentCode.includes("<Suspense")) {
104
- lines.push(`import { Suspense } from 'hono/jsx/streaming'`);
103
+ if (componentCode.includes("<__BfSuspense")) {
104
+ lines.push(`import { Suspense as __BfSuspense } from 'hono/jsx/streaming'`);
105
+ }
106
+ if (componentCode.includes("<__BfErrorBoundary")) {
107
+ lines.push(`import { ErrorBoundary as __BfErrorBoundary } from 'hono/jsx'`);
105
108
  }
106
109
  const templateImports = rewriteImportsForTemplate(ir.metadata.templateImports, this.clientShimSource, this.rewriteRelativeImport);
107
110
  for (const imp of templateImports) {
@@ -518,7 +521,7 @@ export default ${this.componentName}` : "";
518
521
  renderAsync(node) {
519
522
  const fallback = this.renderNode(node.fallback);
520
523
  const children = this.renderChildren(node.children);
521
- return `<Suspense fallback={<>${fallback}</>}>${children}</Suspense>`;
524
+ return `<__BfErrorBoundary fallback={<>${fallback}</>}>` + `<__BfSuspense fallback={<>${fallback}</>}>${children}</__BfSuspense>` + `</__BfErrorBoundary>`;
522
525
  }
523
526
  renderComponent(comp, ctx) {
524
527
  const props = this.renderComponentProps(comp);
package/dist/async.d.ts CHANGED
@@ -4,12 +4,21 @@ export interface BfAsyncProps {
4
4
  fallback: Child;
5
5
  /** Async children that will be streamed when resolved. */
6
6
  children: Child;
7
+ /**
8
+ * Optional observer invoked when the body throws synchronously or its
9
+ * async resolution rejects. The `fallback` is rendered either way; this
10
+ * hook only exists so the error isn't swallowed silently (logging,
11
+ * metrics). It does not change what is rendered.
12
+ */
13
+ onError?: (error: Error) => void;
7
14
  }
8
15
  /**
9
16
  * Async streaming boundary component.
10
17
  *
11
18
  * Renders fallback content immediately (sent in the initial HTTP response
12
- * for fast TTFB), then streams the resolved children when ready.
19
+ * for fast TTFB), then streams the resolved children when ready. If the
20
+ * body throws (sync) or rejects (async), the same fallback is rendered via
21
+ * the surrounding `ErrorBoundary`.
13
22
  */
14
23
  export declare function BfAsync(props: BfAsyncProps): import("hono/jsx/jsx-dev-runtime").JSX.Element;
15
24
  //# sourceMappingURL=async.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../src/async.tsx"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAErC,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,EAAE,KAAK,CAAA;IACf,0DAA0D;IAC1D,QAAQ,EAAE,KAAK,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,kDAM1C"}
1
+ {"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../src/async.tsx"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAErC,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,EAAE,KAAK,CAAA;IACf,0DAA0D;IAC1D,QAAQ,EAAE,KAAK,CAAA;IACf;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,kDAQ1C"}
package/dist/async.js CHANGED
@@ -1,10 +1,15 @@
1
1
  // src/async.tsx
2
2
  import { Suspense } from "hono/jsx/streaming";
3
+ import { ErrorBoundary } from "hono/jsx";
3
4
  import { jsxDEV } from "hono/jsx/jsx-dev-runtime";
4
5
  function BfAsync(props) {
5
- return /* @__PURE__ */ jsxDEV(Suspense, {
6
+ return /* @__PURE__ */ jsxDEV(ErrorBoundary, {
6
7
  fallback: props.fallback,
7
- children: props.children
8
+ onError: props.onError,
9
+ children: /* @__PURE__ */ jsxDEV(Suspense, {
10
+ fallback: props.fallback,
11
+ children: props.children
12
+ }, undefined, false, undefined, this)
8
13
  }, undefined, false, undefined, this);
9
14
  }
10
15
  export {
package/dist/build.js CHANGED
@@ -100,8 +100,11 @@ export default ${this.componentName}` : "";
100
100
  if (utilImports.length > 0) {
101
101
  lines.push(`import { ${utilImports.join(", ")} } from '@barefootjs/hono/utils'`);
102
102
  }
103
- if (componentCode.includes("<Suspense")) {
104
- lines.push(`import { Suspense } from 'hono/jsx/streaming'`);
103
+ if (componentCode.includes("<__BfSuspense")) {
104
+ lines.push(`import { Suspense as __BfSuspense } from 'hono/jsx/streaming'`);
105
+ }
106
+ if (componentCode.includes("<__BfErrorBoundary")) {
107
+ lines.push(`import { ErrorBoundary as __BfErrorBoundary } from 'hono/jsx'`);
105
108
  }
106
109
  const templateImports = rewriteImportsForTemplate(ir.metadata.templateImports, this.clientShimSource, this.rewriteRelativeImport);
107
110
  for (const imp of templateImports) {
@@ -518,7 +521,7 @@ export default ${this.componentName}` : "";
518
521
  renderAsync(node) {
519
522
  const fallback = this.renderNode(node.fallback);
520
523
  const children = this.renderChildren(node.children);
521
- return `<Suspense fallback={<>${fallback}</>}>${children}</Suspense>`;
524
+ return `<__BfErrorBoundary fallback={<>${fallback}</>}>` + `<__BfSuspense fallback={<>${fallback}</>}>${children}</__BfSuspense>` + `</__BfErrorBoundary>`;
522
525
  }
523
526
  renderComponent(comp, ctx) {
524
527
  const props = this.renderComponentProps(comp);
package/dist/index.js CHANGED
@@ -100,8 +100,11 @@ export default ${this.componentName}` : "";
100
100
  if (utilImports.length > 0) {
101
101
  lines.push(`import { ${utilImports.join(", ")} } from '@barefootjs/hono/utils'`);
102
102
  }
103
- if (componentCode.includes("<Suspense")) {
104
- lines.push(`import { Suspense } from 'hono/jsx/streaming'`);
103
+ if (componentCode.includes("<__BfSuspense")) {
104
+ lines.push(`import { Suspense as __BfSuspense } from 'hono/jsx/streaming'`);
105
+ }
106
+ if (componentCode.includes("<__BfErrorBoundary")) {
107
+ lines.push(`import { ErrorBoundary as __BfErrorBoundary } from 'hono/jsx'`);
105
108
  }
106
109
  const templateImports = rewriteImportsForTemplate(ir.metadata.templateImports, this.clientShimSource, this.rewriteRelativeImport);
107
110
  for (const imp of templateImports) {
@@ -518,7 +521,7 @@ export default ${this.componentName}` : "";
518
521
  renderAsync(node) {
519
522
  const fallback = this.renderNode(node.fallback);
520
523
  const children = this.renderChildren(node.children);
521
- return `<Suspense fallback={<>${fallback}</>}>${children}</Suspense>`;
524
+ return `<__BfErrorBoundary fallback={<>${fallback}</>}>` + `<__BfSuspense fallback={<>${fallback}</>}>${children}</__BfSuspense>` + `</__BfErrorBoundary>`;
522
525
  }
523
526
  renderComponent(comp, ctx) {
524
527
  const props = this.renderComponentProps(comp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/hono",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Hono integration for BarefootJS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Hono adapter: compiled `<Async>` boundary wraps its body in ErrorBoundary
3
+ * for the body error path (#1375).
4
+ *
5
+ * The compiler lowers `<Async fallback={…}>…</Async>` to an `IRAsync` node;
6
+ * the Hono adapter's `renderAsync` emits it into the generated SSR template.
7
+ * A bare `<Suspense>` mishandles a body that fails at render time: a
8
+ * synchronous throw aborts the stream with empty output, and a Promise
9
+ * rejection during async resolution escapes as an unhandled rejection while
10
+ * the loading fallback is left stranded.
11
+ *
12
+ * The fix wraps the streaming body in Hono's `ErrorBoundary` with the same
13
+ * `fallback`, mirroring the hand-written `BfAsync` runtime component
14
+ * (`../async.tsx`). The runtime behaviour itself is asserted in
15
+ * `async.test.tsx`; this test pins the *emit shape* of the compiled path.
16
+ */
17
+ import { describe, test, expect } from 'bun:test'
18
+ import { compileJSX } from '@barefootjs/jsx'
19
+ import { HonoAdapter } from '../adapter'
20
+
21
+ const adapter = new HonoAdapter()
22
+
23
+ function templateOf(source: string): string {
24
+ const result = compileJSX(source, 'Page.tsx', { adapter })
25
+ expect(result.errors.filter((e) => e.severity === 'error')).toEqual([])
26
+ return result.files.find((f) => f.type === 'markedTemplate')?.content ?? ''
27
+ }
28
+
29
+ describe('compiled <Async> boundary — ErrorBoundary wrapping (#1375)', () => {
30
+ test('emits ErrorBoundary enclosing Suspense, sharing the fallback', () => {
31
+ const template = templateOf(`
32
+ export function Page() {
33
+ return (
34
+ <div>
35
+ <Async fallback={<p>Loading...</p>}>
36
+ <span>Resolved</span>
37
+ </Async>
38
+ </div>
39
+ )
40
+ }
41
+ `)
42
+
43
+ // Tags are emitted under `__Bf`-prefixed aliases so they can't collide
44
+ // with a user component named `ErrorBoundary` / `Suspense` (#1375).
45
+ expect(template).toContain('<__BfErrorBoundary fallback={<>')
46
+ expect(template).toContain('<__BfSuspense fallback={<>')
47
+ // ErrorBoundary must enclose Suspense (catch errors raised while the
48
+ // Suspense body resolves), not the reverse.
49
+ expect(template.indexOf('<__BfErrorBoundary')).toBeLessThan(template.indexOf('<__BfSuspense'))
50
+ expect(template).toContain('</__BfSuspense></__BfErrorBoundary>')
51
+ })
52
+
53
+ test('injects the aliased ErrorBoundary import alongside Suspense', () => {
54
+ const template = templateOf(`
55
+ export function Page() {
56
+ return (
57
+ <Async fallback={<p>Loading...</p>}>
58
+ <span>Resolved</span>
59
+ </Async>
60
+ )
61
+ }
62
+ `)
63
+
64
+ expect(template).toContain(`import { Suspense as __BfSuspense } from 'hono/jsx/streaming'`)
65
+ expect(template).toContain(`import { ErrorBoundary as __BfErrorBoundary } from 'hono/jsx'`)
66
+ })
67
+
68
+ test('aliasing avoids a duplicate import when the body uses a user component named ErrorBoundary', () => {
69
+ // Regression for the bare-name collision: a user component literally
70
+ // named `ErrorBoundary` used inside the boundary must not cause the
71
+ // injected Hono import to duplicate the user's own `ErrorBoundary`
72
+ // binding (which would be a "Duplicate identifier" build error).
73
+ const template = templateOf(`
74
+ import { ErrorBoundary } from './my-error-boundary'
75
+ export function Page() {
76
+ return (
77
+ <Async fallback={<ErrorBoundary><p>fail</p></ErrorBoundary>}>
78
+ <span>Resolved</span>
79
+ </Async>
80
+ )
81
+ }
82
+ `)
83
+
84
+ // Exactly one `ErrorBoundary` binding survives — the user's. The Hono
85
+ // wrapper imports under the `__BfErrorBoundary` alias, so no collision.
86
+ const bareImports = template.match(/import \{ ErrorBoundary \} from/g) ?? []
87
+ expect(bareImports.length).toBe(1)
88
+ expect(template).toContain(`import { ErrorBoundary } from './my-error-boundary'`)
89
+ expect(template).toContain(`import { ErrorBoundary as __BfErrorBoundary } from 'hono/jsx'`)
90
+ })
91
+
92
+ test('each of multiple boundaries gets its own ErrorBoundary wrapper', () => {
93
+ const template = templateOf(`
94
+ export function Dashboard() {
95
+ return (
96
+ <div>
97
+ <Async fallback={<p>A...</p>}>
98
+ <span>A</span>
99
+ </Async>
100
+ <Async fallback={<p>B...</p>}>
101
+ <span>B</span>
102
+ </Async>
103
+ </div>
104
+ )
105
+ }
106
+ `)
107
+
108
+ const wrappers = template.match(/<__BfErrorBoundary fallback=\{<>/g) ?? []
109
+ expect(wrappers.length).toBe(2)
110
+ })
111
+ })
@@ -33,13 +33,15 @@ describe('BfAsync', () => {
33
33
  </BfAsync>
34
34
  )
35
35
 
36
- const chunks = await collectStream(stream)
36
+ const fullOutput = (await collectStream(stream)).join('')
37
37
 
38
- // First chunk has fallback
39
- expect(chunks[0]).toContain('Loading...')
40
- // Later chunk has resolved content
41
- const fullOutput = chunks.join('')
38
+ // Fallback streams first, resolved content swaps in afterwards. The
39
+ // body is wrapped in an ErrorBoundary (#1375) whose template/swap
40
+ // markers now lead the stream, so assert ordering across the full
41
+ // output rather than pinning the fallback to a literal `chunks[0]`.
42
+ expect(fullOutput).toContain('Loading...')
42
43
  expect(fullOutput).toContain('Loaded!')
44
+ expect(fullOutput.indexOf('Loading...')).toBeLessThan(fullOutput.indexOf('Loaded!'))
43
45
  })
44
46
 
45
47
  it('renders multiple async boundaries independently', async () => {
@@ -89,7 +91,7 @@ describe('BfAsync', () => {
89
91
  expect(fullOutput).toContain('bf-p=')
90
92
  })
91
93
 
92
- it('renders synchronous children without streaming', async () => {
94
+ it('renders synchronous children without a fallback flash', async () => {
93
95
  const SyncContent = () => <div>Already here</div>
94
96
 
95
97
  const stream = renderToReadableStream(
@@ -98,9 +100,126 @@ describe('BfAsync', () => {
98
100
  </BfAsync>
99
101
  )
100
102
 
101
- const chunks = await collectStream(stream)
103
+ const fullOutput = (await collectStream(stream)).join('')
104
+
105
+ // Synchronous content needs no fallback round-trip: it resolves in the
106
+ // initial flush and the loading fallback never appears. The
107
+ // ErrorBoundary wrapper (#1375) routes the content through a
108
+ // template/swap chunk, so it is no longer literally `chunks[0]`, but
109
+ // there is still no `Loading...` flash.
110
+ expect(fullOutput).toContain('Already here')
111
+ expect(fullOutput).not.toContain('Loading...')
112
+ })
113
+ })
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Error paths (#1375): a body that throws — synchronously or asynchronously.
117
+ //
118
+ // A bare `<Suspense>` mishandles both: a synchronous throw aborts the stream
119
+ // with empty output (no fallback), and a rejection during async resolution
120
+ // escapes as an unhandled rejection while the loading fallback is stranded.
121
+ // `BfAsync` wraps the body in Hono's `ErrorBoundary` so the same `fallback`
122
+ // is rendered on either failure and the error never leaks.
123
+ //
124
+ // These are the two cases the Layer 1 / streaming layer can observe directly
125
+ // (a single SSR render). The remaining catalog cases — reset-signal re-mount
126
+ // and throw-during-cleanup — are client-lifecycle behaviours covered by the
127
+ // Layer 6 fixme stubs in `site/ui/e2e/stress-1244.spec.ts`.
128
+ // ---------------------------------------------------------------------------
129
+
130
+ describe('BfAsync — error paths (#1375)', () => {
131
+ // A render that errors must not surface a stray unhandled rejection on the
132
+ // process: install a guard for the duration of each test and assert it
133
+ // stayed quiet. `BfAsync`'s ErrorBoundary is what keeps it quiet.
134
+ function trackUnhandledRejections(): { count: () => number; restore: () => void } {
135
+ let n = 0
136
+ const onRejection = () => {
137
+ n++
138
+ }
139
+ process.on('unhandledRejection', onRejection)
140
+ return {
141
+ count: () => n,
142
+ restore: () => process.off('unhandledRejection', onRejection),
143
+ }
144
+ }
145
+
146
+ it('synchronous throw in the body renders the fallback (not empty output)', async () => {
147
+ const Boom = (): HtmlEscapedString => {
148
+ throw new Error('sync-boom')
149
+ }
150
+
151
+ const guard = trackUnhandledRejections()
152
+ let onErrorMessage: string | undefined
153
+ try {
154
+ const stream = renderToReadableStream(
155
+ <BfAsync
156
+ fallback={<p>Fallback shown</p>}
157
+ onError={(e) => {
158
+ onErrorMessage = e.message
159
+ }}
160
+ >
161
+ <Boom />
162
+ </BfAsync>
163
+ )
164
+ const fullOutput = (await collectStream(stream)).join('')
165
+
166
+ // The fallback is rendered (a bare <Suspense> would yield empty output).
167
+ expect(fullOutput).toContain('Fallback shown')
168
+ // onError observed the failure so it isn't swallowed silently.
169
+ expect(onErrorMessage).toBe('sync-boom')
170
+ } finally {
171
+ guard.restore()
172
+ }
173
+ })
174
+
175
+ it('async rejection during resolution renders the fallback and leaks no rejection', async () => {
176
+ const Rejects = (): Promise<HtmlEscapedString> =>
177
+ new Promise((_resolve, reject) =>
178
+ setTimeout(() => reject(new Error('async-boom')), 10)
179
+ )
180
+
181
+ const guard = trackUnhandledRejections()
182
+ let onErrorMessage: string | undefined
183
+ try {
184
+ const stream = renderToReadableStream(
185
+ <BfAsync
186
+ fallback={<p>Fallback shown</p>}
187
+ onError={(e) => {
188
+ onErrorMessage = e.message
189
+ }}
190
+ >
191
+ <Rejects />
192
+ </BfAsync>
193
+ )
194
+ const fullOutput = (await collectStream(stream)).join('')
195
+
196
+ // Fallback is the final rendered state; the rejected content never
197
+ // mutates into the DOM late.
198
+ expect(fullOutput).toContain('Fallback shown')
199
+ expect(onErrorMessage).toBe('async-boom')
200
+
201
+ // Give any stray rejection a tick to surface, then assert none did.
202
+ await new Promise((r) => setTimeout(r, 0))
203
+ expect(guard.count()).toBe(0)
204
+ } finally {
205
+ guard.restore()
206
+ }
207
+ })
208
+
209
+ it('the happy path still streams resolved content past the error boundary', async () => {
210
+ // Wrapping the body in ErrorBoundary must not regress normal streaming.
211
+ const AsyncContent = () =>
212
+ new Promise<HtmlEscapedString>((resolve) =>
213
+ setTimeout(() => resolve(<div>Loaded past boundary</div>), 10)
214
+ )
215
+
216
+ const stream = renderToReadableStream(
217
+ <BfAsync fallback={<p>Loading...</p>}>
218
+ <AsyncContent />
219
+ </BfAsync>
220
+ )
102
221
 
103
- // Synchronous content should be in the first chunk (no fallback needed)
104
- expect(chunks[0]).toContain('Already here')
222
+ const fullOutput = (await collectStream(stream)).join('')
223
+ expect(fullOutput).toContain('Loaded past boundary')
105
224
  })
106
225
  })
@@ -229,9 +229,16 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
229
229
  lines.push(`import { ${utilImports.join(', ')} } from '@barefootjs/hono/utils'`)
230
230
  }
231
231
 
232
- // Import Suspense when async boundaries are used
233
- if (componentCode.includes('<Suspense')) {
234
- lines.push(`import { Suspense } from 'hono/jsx/streaming'`)
232
+ // Import Suspense / ErrorBoundary when async boundaries are used. Both
233
+ // are imported under `__Bf`-prefixed aliases (and emitted as such by
234
+ // `renderAsync`) so the generated tags can never collide with a user
235
+ // component literally named `Suspense` or `ErrorBoundary` — a bare-name
236
+ // import would otherwise duplicate the user's own import binding (#1375).
237
+ if (componentCode.includes('<__BfSuspense')) {
238
+ lines.push(`import { Suspense as __BfSuspense } from 'hono/jsx/streaming'`)
239
+ }
240
+ if (componentCode.includes('<__BfErrorBoundary')) {
241
+ lines.push(`import { ErrorBoundary as __BfErrorBoundary } from 'hono/jsx'`)
235
242
  }
236
243
 
237
244
  // Re-emit template imports, rewriting `@barefootjs/client` to this
@@ -962,7 +969,19 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
962
969
  renderAsync(node: IRAsync): string {
963
970
  const fallback = this.renderNode(node.fallback)
964
971
  const children = this.renderChildren(node.children)
965
- return `<Suspense fallback={<>${fallback}</>}>${children}</Suspense>`
972
+ // Wrap the streaming body in an ErrorBoundary so a synchronous throw or
973
+ // a rejected Promise during async resolution falls back to the same
974
+ // `fallback` instead of aborting the stream / leaking an unhandled
975
+ // rejection. Mirrors the runtime `BfAsync` component (#1375).
976
+ //
977
+ // Both tags use `__Bf`-prefixed aliases (see the import injection above)
978
+ // so they can't collide with a user component named `Suspense` /
979
+ // `ErrorBoundary` in the same module.
980
+ return (
981
+ `<__BfErrorBoundary fallback={<>${fallback}</>}>` +
982
+ `<__BfSuspense fallback={<>${fallback}</>}>${children}</__BfSuspense>` +
983
+ `</__BfErrorBoundary>`
984
+ )
966
985
  }
967
986
 
968
987
  renderComponent(comp: IRComponent, ctx?: { isRootOfClientComponent?: boolean; isInsideLoop?: boolean; isLoopItemRoot?: boolean }): string {
package/src/async.tsx CHANGED
@@ -14,6 +14,16 @@
14
14
  * Suspense/streaming. The BarefootJS hydration runtime automatically picks
15
15
  * up components rendered inside async boundaries via requestAnimationFrame.
16
16
  *
17
+ * The body is additionally wrapped in Hono's `ErrorBoundary` so that a
18
+ * failure during render — a synchronous `throw` in a child, or a rejected
19
+ * Promise during async resolution — surfaces the same `fallback` instead
20
+ * of producing empty output (sync case) or leaking an unhandled rejection
21
+ * (async case). Without the boundary a bare `<Suspense>` does neither:
22
+ * a synchronous throw aborts the stream with no fallback, and a rejection
23
+ * during streaming escapes as an unhandled rejection while the loading
24
+ * fallback is left stranded. See `__tests__/async.test.tsx` error-path
25
+ * cases (#1375).
26
+ *
17
27
  * Usage:
18
28
  * ```tsx
19
29
  * import { BfAsync } from '@barefootjs/hono/async'
@@ -31,6 +41,7 @@
31
41
  */
32
42
 
33
43
  import { Suspense } from 'hono/jsx/streaming'
44
+ import { ErrorBoundary } from 'hono/jsx'
34
45
  import type { Child } from 'hono/jsx'
35
46
 
36
47
  export interface BfAsyncProps {
@@ -38,18 +49,29 @@ export interface BfAsyncProps {
38
49
  fallback: Child
39
50
  /** Async children that will be streamed when resolved. */
40
51
  children: Child
52
+ /**
53
+ * Optional observer invoked when the body throws synchronously or its
54
+ * async resolution rejects. The `fallback` is rendered either way; this
55
+ * hook only exists so the error isn't swallowed silently (logging,
56
+ * metrics). It does not change what is rendered.
57
+ */
58
+ onError?: (error: Error) => void
41
59
  }
42
60
 
43
61
  /**
44
62
  * Async streaming boundary component.
45
63
  *
46
64
  * Renders fallback content immediately (sent in the initial HTTP response
47
- * for fast TTFB), then streams the resolved children when ready.
65
+ * for fast TTFB), then streams the resolved children when ready. If the
66
+ * body throws (sync) or rejects (async), the same fallback is rendered via
67
+ * the surrounding `ErrorBoundary`.
48
68
  */
49
69
  export function BfAsync(props: BfAsyncProps) {
50
70
  return (
51
- <Suspense fallback={props.fallback}>
52
- {props.children}
53
- </Suspense>
71
+ <ErrorBoundary fallback={props.fallback} onError={props.onError}>
72
+ <Suspense fallback={props.fallback}>
73
+ {props.children}
74
+ </Suspense>
75
+ </ErrorBoundary>
54
76
  )
55
77
  }