@abide/abide 0.110.3 → 0.111.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 (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/docs/REGISTRY.md +22 -21
  3. package/docs/RULEBOOK.md +85 -19
  4. package/docs/abide.md +38 -24
  5. package/package.json +1 -1
  6. package/src/compiler/compileApp.ts +2 -1
  7. package/src/server/BRIDGE_VERSION.ts +1 -1
  8. package/src/server/HEARTBEAT_MILLISECONDS.ts +2 -0
  9. package/src/server/IDLE_TIMEOUT_SECONDS.ts +6 -0
  10. package/src/server/answerRequest.ts +44 -3
  11. package/src/server/answerRpc.ts +14 -41
  12. package/src/server/answerSocket.ts +2 -2
  13. package/src/server/answerValue.ts +15 -3
  14. package/src/server/answerWorker.ts +195 -0
  15. package/src/server/appManifest.ts +48 -0
  16. package/src/server/assistant.ts +1 -1
  17. package/src/server/bridgeRelay.ts +4 -50
  18. package/src/server/claudeApi.ts +1 -1
  19. package/src/server/claudeCode.ts +3 -3
  20. package/src/server/clientBundle.ts +7 -0
  21. package/src/server/config.ts +14 -0
  22. package/src/server/createApp.ts +28 -4
  23. package/src/server/devWorker.ts +4 -1
  24. package/src/server/generatedSurfaces.ts +4 -1
  25. package/src/server/requestScope.ts +31 -11
  26. package/src/server/responseRelay.ts +7 -7
  27. package/src/server/serveBuild.ts +2 -0
  28. package/src/server/serveDev.ts +4 -1
  29. package/src/server/streamEvent.ts +60 -1
  30. package/src/shared/ADDRESSES.ts +11 -0
  31. package/src/shared/LONGEST_DELAY.ts +2 -0
  32. package/src/shared/OFFLINE_CACHES.ts +12 -0
  33. package/src/shared/OFFLINE_META.ts +7 -0
  34. package/src/shared/answerOf.ts +29 -4
  35. package/src/shared/attributeText.ts +1 -1
  36. package/src/shared/reactive/Expiry.ts +13 -1
  37. package/src/shared/reactive/Reactive.ts +13 -3
  38. package/src/shared/safeUrl.ts +8 -1
  39. package/src/ui/Router.ts +25 -0
  40. package/src/ui/registerWorker.ts +34 -0
  41. package/src/ui/routing.ts +1 -0
  42. package/src/ui/socketWire.ts +33 -2
package/docs/abide.md CHANGED
@@ -264,7 +264,7 @@ Read on: [Open it to machines](/guide/open-it-to-machines.md)
264
264
 
265
265
  ## 10. Test, measure and ship
266
266
 
267
- The CRM gets a reports page, and each report gets a test: what a click rewrote, what a change costs as the pipeline grows, and where a slow click spends its time. Then the CRM ships, as a server, a single binary or a desktop app.
267
+ The CRM gets a reports page, and each report gets a test: what a click rewrote, what a change costs as the pipeline grows, and where a slow click spends its time. Then the CRM ships, as a server, a single binary or a desktop app, and keeps its pages readable offline.
268
268
 
269
269
  Read on: [Test, measure, ship](/guide/test-measure-and-ship.md)
270
270
 
@@ -4228,7 +4228,7 @@ test('a report that stops producing is refused 504', async () => {
4228
4228
 
4229
4229
  On a handler that yields, the bound is per row. So one number lets a long report run and still catches a source that has stopped. A call past it is refused `504`, and the rows already sent stay on the page.
4230
4230
 
4231
- `timeout` also raises the server's idle timeout for that request, so a slow handler does not have its connection closed under it. Its sibling is `maxBodySize`: the bytes a mutation's body may carry, refused `413` before a declared oversize body is read.
4231
+ Without one, nothing is cut short: the server never closes a connection under a call it is still answering, a slow handler or a quiet stream alike. Its sibling is `maxBodySize`: the bytes a mutation's body may carry, refused `413` before a declared oversize body is read.
4232
4232
 
4233
4233
  | Setting | Default | What it bounds |
4234
4234
  | --- | --- | --- |
@@ -5680,7 +5680,7 @@ Without `--url`, the CLI reads `ABIDE_APP_URL`, then falls back to the local app
5680
5680
 
5681
5681
  *Test the CRM, measure the work it does, and ship it.*
5682
5682
 
5683
- The CRM gets a reports page, and each report gets a test: what a click rewrote, what a change costs as the pipeline grows, and where a slow click spends its time. Then the CRM ships, as a server, a single binary or a desktop app.
5683
+ The CRM gets a reports page, and each report gets a test: what a click rewrote, what a change costs as the pipeline grows, and where a slow click spends its time. Then the CRM ships, as a server, a single binary or a desktop app, and keeps its pages readable offline.
5684
5684
 
5685
5685
  Each card is a file of the reports page, beside the test or the bench that proves it. The commands that ship the CRM close the chapter.
5686
5686
 
@@ -6407,6 +6407,19 @@ watch(() => printReport.on(() => print()))
6407
6407
 
6408
6408
  The reports page adds a Reports menu while it is showing. `onMenu` is read the way markup is, so the menus follow the page's state. Each entry is a `MenuItem`: `'separator'`, an item with an `href`, one with a `MenuRole` the platform performs, or a `MenuEntry` made by `menuItem`. `MenuEntry.on` files what runs when it is picked, and hands back what stops it, which is what `watch` takes. In a browser nothing picks an item, and the page runs unchanged.
6409
6409
 
6410
+ ## Keep the CRM readable offline
6411
+
6412
+ ```
6413
+ abide start
6414
+ ABIDE_OFFLINE=0 abide start
6415
+ ```
6416
+
6417
+ Each page the CRM opens is kept on the device, with the answers its handlers gave, the public files it loaded and the bundle it runs, until `ABIDE_OFFLINE=0` turns that off where the CRM is served. With the network gone, a page opened before loads again with its data and works. One never opened says the device is offline, and a link to it leaves the page you are on standing.
6418
+
6419
+ With `src/public/icon.png` beside it, each page also links a manifest, so a phone can add the CRM to its home screen, which is what keeps what was saved there for good.
6420
+
6421
+ A kept page shows what it showed when it was last opened, live updates stop, and a change fails until the network is back. A sign-in, a sign-out or a submitted form clears what was kept, and a new build replaces it. `abide dev` keeps nothing.
6422
+
6410
6423
  ## Next
6411
6424
 
6412
6425
  That is the CRM: contacts, deals, a live feed, pages, access, operations, machines, and now reports, tested and shipped. Start again from [Start an app](/guide/start-an-app.md) with an app of your own.
@@ -6709,7 +6722,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
6709
6722
 
6710
6723
  | Name | Signature | Meaning | Rules |
6711
6724
  | --- | --- | --- | --- |
6712
- | `s.invalidate` | `() => void` | Drop the cache behind the value. | [7.1](/rulebook#7.1), [7.2](/rulebook#7.2), [7.3](/rulebook#7.3), [7.6](/rulebook#7.6), [7.7](/rulebook#7.7), [7.9](/rulebook#7.9), [7.10](/rulebook#7.10), [7.12](/rulebook#7.12), [7.13](/rulebook#7.13), [7.14](/rulebook#7.14), [7.15](/rulebook#7.15), [7.16](/rulebook#7.16), [7.17](/rulebook#7.17), [11.51](/rulebook#11.51) |
6725
+ | `s.invalidate` | `() => void` | Drop the cache behind the value. | [7.1](/rulebook#7.1), [7.2](/rulebook#7.2), [7.3](/rulebook#7.3), [7.6](/rulebook#7.6), [7.7](/rulebook#7.7), [7.9](/rulebook#7.9), [7.10](/rulebook#7.10), [7.12](/rulebook#7.12), [7.13](/rulebook#7.13), [7.14](/rulebook#7.14), [7.15](/rulebook#7.15), [7.16](/rulebook#7.16), [7.17](/rulebook#7.17), [7.19](/rulebook#7.19), [11.51](/rulebook#11.51) |
6713
6726
  | `s.refresh` | `() => void` | Reload while continuing to serve what is held. | [4.12](/rulebook#4.12), [7.1](/rulebook#7.1), [7.4](/rulebook#7.4), [7.5](/rulebook#7.5), [7.6](/rulebook#7.6), [7.11](/rulebook#7.11), [7.18](/rulebook#7.18), [11.51](/rulebook#11.51), [5.24](/rulebook#5.24) |
6714
6727
 
6715
6728
  ## Effects
@@ -7206,7 +7219,7 @@ Reactive primitives: what is written in place rather than imported.
7206
7219
 
7207
7220
  | Name | Signature | Meaning | Rules |
7208
7221
  | --- | --- | --- | --- |
7209
- | `Rpc` | `<Value, Args, Failures, Produced = Value>(args?: Args, options?: { signal?: AbortSignal }) => Reactive<Value, Value, Failures \| Failed<'HttpError', undefined>, Produced>` | A handler reached over http, or in process on a server. | [16.3](/rulebook#16.3), [16.4](/rulebook#16.4), [16.8](/rulebook#16.8), [16.11](/rulebook#16.11), [16.25](/rulebook#16.25), [16.26](/rulebook#16.26), [16.27](/rulebook#16.27), [16.28](/rulebook#16.28), [16.29](/rulebook#16.29), [16.33](/rulebook#16.33), [16.43](/rulebook#16.43), [16.48](/rulebook#16.48), [16.49](/rulebook#16.49), [16.62](/rulebook#16.62), [41.5](/rulebook#41.5), [41.7](/rulebook#41.7), [41.12](/rulebook#41.12), [41.21](/rulebook#41.21), [41.22](/rulebook#41.22), [41.25](/rulebook#41.25), [41.26](/rulebook#41.26), [41.27](/rulebook#41.27) |
7222
+ | `Rpc` | `<Value, Args, Failures, Produced = Value>(args?: Args, options?: { signal?: AbortSignal }) => Reactive<Value, Value, Failures \| Failed<'HttpError', undefined>, Produced>` | A handler reached over http, or in process on a server. | [16.3](/rulebook#16.3), [16.4](/rulebook#16.4), [16.8](/rulebook#16.8), [16.11](/rulebook#16.11), [16.25](/rulebook#16.25), [16.26](/rulebook#16.26), [16.27](/rulebook#16.27), [16.28](/rulebook#16.28), [16.29](/rulebook#16.29), [16.33](/rulebook#16.33), [16.43](/rulebook#16.43), [16.48](/rulebook#16.48), [16.49](/rulebook#16.49), [16.62](/rulebook#16.62), [41.5](/rulebook#41.5), [41.7](/rulebook#41.7), [41.12](/rulebook#41.12), [41.21](/rulebook#41.21), [41.22](/rulebook#41.22), [41.25](/rulebook#41.25), [41.26](/rulebook#41.26), [41.27](/rulebook#41.27), [16.79](/rulebook#16.79) |
7210
7223
  | `Value` | `unknown` | What the addressed `Reactive` holds. | [16.3](/rulebook#16.3), [16.30](/rulebook#16.30) |
7211
7224
  | `Produced` | `unknown` | What it yields, per the unit rule. | [1.2](/rulebook#1.2), [16.3](/rulebook#16.3) |
7212
7225
  | `Args` | `Record<string, JsonValue> \| undefined` | The arguments, in the body on a mutation and in the URL on a `GET` or `DELETE`. | [11.24](/rulebook#11.24), [16.18](/rulebook#16.18), [19.15](/rulebook#19.15) |
@@ -7314,7 +7327,7 @@ Reactive primitives: what is written in place rather than imported.
7314
7327
  | --- | --- | --- | --- |
7315
7328
  | `description` | `string` | The human description carried onto every generated surface. | [16.28](/rulebook#16.28) |
7316
7329
  | `middleware` | `Middleware<{ request: Request; args: () => Args }, Value \| Failures>[]` | The rungs run per call. | [16.11](/rulebook#16.11), [16.12](/rulebook#16.12), [16.16](/rulebook#16.16), [16.17](/rulebook#16.17), [16.18](/rulebook#16.18), [16.19](/rulebook#16.19), [16.20](/rulebook#16.20) |
7317
- | `timeout` | `number` | How long the call may stay open, and the floor under the request's idle timeout. | [16.22](/rulebook#16.22), [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [16.57](/rulebook#16.57), [16.58](/rulebook#16.58), [16.59](/rulebook#16.59), [16.60](/rulebook#16.60), [16.61](/rulebook#16.61), [16.77](/rulebook#16.77) |
7330
+ | `timeout` | `number` | How long the call may go without progress. | [16.22](/rulebook#16.22), [16.58](/rulebook#16.58), [16.59](/rulebook#16.59), [16.60](/rulebook#16.60), [16.61](/rulebook#16.61), [16.77](/rulebook#16.77) |
7318
7331
  | `clients` | `Clients` | Which surfaces carry this handler. | [16.37](/rulebook#16.37), [20.3](/rulebook#20.3) |
7319
7332
  | `crossOrigin` | `boolean \| string[]` | Whether other origins may call it, or follow it over the mux. | [16.24](/rulebook#16.24), [18.2](/rulebook#18.2), [21.19](/rulebook#21.19) |
7320
7333
  | `maxBodySize` | `number` | The largest request body a mutation will accept, in bytes. | [16.13](/rulebook#16.13), [16.54](/rulebook#16.54), [16.55](/rulebook#16.55) |
@@ -7427,7 +7440,7 @@ Reactive primitives: what is written in place rather than imported.
7427
7440
 
7428
7441
  | Name | Signature | Meaning | Rules |
7429
7442
  | --- | --- | --- | --- |
7430
- | `sse` | `<T>(values: Values<T>) => Values<T>` | The same machine framed as server-sent events, fixed. | [16.45](/rulebook#16.45), [16.75](/rulebook#16.75), [17.6](/rulebook#17.6), [17.7](/rulebook#17.7) |
7443
+ | `sse` | `<T>(values: Values<T>) => Values<T>` | The same machine framed as server-sent events, fixed. | [16.45](/rulebook#16.45), [16.57](/rulebook#16.57), [16.75](/rulebook#16.75), [17.6](/rulebook#17.6), [17.7](/rulebook#17.7) |
7431
7444
 
7432
7445
  ## Taught in
7433
7446
 
@@ -7504,7 +7517,7 @@ Transports: what is written in place rather than imported.
7504
7517
  | `access-control-allow-origin` | an rpc answer to another origin | The origin a handler admitted. | [21.19](/rulebook#21.19), [16.24](/rulebook#16.24) |
7505
7518
  | `allow: GET, HEAD` | a page, a public file or the bundle, asked by another method | The methods the address answers. | [15.17](/rulebook#15.17) |
7506
7519
  | `server-timing` | an rpc answer; `Middleware` and a `timing` span on any answer | The handler's duration, named `Direct`, `Seeded` from the render's run, `Cached` from a held `global` production, or `Started` to a stream's first chunk; the rungs' time; and each `timing` span. | [21.15](/rulebook#21.15), [21.20](/rulebook#21.20), [21.21](/rulebook#21.21), [24.6](/rulebook#24.6) |
7507
- | `etag` | a `GET` answered from a `global` value | The version of the production answered. | [18.5](/rulebook#18.5), [18.6](/rulebook#18.6), [18.7](/rulebook#18.7), [18.9](/rulebook#18.9), [18.19](/rulebook#18.19), [18.20](/rulebook#18.20), [18.22](/rulebook#18.22), [18.23](/rulebook#18.23) |
7520
+ | `etag` | a `GET` answered from a `global` value | The version of the production answered. | [18.5](/rulebook#18.5), [18.6](/rulebook#18.6), [18.7](/rulebook#18.7), [18.9](/rulebook#18.9), [18.19](/rulebook#18.19), [18.20](/rulebook#18.20), [18.22](/rulebook#18.22), [18.23](/rulebook#18.23), [16.57](/rulebook#16.57) |
7508
7521
 
7509
7522
  ## Taught in
7510
7523
 
@@ -7715,7 +7728,7 @@ Generated surfaces: what is written in place rather than imported.
7715
7728
 
7716
7729
  | Name | Signature | Meaning | Rules |
7717
7730
  | --- | --- | --- | --- |
7718
- | `AssistantEvent` | `{ type: 'text'; text: string } \| { type: 'tool'; id: string; name: string } \| { type: 'input'; id: string; input: unknown } \| { type: 'result'; id: string; text: string; failed: boolean } \| { type: 'done'; stop: string; usage: { input: number; output: number } }` | What a conversation produces: text, a tool call as it starts, its input once written, its result, and the end. | [44.2](/rulebook#44.2), [44.11](/rulebook#44.11), [44.29](/rulebook#44.29) |
7731
+ | `AssistantEvent` | `{ type: 'text'; text: string } \| { type: 'tool'; id: string; name: string } \| { type: 'input'; id: string; input: unknown } \| { type: 'result'; id: string; text: string; failed: boolean } \| { type: 'done'; stop: string; usage: { input: number; output: number } }` | What a conversation produces: text, a tool call as it starts, its input once written, its result, and the end. | [44.2](/rulebook#44.2), [44.11](/rulebook#44.11), [44.28](/rulebook#44.28) |
7719
7732
 
7720
7733
  ## Taught in
7721
7734
 
@@ -7751,7 +7764,7 @@ Generated surfaces: what is written in place rather than imported.
7751
7764
 
7752
7765
  | Name | Signature | Meaning | Rules |
7753
7766
  | --- | --- | --- | --- |
7754
- | `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | [44.2](/rulebook#44.2), [44.29](/rulebook#44.29) |
7767
+ | `Provider` | `{ run(messages: AssistantMessage[], tools: Toolbox, limits: { maxTurns: number; signal: AbortSignal }): AsyncGenerator<AssistantEvent> }` | What runs a conversation. | [44.2](/rulebook#44.2), [44.28](/rulebook#44.28) |
7755
7768
 
7756
7769
  ## Taught in
7757
7770
 
@@ -7841,10 +7854,10 @@ Generated surfaces: what is written in place rather than imported.
7841
7854
 
7842
7855
  | Name | Signature | Meaning | Rules |
7843
7856
  | --- | --- | --- | --- |
7844
- | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.28](/rulebook#44.28), [44.31](/rulebook#44.31) |
7845
- | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs a bridge with this browser, granting the built-in tools it is given. | [44.14](/rulebook#44.14), [44.33](/rulebook#44.33) |
7846
- | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22), [44.33](/rulebook#44.33) |
7847
- | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | [36.7](/rulebook#36.7), [44.14](/rulebook#44.14), [44.15](/rulebook#44.15), [44.19](/rulebook#44.19), [44.23](/rulebook#44.23), [44.26](/rulebook#44.26), [44.27](/rulebook#44.27), [16.78](/rulebook#16.78) |
7857
+ | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.27](/rulebook#44.27), [44.30](/rulebook#44.30) |
7858
+ | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs a bridge with this browser, granting the built-in tools it is given. | [44.14](/rulebook#44.14), [44.32](/rulebook#44.32) |
7859
+ | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22), [44.32](/rulebook#44.32) |
7860
+ | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | [36.7](/rulebook#36.7), [44.14](/rulebook#44.14), [44.15](/rulebook#44.15), [44.19](/rulebook#44.19), [44.23](/rulebook#44.23), [44.26](/rulebook#44.26), [16.57](/rulebook#16.57), [16.78](/rulebook#16.78) |
7848
7861
 
7849
7862
  ## Taught in
7850
7863
 
@@ -7862,7 +7875,7 @@ Generated surfaces: what is written in place rather than imported.
7862
7875
 
7863
7876
  | Name | Signature | Meaning | Rules |
7864
7877
  | --- | --- | --- | --- |
7865
- | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | [44.16](/rulebook#44.16), [44.30](/rulebook#44.30) |
7878
+ | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | [44.16](/rulebook#44.16), [44.29](/rulebook#44.29) |
7866
7879
 
7867
7880
  ## Taught in
7868
7881
 
@@ -7991,7 +8004,7 @@ Generated surfaces: what is written in place rather than imported.
7991
8004
 
7992
8005
  | Name | Signature | Meaning | Rules |
7993
8006
  | --- | --- | --- | --- |
7994
- | `online` | `Reactive<boolean>` | Whether the caller can reach the app. | [23.7](/rulebook#23.7), [23.8](/rulebook#23.8) |
8007
+ | `online` | `Reactive<boolean>` | Whether the caller can reach the app. | [23.7](/rulebook#23.7), [23.8](/rulebook#23.8), [18.33](/rulebook#18.33), [18.34](/rulebook#18.34) |
7995
8008
 
7996
8009
  ## Taught in
7997
8010
 
@@ -8300,13 +8313,13 @@ Generated surfaces: what is written in place rather than imported.
8300
8313
  | `abide:principal` | `debug` `warning` | A principal set or cleared. | [28.8](/rulebook#28.8) |
8301
8314
  | `abide:health` | `warning` | An `onHealth` that threw or answered a non-object. | [25.2](/rulebook#25.2), [28.8](/rulebook#28.8) |
8302
8315
  | `abide:mcp` | `debug` `warning` | A handler withheld from MCP, a tool call answered, or a tool published with no description. | [20.3](/rulebook#20.3), [20.9](/rulebook#20.9), [20.18](/rulebook#20.18), [28.8](/rulebook#28.8) |
8303
- | `abide:assistant` | `debug` | One line per conversation, once it ends. | [44.32](/rulebook#44.32) |
8316
+ | `abide:assistant` | `debug` | One line per conversation, once it ends. | [44.31](/rulebook#44.31) |
8304
8317
  | `abide:openapi` | `warning` | A handler whose schema had no JSON Schema export. | [20.11](/rulebook#20.11), [28.8](/rulebook#28.8) |
8305
8318
  | `abide:render` | `warning` | An `error.abide` that itself failed. | [28.8](/rulebook#28.8) |
8306
8319
  | `abide:refuse` | `warning` | A message formatter that threw. | [15.13](/rulebook#15.13), [28.8](/rulebook#28.8) |
8307
8320
  | `abide:reactive` | `warning` | A failed revalidation over a value still being served. | [4.12](/rulebook#4.12), [28.8](/rulebook#28.8) |
8308
8321
  | `abide:watch` | `warning` | A `watch` effect or `Disposer` that threw. | [12.6](/rulebook#12.6), [28.8](/rulebook#28.8) |
8309
- | `abide:hydrate` / `abide:navigate` | `warning` | The browser lane. | [28.8](/rulebook#28.8), [41.10](/rulebook#41.10), [41.15](/rulebook#41.15), [41.24](/rulebook#41.24) |
8322
+ | `abide:hydrate` / `abide:navigate` | `warning` | The browser lane. | [28.8](/rulebook#28.8), [41.10](/rulebook#41.10), [41.15](/rulebook#41.15), [41.24](/rulebook#41.24), [45.10](/rulebook#45.10) |
8310
8323
 
8311
8324
  ## Taught in
8312
8325
 
@@ -8578,7 +8591,7 @@ Generated surfaces: what is written in place rather than imported.
8578
8591
 
8579
8592
  | Name | Signature | Meaning | Rules |
8580
8593
  | --- | --- | --- | --- |
8581
- | `navigate` | `(url?: URL \| string, options?: { replace?: boolean, keepScroll?: boolean }) => Promise<void>` | Navigates. | [30.7](/rulebook#30.7), [30.9](/rulebook#30.9), [30.10](/rulebook#30.10), [30.11](/rulebook#30.11), [30.12](/rulebook#30.12), [30.13](/rulebook#30.13), [30.14](/rulebook#30.14), [30.15](/rulebook#30.15), [30.16](/rulebook#30.16), [30.17](/rulebook#30.17), [30.18](/rulebook#30.18), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.22](/rulebook#30.22), [30.23](/rulebook#30.23), [30.24](/rulebook#30.24), [30.25](/rulebook#30.25), [30.27](/rulebook#30.27), [30.29](/rulebook#30.29), [30.30](/rulebook#30.30), [30.31](/rulebook#30.31) |
8594
+ | `navigate` | `(url?: URL \| string, options?: { replace?: boolean, keepScroll?: boolean }) => Promise<void>` | Navigates. | [30.7](/rulebook#30.7), [30.9](/rulebook#30.9), [30.10](/rulebook#30.10), [30.11](/rulebook#30.11), [30.12](/rulebook#30.12), [30.13](/rulebook#30.13), [30.14](/rulebook#30.14), [30.15](/rulebook#30.15), [30.16](/rulebook#30.16), [30.17](/rulebook#30.17), [30.18](/rulebook#30.18), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.22](/rulebook#30.22), [30.23](/rulebook#30.23), [30.24](/rulebook#30.24), [30.25](/rulebook#30.25), [30.27](/rulebook#30.27), [30.29](/rulebook#30.29), [30.30](/rulebook#30.30), [30.31](/rulebook#30.31), [30.32](/rulebook#30.32), [45.12](/rulebook#45.12) |
8582
8595
 
8583
8596
  ## Taught in
8584
8597
 
@@ -8800,7 +8813,7 @@ Pages and routing: what is written in place rather than imported.
8800
8813
 
8801
8814
  | Name | Signature | Meaning | Rules |
8802
8815
  | --- | --- | --- | --- |
8803
- | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | [29.10](/rulebook#29.10), [35.4](/rulebook#35.4), [35.5](/rulebook#35.5), [35.6](/rulebook#35.6), [35.15](/rulebook#35.15), [35.16](/rulebook#35.16), [41.1](/rulebook#41.1), [41.2](/rulebook#41.2), [41.3](/rulebook#41.3), [41.4](/rulebook#41.4), [41.9](/rulebook#41.9), [41.13](/rulebook#41.13), [41.16](/rulebook#41.16), [41.17](/rulebook#41.17), [41.18](/rulebook#41.18), [41.19](/rulebook#41.19), [41.20](/rulebook#41.20), [41.28](/rulebook#41.28), [35.11](/rulebook#35.11) |
8816
+ | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | [29.10](/rulebook#29.10), [35.4](/rulebook#35.4), [35.5](/rulebook#35.5), [35.6](/rulebook#35.6), [35.15](/rulebook#35.15), [35.16](/rulebook#35.16), [41.1](/rulebook#41.1), [41.2](/rulebook#41.2), [41.3](/rulebook#41.3), [41.4](/rulebook#41.4), [41.9](/rulebook#41.9), [41.13](/rulebook#41.13), [41.16](/rulebook#41.16), [41.17](/rulebook#41.17), [41.18](/rulebook#41.18), [41.19](/rulebook#41.19), [41.20](/rulebook#41.20), [41.28](/rulebook#41.28), [35.11](/rulebook#35.11), [41.29](/rulebook#41.29) |
8804
8817
 
8805
8818
  ## Taught in
8806
8819
 
@@ -8828,10 +8841,11 @@ Configuration: what is written in place rather than imported.
8828
8841
  | `ABIDE_PRINCIPAL_TTL` | `number` | The principal cookie's life, in ms. | [26.15](/rulebook#26.15), [26.16](/rulebook#26.16) |
8829
8842
  | `ABIDE_APP_TOKEN` | `string \| null` | The bearer the remote CLI sends. | [36.6](/rulebook#36.6) |
8830
8843
  | `ABIDE_APP_URL` | `string \| null` | Names an app somewhere else, for the remote CLI. | [36.6](/rulebook#36.6) |
8831
- | `ABIDE_RPC_TIMEOUT` | `number` | The default ms a call may go without progress. | [16.22](/rulebook#16.22), [16.56](/rulebook#16.56), [16.58](/rulebook#16.58) |
8844
+ | `ABIDE_RPC_TIMEOUT` | `number` | The default ms a call may go without progress. | [16.22](/rulebook#16.22), [16.58](/rulebook#16.58) |
8832
8845
  | `ABIDE_MAX_REQUEST_BODY_SIZE` | `number` | The default ceiling on a mutation's body. | [16.13](/rulebook#16.13), [16.54](/rulebook#16.54), [16.55](/rulebook#16.55), [16.78](/rulebook#16.78) |
8833
8846
  | `ABIDE_MAX_SOCKET_MESSAGE_SIZE` | `number` | The ceiling on a frame received over the socket mux, in bytes. | [18.28](/rulebook#18.28) |
8834
8847
  | `ABIDE_MAX_SOCKET_SUBSCRIPTIONS` | `number` | How many values one connection to the socket mux may follow. | [18.31](/rulebook#18.31), [18.32](/rulebook#18.32) |
8848
+ | `ABIDE_OFFLINE` | `boolean` | Whether the app keeps its pages and its bundle on the device, for when the network is gone. | [45.1](/rulebook#45.1), [45.2](/rulebook#45.2), [45.3](/rulebook#45.3), [45.4](/rulebook#45.4), [45.5](/rulebook#45.5), [45.6](/rulebook#45.6), [45.7](/rulebook#45.7), [45.8](/rulebook#45.8), [45.9](/rulebook#45.9), [45.10](/rulebook#45.10), [45.11](/rulebook#45.11), [45.12](/rulebook#45.12), [45.13](/rulebook#45.13), [45.14](/rulebook#45.14), [45.15](/rulebook#45.15), [45.16](/rulebook#45.16) |
8835
8849
  | `ABIDE_STOP_TIMEOUT` | `number` | How long `App.stop` drains, in ms. | [42.17](/rulebook#42.17), [42.18](/rulebook#42.18) |
8836
8850
  | `ABIDE_MAX_STREAM_BUFFER_SIZE` | `number` | The cap on a stream transcript held in memory. | [36.18](/rulebook#36.18) |
8837
8851
  | `ABIDE_MAX_LOG_BUFFER_COUNT` | `number` | How many lines `log.records` is trimmed back to. | [28.1](/rulebook#28.1) |
@@ -8852,7 +8866,7 @@ Configuration: what is written in place rather than imported.
8852
8866
  | `**/public/**` | The files answered as authored, at their path below that directory. | [23.15](/rulebook#23.15), [23.16](/rulebook#23.16), [23.18](/rulebook#23.18), [36.11](/rulebook#36.11), [36.12](/rulebook#36.12), [36.13](/rulebook#36.13), [36.14](/rulebook#36.14), [36.15](/rulebook#36.15), [36.17](/rulebook#36.17), [21.12](/rulebook#21.12), [21.13](/rulebook#21.13), [21.14](/rulebook#21.14), [38.18](/rulebook#38.18), [38.19](/rulebook#38.19) |
8853
8867
  | `src/public/favicon.ico` | The file answered at `/favicon.ico`. | [36.11](/rulebook#36.11) |
8854
8868
  | `src/public/robots.txt` | The file answered at `/robots.txt`. | [36.11](/rulebook#36.11) |
8855
- | `src/public/icon.png` · `src/public/icon.icns` | The files answered at `/icon.png` and `/icon.icns`, and the desktop app's icon. | [36.11](/rulebook#36.11), [38.79](/rulebook#38.79) |
8869
+ | `src/public/icon.png` · `src/public/icon.icns` | The files answered at `/icon.png` and `/icon.icns`, and the desktop app's icon. | [36.11](/rulebook#36.11), [38.79](/rulebook#38.79), [45.15](/rulebook#45.15) |
8856
8870
  | `src/**/` | A directory of the app's, a segment of the route of every file directly in it. | [23.14](/rulebook#23.14), [23.17](/rulebook#23.17), [23.22](/rulebook#23.22) |
8857
8871
  | `**/index.abide` | The page at its directory's route. | [19.1](/rulebook#19.1), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [23.14](/rulebook#23.14), [23.18](/rulebook#23.18) |
8858
8872
  | `**/<name>.abide` | The page at its directory's route and then `<name>`. | [19.1](/rulebook#19.1), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [23.14](/rulebook#23.14), [23.21](/rulebook#23.21) |
@@ -9164,7 +9178,7 @@ Configuration: what is written in place rather than imported.
9164
9178
  | `App` | `interface App` | What a host, a test or a script holds the app as. | [42.12](/rulebook#42.12), [42.13](/rulebook#42.13) |
9165
9179
  | `App.fetch` | `(request: Request) => Promise<Response>` | The whole request path, entered in process. | [42.5](/rulebook#42.5), [42.6](/rulebook#42.6) |
9166
9180
  | `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request }) => Promise<Result>` | One scope around a function, and nothing else. | [42.7](/rulebook#42.7), [42.8](/rulebook#42.8), [11.38](/rulebook#11.38) |
9167
- | `App.listen` | `(options?: { port?: number; hostname?: string; unix?: string }) => Promise<void>` | The socket, bound. | [42.9](/rulebook#42.9), [18.28](/rulebook#18.28), [18.29](/rulebook#18.29), [18.30](/rulebook#18.30) |
9181
+ | `App.listen` | `(options?: { port?: number; hostname?: string; unix?: string }) => Promise<void>` | The socket, bound. | [42.9](/rulebook#42.9), [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [18.28](/rulebook#18.28), [18.29](/rulebook#18.29), [18.30](/rulebook#18.30) |
9168
9182
  | `App.stop` | `() => Promise<void>` | The drain and the teardown. | [42.10](/rulebook#42.10), [42.11](/rulebook#42.11), [42.15](/rulebook#42.15), [42.17](/rulebook#42.17), [37.4](/rulebook#37.4) |
9169
9183
  | `App[Symbol.asyncDispose]` | `() => Promise<void>` | `App.stop`, under the name `await using` calls. | [42.14](/rulebook#42.14) |
9170
9184
 
@@ -10094,7 +10108,7 @@ CLI: what is written in place rather than imported.
10094
10108
  | `abide run <file> [args…]` | Runs a script under the abide runtime. | [38.2](/rulebook#38.2), [38.57](/rulebook#38.57) |
10095
10109
  | `abide check [dir…]` | The type-checker over a `.abide` tree. | [38.3](/rulebook#38.3), [38.31](/rulebook#38.31), [38.32](/rulebook#38.32), [38.33](/rulebook#38.33), [38.40](/rulebook#38.40), [38.105](/rulebook#38.105) |
10096
10110
  | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | [38.108](/rulebook#38.108) |
10097
- | `abide dev [--port <n>]` | Watches the project and keeps the app up. | [37.8](/rulebook#37.8), [37.9](/rulebook#37.9), [38.4](/rulebook#38.4), [38.14](/rulebook#38.14), [38.15](/rulebook#38.15), [38.16](/rulebook#38.16), [38.17](/rulebook#38.17), [38.19](/rulebook#38.19), [38.24](/rulebook#38.24), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.28](/rulebook#38.28), [38.30](/rulebook#38.30), [38.35](/rulebook#38.35), [38.36](/rulebook#38.36), [38.37](/rulebook#38.37), [38.38](/rulebook#38.38), [38.41](/rulebook#38.41), [38.43](/rulebook#38.43), [38.58](/rulebook#38.58), [38.63](/rulebook#38.63), [38.64](/rulebook#38.64), [38.65](/rulebook#38.65), [38.94](/rulebook#38.94), [38.96](/rulebook#38.96), [38.97](/rulebook#38.97) |
10111
+ | `abide dev [--port <n>]` | Watches the project and keeps the app up. | [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [37.8](/rulebook#37.8), [37.9](/rulebook#37.9), [38.4](/rulebook#38.4), [38.14](/rulebook#38.14), [38.15](/rulebook#38.15), [38.16](/rulebook#38.16), [38.17](/rulebook#38.17), [38.19](/rulebook#38.19), [38.24](/rulebook#38.24), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.28](/rulebook#38.28), [38.30](/rulebook#38.30), [38.35](/rulebook#38.35), [38.36](/rulebook#38.36), [38.37](/rulebook#38.37), [38.38](/rulebook#38.38), [38.41](/rulebook#38.41), [38.43](/rulebook#38.43), [38.58](/rulebook#38.58), [38.63](/rulebook#38.63), [38.64](/rulebook#38.64), [38.65](/rulebook#38.65), [38.94](/rulebook#38.94), [38.96](/rulebook#38.96), [38.97](/rulebook#38.97) |
10098
10112
  | `abide build` | Builds the client into content-hashed chunks and a manifest. | [38.43](/rulebook#38.43), [38.5](/rulebook#38.5), [38.18](/rulebook#38.18), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.30](/rulebook#38.30), [38.66](/rulebook#38.66), [38.94](/rulebook#38.94) |
10099
10113
  | `abide start [--port <n>]` | Boots the built app. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.100](/rulebook#38.100), [38.20](/rulebook#38.20), [38.21](/rulebook#38.21), [38.22](/rulebook#38.22), [38.23](/rulebook#38.23), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.28](/rulebook#38.28), [38.29](/rulebook#38.29), [38.58](/rulebook#38.58), [38.67](/rulebook#38.67), [38.68](/rulebook#38.68), [38.95](/rulebook#38.95), [38.97](/rulebook#38.97), [38.110](/rulebook#38.110) |
10100
10114
  | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.7](/rulebook#38.7), [38.53](/rulebook#38.53), [38.54](/rulebook#38.54), [38.55](/rulebook#38.55), [38.58](/rulebook#38.58), [38.59](/rulebook#38.59), [38.60](/rulebook#38.60), [38.62](/rulebook#38.62) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.110.3",
3
+ "version": "0.111.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -525,7 +525,7 @@ function tableOf(
525
525
  */
526
526
  function clientTable(routes: AppRoute[], transitions: boolean): string {
527
527
  return `// Written by \`abide build\`. The addresses this app answers, and what to load for each.
528
- import { listenForRoutes, startRouter } from '${ROUTING_SPECIFIER}'
528
+ import { listenForRoutes, registerWorker, startRouter } from '${ROUTING_SPECIFIER}'
529
529
 
530
530
  export const routes = [
531
531
  ${tableOf(routes)}
@@ -538,6 +538,7 @@ export function start(host: Element) {
538
538
  const router = startRouter(routes, host, ${transitions})
539
539
  listenForRoutes(router)
540
540
  const ready = router.hydrate()
541
+ registerWorker()
541
542
  return { router, ready }
542
543
  }
543
544
  `
@@ -1,3 +1,3 @@
1
1
  // The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
2
2
  // server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
3
- export const BRIDGE_VERSION = '0.3.2'
3
+ export const BRIDGE_VERSION = '0.3.3'
@@ -0,0 +1,2 @@
1
+ /** How often a held-open event stream carries a comment while it has nothing else to send. 16.57. */
2
+ export const HEARTBEAT_MILLISECONDS = 15_000
@@ -0,0 +1,6 @@
1
+ /**
2
+ * How long a connection `App.listen` or `abide dev` binds may go without a byte while nothing over it
3
+ * is being answered (16.56): a body still arriving, or a kept-alive connection between requests.
4
+ * `Bun.serve`'s own default, named so the bound a request lifts can be put back.
5
+ */
6
+ export const IDLE_TIMEOUT_SECONDS = 10
@@ -8,15 +8,19 @@
8
8
 
9
9
  import {
10
10
  ABIDE_ADDRESS,
11
+ APP_MANIFEST_ADDRESS,
11
12
  BRIDGE_ADDRESS,
12
13
  CLIENT_ADDRESS,
13
14
  HEALTH_ADDRESS,
14
15
  MCP_ADDRESS,
15
16
  OPENAPI_ADDRESS,
16
17
  PRIVATE_HANDLERS_ADDRESS,
18
+ WORKER_ADDRESS,
17
19
  } from '#shared/ADDRESSES.ts'
18
20
  import { APP_META } from '#shared/APP_META.ts'
19
21
  import { DEBUG_META } from '#shared/DEBUG_META.ts'
22
+ import { ICON_ADDRESSES } from '#shared/LAUNCHER_FILES.ts'
23
+ import { OFFLINE_META } from '#shared/OFFLINE_META.ts'
20
24
  import { decodedOrKept } from '#shared/decodedOrKept.ts'
21
25
  import { ERROR_META } from '#shared/ERROR_META.ts'
22
26
  import { escapeAttribute } from '#shared/escapeText.ts'
@@ -49,7 +53,10 @@ import {
49
53
  import { answerBlobTagged, answerValue, contentEtag } from './answerValue.ts'
50
54
  import { answerBridge } from './bridgeRelay.ts'
51
55
  import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
56
+ import { appManifest } from './appManifest.ts'
57
+ import { answerWorker } from './answerWorker.ts'
52
58
  import { callerOrMint } from './callerOf.ts'
59
+ import { settingOn } from './config.ts'
53
60
  import { compressAnswer } from './compressAnswer.ts'
54
61
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
55
62
  import { noteCallInRender } from './deferredBuilds.ts'
@@ -72,6 +79,7 @@ import {
72
79
  adoptRequest,
73
80
  beginRender,
74
81
  currentRequestScope,
82
+ type PageRender,
75
83
  type RequestScope,
76
84
  within,
77
85
  writtenStatus,
@@ -162,6 +170,9 @@ export type AppSources = {
162
170
  explained: ((thrown: unknown) => Response) | undefined
163
171
  }
164
172
 
173
+ // 41.29 — the seed sink of a render no page hydrates: every call is left to run when it is made.
174
+ const SEEDS_NOTHING: PageRender['seed'] = () => false
175
+
165
176
  /** The `Response` to one request: a handler, a surface of abide's own, a file, or a page. */
166
177
  export async function answerRequest(
167
178
  app: AppSources,
@@ -338,7 +349,10 @@ async function answerMatched(
338
349
  arrive(at.href, page.pattern, params)
339
350
  // 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
340
351
  // and 41.13, every call it makes is left for the browser's own call to be answered from.
341
- beginRender({ seed: seedFor(caller), calls: [] })
352
+ // 41.29 — BUT NOT A PAGE A SCRIPT FETCHED, which nothing hydrates: its entries would answer the
353
+ // next call this caller makes with what the render saw, a sign-in since included.
354
+ const hydrated = request.headers.get('sec-fetch-dest') !== 'empty'
355
+ beginRender({ seed: hydrated ? seedFor(caller) : SEEDS_NOTHING, calls: [] })
342
356
  // 41.1, 35.2 — THE DOCUMENT IS THE BUILT PAGE, read whole before there is a `Response` to commit a
343
357
  // status to: a refusal the page read, or a throw, is answered by the nearest error page at its own
344
358
  // status rather than as a 200 the stream then abandons.
@@ -457,6 +471,11 @@ type ClientLane = {
457
471
  manifest: Manifest
458
472
  framework: AppSources['framework']
459
473
  head: string
474
+ /**
475
+ * 45.1, 45.9 — whether the worker saves `manifest`'s bundle and the pages register it: `abide
476
+ * build`'s, unless `ABIDE_OFFLINE` is off. Where not, the worker address answers the removing one.
477
+ */
478
+ offline: boolean
460
479
  }
461
480
 
462
481
  /**
@@ -476,10 +495,19 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
476
495
  ])
477
496
  // 35.6 — every page is served in abide's own document; what it carries beyond that, the root
478
497
  // `layout.abide` writes (35.15).
498
+ // 45.15 — installable where the pages are kept offline and the app has an icon to install by.
499
+ const icon = published.get(ICON_ADDRESSES.png)
500
+ const install =
501
+ icon !== undefined && settingOn('ABIDE_OFFLINE', true)
502
+ ? await appManifest(icon.path)
503
+ : undefined
479
504
  const documentOf = (modules: string[]): string =>
480
505
  intoHead(
481
506
  DEFAULT_DOCUMENT,
482
- clientTags(client.manifest, modules) + client.head,
507
+ clientTags(client.manifest, modules) +
508
+ (client.offline ? `<meta name="${OFFLINE_META}">` : '') +
509
+ (install?.tags ?? '') +
510
+ client.head,
483
511
  )
484
512
  const pages: Omit<Ranked, 'segments'>[] = []
485
513
  for (const route of parts.table.routes)
@@ -517,7 +545,20 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
517
545
  errors,
518
546
  // 35.18 — marked, so the browser hydrates no page over what an error page painted.
519
547
  errorDocument: intoHead(documentOf([]), `<meta name="${ERROR_META}">`),
520
- framework: client.framework,
548
+ framework: (pathname, request) => {
549
+ if (pathname === WORKER_ADDRESS)
550
+ return answerWorker(
551
+ client.offline ? client.manifest : undefined,
552
+ )
553
+ if (pathname === APP_MANIFEST_ADDRESS && install !== undefined)
554
+ return new Response(install.body, {
555
+ headers: {
556
+ 'content-type': 'application/manifest+json',
557
+ 'cache-control': 'no-cache',
558
+ },
559
+ })
560
+ return client.framework(pathname, request)
561
+ },
521
562
  published,
522
563
  explained: parts.explained,
523
564
  ...mounted,
@@ -3,7 +3,7 @@
3
3
  // args off the request, and the answer onto a response.
4
4
  //
5
5
  // WIRE-ONLY, per 16.13: the `Origin` gate on a mutation, `maxBodySize`, the `crossOrigin` preflight,
6
- // raising the request's idle timeout (16.22), and 19.7's coercion of the text a URL carried.
6
+ // and 19.7's coercion of the text a URL carried.
7
7
 
8
8
  import type { CopyOptions } from '#shared/CopyOptions.ts'
9
9
  import { type Failed, isFailed } from '#shared/Failed.ts'
@@ -47,9 +47,9 @@ import { type AppRung, failureOf, rungsOver } from './hooks.ts'
47
47
  import { prefers } from './prefers.ts'
48
48
  import {
49
49
  currentRequestScope,
50
- LISTENING,
51
50
  rungsTook,
52
51
  timed,
52
+ unreap,
53
53
  writtenStatus,
54
54
  } from './requestScope.ts'
55
55
  import { type Outcome, outcomeOf, takeSeed } from './seedBuffer.ts'
@@ -265,9 +265,6 @@ export async function answerRpc(
265
265
  if (isFailed(refused)) return answered(refused)
266
266
  throw refused
267
267
  }
268
- // 16.56 — where neither names one, nothing is raised and the server's own idle timeout stands.
269
- const timeout = handler.timeout ?? RPC_SERVER.timeout()
270
- if (timeout !== undefined) raiseTimeout(timeout)
271
268
  // 41.7 — the call a hydrating page makes for what its render already reached is answered from
272
269
  // that, and the handler is not run again (41.13).
273
270
  // 16.51 — a `HEAD` leaves the seed for the `GET` it stands in front of.
@@ -499,23 +496,25 @@ function withParameters(
499
496
  }
500
497
 
501
498
  /**
502
- * The request's body under the ceiling `maxBodySize` sets, or `ABIDE_MAX_REQUEST_BODY_SIZE` where it
503
- * is unset, or a 413 refusal past it — the request itself where neither names one.
499
+ * The request's body, received whole, under the ceiling `maxBodySize` sets, or
500
+ * `ABIDE_MAX_REQUEST_BODY_SIZE` where it is unset, or a 413 refusal past it.
504
501
  *
505
502
  * 16.54 — NO CEILING BY DEFAULT. 16.55 — read off the declared length before a byte of the body is.
506
503
  * 16.78 — EVERY BODY ABIDE READS, a handler's and `/__abide/mcp`'s and `/__abide/bridge`'s alike.
504
+ * 16.23 — and the request received whole once it is read, which is where its idle timeout goes off.
507
505
  */
508
506
  export async function bodyWithin(
509
507
  request: Request,
510
508
  maxBodySize: number | undefined,
511
- ): Promise<Request | Response | Failed<'HttpError', undefined>> {
509
+ ): Promise<Response | Failed<'HttpError', undefined>> {
512
510
  const ceiling =
513
511
  maxBodySize ??
514
512
  positiveSetting('ABIDE_MAX_REQUEST_BODY_SIZE', Number.POSITIVE_INFINITY)
515
- if (!Number.isFinite(ceiling)) return request
516
513
  if (Number(request.headers.get('content-length') ?? 0) > ceiling)
517
514
  return overCeiling(ceiling)
518
- return bodyUnder(request, ceiling)
515
+ const body = await bodyUnder(request, ceiling)
516
+ if (!isFailed(body)) unreap(currentRequestScope())
517
+ return body
519
518
  }
520
519
 
521
520
  /**
@@ -530,6 +529,11 @@ async function bodyUnder(
530
529
  request: Request,
531
530
  ceiling: number,
532
531
  ): Promise<Response | Failed<'HttpError', undefined>> {
532
+ if (!Number.isFinite(ceiling))
533
+ // A blob, which a `Response` shares where it copies an `ArrayBuffer`.
534
+ return new Response(await request.blob(), {
535
+ headers: request.headers,
536
+ })
533
537
  const chunks: Uint8Array[] = []
534
538
  let size = 0
535
539
  if (request.body !== null)
@@ -560,37 +564,6 @@ function preflight(found: Mounted, origin: string, request: Request): Response {
560
564
  return new Response(null, { status: 204, headers })
561
565
  }
562
566
 
563
- /**
564
- * Raises the request's idle timeout to cover `milliseconds`, never lowering it.
565
- *
566
- * 16.22, 16.23 — the floor is the longest any handler in the request asked for. Where no socket is
567
- * bound there is no idle timeout to raise.
568
- *
569
- * 16.57 — `Infinity` is never reaped, which bun spells 0; any finite bound is capped at the 255
570
- * seconds bun's idle timeout can hold.
571
- *
572
- * FOUR SECONDS PAST THE TIMEOUT, NOT AT IT: a load that goes its timeout is refused at that moment,
573
- * and a stream's refusal is a frame written then (16.68). bun's sockets check an idle timeout on a
574
- * tick four seconds wide, so an idle timeout of the same second — or one past it — fired anywhere in
575
- * the tick before it and reaped the connection ahead of the frame, one stream in two under `abide
576
- * dev`. Four is the tick, so the refusal always goes out first.
577
- *
578
- * ON THE REQUEST THE LISTENER RECEIVED, never the one being answered: under a mount, or past a
579
- * rung that handed `next` its own, that is a copy, and bun's `timeout` does nothing for a request
580
- * it was not handed itself.
581
- */
582
- export function raiseTimeout(milliseconds: number): void {
583
- const scope = currentRequestScope()
584
- const received = scope?.received
585
- if (scope === undefined || received === undefined) return
586
- const seconds = Number.isFinite(milliseconds)
587
- ? Math.min(255, Math.ceil(milliseconds / 1000) + 4)
588
- : Number.POSITIVE_INFINITY
589
- if (seconds <= scope.timeout) return
590
- scope.timeout = seconds
591
- LISTENING.timeout(received, Number.isFinite(seconds) ? seconds : 0)
592
- }
593
-
594
567
  function isForm(request: Request): boolean {
595
568
  const type = request.headers.get('content-type') ?? ''
596
569
  return (
@@ -315,7 +315,7 @@ export class MuxConnection {
315
315
  upgrade.headers.get('origin'),
316
316
  upgrade.url,
317
317
  )
318
- // NO `received`: an upgraded connection has no http idle timeout for 16.22 to raise.
318
+ // NO `received`: an upgraded connection has no http idle timeout for 16.23 to set.
319
319
  this.scope = scopeFor(
320
320
  new Request(upgrade, { signal: this.hangUp.signal }),
321
321
  undefined,
@@ -562,7 +562,7 @@ export function followAsEvents(
562
562
  let ended = false
563
563
  return new ReadableStream({
564
564
  start(controller) {
565
- controller.enqueue(ENCODER.encode(EVENT_STREAM_OPENER))
565
+ controller.enqueue(EVENT_STREAM_OPENER)
566
566
  // Ended by the app's stop as any body it holds open is, or that stop waits on it.
567
567
  unhold = heldByApp(() => {
568
568
  if (ended) return
@@ -14,6 +14,7 @@ import {
14
14
  } from './requestScope.ts'
15
15
  import {
16
16
  EVENT_STREAM_OPENER,
17
+ heldBody,
17
18
  refusalFrame,
18
19
  streamEvent,
19
20
  } from './streamEvent.ts'
@@ -38,7 +39,7 @@ export async function answerValue(
38
39
  headers: Headers,
39
40
  status: number,
40
41
  ): Promise<Response> {
41
- if (value instanceof Response) return value
42
+ if (value instanceof Response) return heldUntilSent(value)
42
43
  const framing = currentRequestScope()?.framing
43
44
  // 17.9, 17.10 — what `json` fixed: the standing value, whatever it is, and `null` for nothing.
44
45
  if (framing === 'json') {
@@ -223,8 +224,7 @@ export function bodyOf(
223
224
  const unhold = heldByApp(() => stopped())
224
225
  return new ReadableStream<Uint8Array>({
225
226
  start(controller) {
226
- if (framing === 'sse')
227
- controller.enqueue(ENCODER.encode(EVENT_STREAM_OPENER))
227
+ if (framing === 'sse') controller.enqueue(EVENT_STREAM_OPENER)
228
228
  stopped = () => {
229
229
  if (ended) return
230
230
  release()
@@ -290,6 +290,18 @@ export function bodyOf(
290
290
  })
291
291
  }
292
292
 
293
+ /**
294
+ * `answer`, holding its request's scope until its body has been read to the end.
295
+ *
296
+ * 12.5, 16.23 — A `Response` A HANDLER BUILT MAY BE A STREAM, and nothing tells a stream from a
297
+ * string or a blob once it is one: `body` is a `ReadableStream` either way. So every body is held,
298
+ * at the price of a buffered one going out chunked rather than under a `Content-Length`.
299
+ */
300
+ function heldUntilSent(answer: Response): Response {
301
+ if (answer.body === null) return answer
302
+ return new Response(heldBody(answer.body, holdScope(), false), answer)
303
+ }
304
+
293
305
  /**
294
306
  * A blob answered at its own type under the given etag, range-aware.
295
307
  *