@abide/abide 0.114.1 → 0.115.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.
- package/CHANGELOG.md +6 -0
- package/docs/REGISTRY.md +24 -5
- package/docs/RULEBOOK.md +137 -11
- package/docs/abide.md +198 -8
- package/package.json +4 -2
- package/src/compiler/REACTIVE_MEMBERS.ts +1 -0
- package/src/server/OFFLINE_STALE.ts +10 -0
- package/src/server/answerRequest.ts +39 -10
- package/src/server/answerRpc.ts +25 -5
- package/src/server/answerWorker.ts +111 -13
- package/src/server/createApp.ts +8 -2
- package/src/server/render.ts +3 -2
- package/src/server/seedBuffer.ts +28 -9
- package/src/shared/KEEP_HEADER.ts +7 -0
- package/src/shared/KEEP_OPENED.ts +7 -0
- package/src/shared/KEPT_HEADER.ts +6 -0
- package/src/shared/LOAD_OFFLINE.ts +15 -0
- package/src/shared/OUTBOX_SENDING.ts +9 -0
- package/src/shared/answerOf.ts +14 -2
- package/src/shared/index.ts +2 -0
- package/src/shared/outbox.ts +116 -0
- package/src/shared/reactive/Keyed.ts +9 -0
- package/src/shared/reactive/Reactive.ts +197 -3
- package/src/shared/reactive/SELECTION_MEMBERS.ts +1 -0
- package/src/shared/reactive/callLoad.ts +8 -0
- package/src/shared/reactive/index.ts +23 -1
- package/src/shared/rpc.ts +25 -5
- package/src/shared/rpcRequest.ts +12 -8
- package/src/ui/outboxQueue.ts +427 -0
- package/src/ui/socketWire.ts +31 -2
- package/src/ui/wire.ts +111 -10
package/docs/abide.md
CHANGED
|
@@ -38,6 +38,7 @@ at its address with `.md` on the end, and the links below point at those files.
|
|
|
38
38
|
- `Selection` — `reference/types/Selection.md`
|
|
39
39
|
- `busy` — `reference/busy.md`
|
|
40
40
|
- `ready` — `reference/ready.md`
|
|
41
|
+
- `synced` — `reference/synced.md`
|
|
41
42
|
- `refresh` — `reference/refresh.md`
|
|
42
43
|
- `invalidate` — `reference/invalidate.md`
|
|
43
44
|
- `refuse` — `reference/refuse.md`
|
|
@@ -87,6 +88,8 @@ at its address with `.md` on the end, and the links below point at those files.
|
|
|
87
88
|
- `route` — `reference/route.md`
|
|
88
89
|
- `Params` — `reference/types/Params.md`
|
|
89
90
|
- `online` — `reference/online.md`
|
|
91
|
+
- `outbox` — `reference/outbox.md`
|
|
92
|
+
- `QueuedCall` — `reference/types/QueuedCall.md`
|
|
90
93
|
- `reachable` — `reference/reachable.md`
|
|
91
94
|
- `trace` — `reference/trace.md`
|
|
92
95
|
- `server` — `reference/server.md`
|
|
@@ -4889,7 +4892,7 @@ test('the health check carries the database beside the baseline', async () => {
|
|
|
4889
4892
|
|
|
4890
4893
|
## The browser going offline is a reactive value
|
|
4891
4894
|
|
|
4892
|
-
**`online` — the connection, read by name** —
|
|
4895
|
+
**`online` — the connection, read by name** — On a train the network comes and goes. Take the browser offline in DevTools' Network panel and the banner says so.
|
|
4893
4896
|
|
|
4894
4897
|
```abide src/ops/lib/OfflineBanner.abide
|
|
4895
4898
|
<script>
|
|
@@ -4897,7 +4900,9 @@ import { online } from '@abide/abide'
|
|
|
4897
4900
|
</script>
|
|
4898
4901
|
|
|
4899
4902
|
{#if !online}
|
|
4900
|
-
<p class="status">
|
|
4903
|
+
<p class="status">
|
|
4904
|
+
You are offline. Calls you log are sent once you are back.
|
|
4905
|
+
</p>
|
|
4901
4906
|
{/if}
|
|
4902
4907
|
|
|
4903
4908
|
<ul class="fields">
|
|
@@ -4938,6 +4943,107 @@ watch(() => { if (online) refresh() })
|
|
|
4938
4943
|
</script>
|
|
4939
4944
|
```
|
|
4940
4945
|
|
|
4946
|
+
## A call logged offline is sent when the network is back
|
|
4947
|
+
|
|
4948
|
+
**`offline` — what a call settles to with no network** — A call logged on a train should not be lost in a tunnel. Take the browser offline in DevTools' Network panel, log a call, then go back online.
|
|
4949
|
+
|
|
4950
|
+
```abide src/ops/lib/CallLog.abide
|
|
4951
|
+
<script>
|
|
4952
|
+
import { outbox, reactive } from '@abide/abide'
|
|
4953
|
+
import listCalls from '../listCalls.ts'
|
|
4954
|
+
import logCall from '../logCall.ts'
|
|
4955
|
+
|
|
4956
|
+
const calls = listCalls()
|
|
4957
|
+
let note = reactive('')
|
|
4958
|
+
|
|
4959
|
+
function log() {
|
|
4960
|
+
const call = { id: crypto.randomUUID(), note }
|
|
4961
|
+
note = ''
|
|
4962
|
+
// With no network: shown at once, and sent from the outbox later.
|
|
4963
|
+
logCall(call, {
|
|
4964
|
+
offline: () => calls.patch((held) => held.push(call)),
|
|
4965
|
+
})
|
|
4966
|
+
}
|
|
4967
|
+
</script>
|
|
4968
|
+
|
|
4969
|
+
<div class="row">
|
|
4970
|
+
<input bind:value={note} placeholder="What Grace said">
|
|
4971
|
+
<button onclick={log}>Log call</button>
|
|
4972
|
+
</div>
|
|
4973
|
+
|
|
4974
|
+
<ul class="fields stacked">
|
|
4975
|
+
{#for call of calls.ready() ? calls : [] by call.id}
|
|
4976
|
+
<li><span>call</span><strong>{call.note}</strong></li>
|
|
4977
|
+
{/for}
|
|
4978
|
+
</ul>
|
|
4979
|
+
|
|
4980
|
+
<ul class="tiles">
|
|
4981
|
+
<li><span>waiting</span>
|
|
4982
|
+
<strong>{outbox(logCall).length}</strong></li>
|
|
4983
|
+
<li><span>saved</span>
|
|
4984
|
+
<strong>{calls.synced() ? 'yes' : 'not yet'}</strong></li>
|
|
4985
|
+
</ul>
|
|
4986
|
+
```
|
|
4987
|
+
|
|
4988
|
+
```ts src/ops/logCall.ts
|
|
4989
|
+
import { POST } from '@abide/abide/server'
|
|
4990
|
+
import { type Call, calls } from './lib/calls.ts'
|
|
4991
|
+
|
|
4992
|
+
/** Logs a call once, however often it is sent: the browser picks its id. */
|
|
4993
|
+
export default POST(
|
|
4994
|
+
({ id, note }: Call) => {
|
|
4995
|
+
calls.patch((held) => {
|
|
4996
|
+
if (!held.some((call) => call.id === id)) held.push({ id, note })
|
|
4997
|
+
})
|
|
4998
|
+
},
|
|
4999
|
+
{ description: 'Logs a call against a contact.' },
|
|
5000
|
+
)
|
|
5001
|
+
```
|
|
5002
|
+
|
|
5003
|
+
```ts src/ops/lib/CallLog.test.ts
|
|
5004
|
+
import { expect, test } from 'bun:test'
|
|
5005
|
+
import { flushSync } from '@abide/abide'
|
|
5006
|
+
import { mount } from '@abide/harness/abide'
|
|
5007
|
+
import { calls } from './calls.ts'
|
|
5008
|
+
import CallLog from './CallLog.abide'
|
|
5009
|
+
|
|
5010
|
+
test('a call logged with the network is saved, and nothing waits', async () => {
|
|
5011
|
+
using _log = await mount(CallLog({}))
|
|
5012
|
+
const input = document.querySelector('input') as HTMLInputElement
|
|
5013
|
+
input.value = 'Sent the quote'
|
|
5014
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
5015
|
+
document.querySelector('button')?.click()
|
|
5016
|
+
await Bun.sleep(10)
|
|
5017
|
+
flushSync()
|
|
5018
|
+
expect(calls().map((call) => call.note)).toContain('Sent the quote')
|
|
5019
|
+
const tiles = [...document.querySelectorAll('.tiles strong')]
|
|
5020
|
+
expect(tiles.map((tile) => tile.textContent)).toEqual(['0', 'yes'])
|
|
5021
|
+
})
|
|
5022
|
+
|
|
5023
|
+
test('a call sent twice under one id is logged once', async () => {
|
|
5024
|
+
const { default: logCall } = await import('../logCall.ts')
|
|
5025
|
+
const call = { id: 'again', note: 'Asked twice' }
|
|
5026
|
+
await logCall(call)
|
|
5027
|
+
await logCall(call)
|
|
5028
|
+
expect(calls().filter((each) => each.id === 'again').length).toBe(1)
|
|
5029
|
+
})
|
|
5030
|
+
```
|
|
5031
|
+
|
|
5032
|
+
A call given `offline` that gets no answer settles to what `offline` returns. A mutation then waits in the outbox, kept on the device through a reload, and is sent once the network answers: oldest first, one at a time, by one tab for all of them. Here `offline` also adds the call to the list, so it shows at once. `outbox` is asked of a selection as `busy` is, and answers a list: `outbox(logCall)` is the calls waiting for that handler.
|
|
5033
|
+
|
|
5034
|
+
What is shown before the server has it says so: `synced()` is false on a value written in the browser rather than made by its producer, and on anything read from one. The list keeps its added call until the outbox is empty, then reads the server's again. A call is sent with the same `idempotency-key` every time, and `logCall` takes the browser's id, so a send whose answer was lost and is sent again logs the call once.
|
|
5035
|
+
|
|
5036
|
+
| Name | What it does for the CRM |
|
|
5037
|
+
| --- | --- |
|
|
5038
|
+
| `outbox` · `rpc.outbox` | the calls waiting to be sent, and the ones the server refused, over a selection |
|
|
5039
|
+
| `QueuedCall` | one call in the outbox: its key, its address, its args, and how often it was sent |
|
|
5040
|
+
| `call.value` | what the call settled to: its stand-in, read as unsynced, until the server answers, and its refusal as `error()`, after a reload too |
|
|
5041
|
+
| `call.retry` | sends a refused call again |
|
|
5042
|
+
| `call.discard` | drops a call, sent or not |
|
|
5043
|
+
| `s.synced` | whether a value holds what its producer made |
|
|
5044
|
+
| `k.synced` · `rpc.synced` | the same, over a keyed value's or a handler's entries |
|
|
5045
|
+
| `synced` | the same, over a selection |
|
|
5046
|
+
|
|
4941
5047
|
## Next
|
|
4942
5048
|
|
|
4943
5049
|
- [Open it to machines](/guide/open-it-to-machines.md) — the CRM's handlers, described to other programs
|
|
@@ -6422,7 +6528,15 @@ abide start
|
|
|
6422
6528
|
ABIDE_OFFLINE=0 abide start
|
|
6423
6529
|
```
|
|
6424
6530
|
|
|
6425
|
-
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
|
|
6531
|
+
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. A kept page opens at once, however weak the signal, and the network is asked for the next copy behind it. With the network gone, a page opened in the last week 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.
|
|
6532
|
+
|
|
6533
|
+
| Name | What it does for the CRM |
|
|
6534
|
+
| --- | --- |
|
|
6535
|
+
| `cache-control: private, stale-while-revalidate=604800, stale-if-error=604800` | what a page and a handler's answer carry, so the device shows what it kept while it asks again |
|
|
6536
|
+
| `cache-control: public, max-age=0, stale-while-revalidate=604800, stale-if-error=604800` | the same, for a public file |
|
|
6537
|
+
| `clear-site-data: "cache"` | what a sign-in or a sign-out answers, so the browser forgets what it kept for the last person |
|
|
6538
|
+
|
|
6539
|
+
What is kept is what those headers allow, and nothing else decides it. A handler that writes `cache-control: no-store` through `response` is never kept, and an answer with a `ttl` is shown from the device until its `max-age` runs out. A refresh always asks the network.
|
|
6426
6540
|
|
|
6427
6541
|
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.
|
|
6428
6542
|
|
|
@@ -6464,6 +6578,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6464
6578
|
| [`Selection`](/reference/types/Selection.md) | `@abide/abide` |
|
|
6465
6579
|
| [`busy`](/reference/busy.md) | `@abide/abide` |
|
|
6466
6580
|
| [`ready`](/reference/ready.md) | `@abide/abide` |
|
|
6581
|
+
| [`synced`](/reference/synced.md) | `@abide/abide` |
|
|
6467
6582
|
| [`refresh`](/reference/refresh.md) | `@abide/abide` |
|
|
6468
6583
|
| [`invalidate`](/reference/invalidate.md) | `@abide/abide` |
|
|
6469
6584
|
| [`refuse`](/reference/refuse.md) | `@abide/abide` |
|
|
@@ -6533,6 +6648,8 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6533
6648
|
| [`route`](/reference/route.md) | `@abide/abide` |
|
|
6534
6649
|
| [`Params`](/reference/types/Params.md) | `@abide/abide` |
|
|
6535
6650
|
| [`online`](/reference/online.md) | `@abide/abide` |
|
|
6651
|
+
| [`outbox`](/reference/outbox.md) | `@abide/abide` |
|
|
6652
|
+
| [`QueuedCall`](/reference/types/QueuedCall.md) | `@abide/abide` |
|
|
6536
6653
|
| [`reachable`](/reference/reachable.md) | `@abide/abide/server` |
|
|
6537
6654
|
| [`trace`](/reference/trace.md) | `@abide/abide/server` |
|
|
6538
6655
|
| [`server`](/reference/server.md) | `@abide/abide/server` |
|
|
@@ -6724,6 +6841,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6724
6841
|
| `s.busy` | `() => boolean` | Work is in flight for this value. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.4](/rulebook#3.4), [3.7](/rulebook#3.7), [3.9](/rulebook#3.9), [5.15](/rulebook#5.15), [7.5](/rulebook#7.5), [7.8](/rulebook#7.8), [11.19](/rulebook#11.19), [11.20](/rulebook#11.20), [11.21](/rulebook#11.21), [11.22](/rulebook#11.22), [11.50](/rulebook#11.50) |
|
|
6725
6842
|
| `s.ready` | `() => boolean` | There is a value to serve. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.5](/rulebook#3.5), [3.6](/rulebook#3.6), [3.8](/rulebook#3.8), [7.8](/rulebook#7.8), [11.20](/rulebook#11.20), [11.54](/rulebook#11.54), [11.58](/rulebook#11.58) |
|
|
6726
6843
|
| `s.error` | `() => Failures \| undefined` | The standing refusal. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.5](/rulebook#3.5), [4.10](/rulebook#4.10), [4.11](/rulebook#4.11), [4.12](/rulebook#4.12) |
|
|
6844
|
+
| `s.synced` | `() => boolean` | What is held is what the producer made. | [3.1](/rulebook#3.1), [3.2](/rulebook#3.2), [3.3](/rulebook#3.3), [3.10](/rulebook#3.10), [3.11](/rulebook#3.11), [3.12](/rulebook#3.12), [3.13](/rulebook#3.13) |
|
|
6727
6845
|
| `s.isError` | `<Name extends Failures['name']>(error: unknown, name: Name) => error is Extract<Failures, { name: Name }>` | Whether a caught failure is the one named. | [3.1](/rulebook#3.1) |
|
|
6728
6846
|
|
|
6729
6847
|
## Triggers
|
|
@@ -6746,6 +6864,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6746
6864
|
- [Talk to the server](/guide/talk-to-the-server.md)
|
|
6747
6865
|
- [Keep it current](/guide/keep-it-current.md)
|
|
6748
6866
|
- [Make it live](/guide/make-it-live.md)
|
|
6867
|
+
- [Run it](/guide/run-it.md)
|
|
6749
6868
|
|
|
6750
6869
|
---
|
|
6751
6870
|
|
|
@@ -6768,6 +6887,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6768
6887
|
| --- | --- | --- | --- |
|
|
6769
6888
|
| `k.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to matching entries. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.12](/rulebook#13.12) |
|
|
6770
6889
|
| `k.ready` | `(pattern?: Partial<Args>) => boolean` | `ready` narrowed to matching entries. | [13.11](/rulebook#13.11), [13.12](/rulebook#13.12) |
|
|
6890
|
+
| `k.synced` | `(pattern?: Partial<Args>) => boolean` | `synced` narrowed to matching entries. | [13.12](/rulebook#13.12), [13.18](/rulebook#13.18) |
|
|
6771
6891
|
| `k.invalidate` | `(pattern?: Partial<Args>) => void` | `invalidate` narrowed to matching entries. | [7.2](/rulebook#7.2), [7.3](/rulebook#7.3), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12) |
|
|
6772
6892
|
| `k.refresh` | `(pattern?: Partial<Args>) => void` | `refresh` narrowed to matching entries. | [7.4](/rulebook#7.4), [7.5](/rulebook#7.5), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12) |
|
|
6773
6893
|
|
|
@@ -6775,6 +6895,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6775
6895
|
|
|
6776
6896
|
- [Edit a contact](/guide/edit-a-contact.md)
|
|
6777
6897
|
- [Keep it current](/guide/keep-it-current.md)
|
|
6898
|
+
- [Run it](/guide/run-it.md)
|
|
6778
6899
|
|
|
6779
6900
|
---
|
|
6780
6901
|
|
|
@@ -7028,7 +7149,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
7028
7149
|
|
|
7029
7150
|
| Name | Signature | Meaning | Rules |
|
|
7030
7151
|
| --- | --- | --- | --- |
|
|
7031
|
-
| `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.12](/rulebook#13.12) |
|
|
7152
|
+
| `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.12](/rulebook#13.12), [46.11](/rulebook#46.11) |
|
|
7032
7153
|
|
|
7033
7154
|
## Taught in
|
|
7034
7155
|
|
|
@@ -7054,6 +7175,24 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
7054
7175
|
|
|
7055
7176
|
---
|
|
7056
7177
|
|
|
7178
|
+
<!-- reference/synced.md -->
|
|
7179
|
+
|
|
7180
|
+
# `synced`
|
|
7181
|
+
|
|
7182
|
+
`import { synced } from '@abide/abide'`
|
|
7183
|
+
|
|
7184
|
+
## Selections — over many entries
|
|
7185
|
+
|
|
7186
|
+
| Name | Signature | Meaning | Rules |
|
|
7187
|
+
| --- | --- | --- | --- |
|
|
7188
|
+
| `synced` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection holds what its producer made. | [13.12](/rulebook#13.12), [13.18](/rulebook#13.18) |
|
|
7189
|
+
|
|
7190
|
+
## Taught in
|
|
7191
|
+
|
|
7192
|
+
- [Run it](/guide/run-it.md)
|
|
7193
|
+
|
|
7194
|
+
---
|
|
7195
|
+
|
|
7057
7196
|
<!-- reference/refresh.md -->
|
|
7058
7197
|
|
|
7059
7198
|
# `refresh`
|
|
@@ -7227,12 +7366,14 @@ Reactive primitives: what is written in place rather than imported.
|
|
|
7227
7366
|
|
|
7228
7367
|
| Name | Signature | Meaning | Rules |
|
|
7229
7368
|
| --- | --- | --- | --- |
|
|
7230
|
-
| `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) |
|
|
7369
|
+
| `Rpc` | `<Value, Args, Failures, Produced = Value>(args?: Args, options?: { signal?: AbortSignal; offline?: (args: Args) => Value }) => 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), [16.80](/rulebook#16.80), [16.85](/rulebook#16.85) |
|
|
7370
|
+
| `offline` | `(args: Args) => Value` | What a call settles to where a browser could not get its answer. | [16.81](/rulebook#16.81), [16.82](/rulebook#16.82), [16.83](/rulebook#16.83), [16.84](/rulebook#16.84) |
|
|
7231
7371
|
| `Value` | `unknown` | What the addressed `Reactive` holds. | [16.3](/rulebook#16.3), [16.30](/rulebook#16.30) |
|
|
7232
7372
|
| `Produced` | `unknown` | What it yields, per the unit rule. | [1.2](/rulebook#1.2), [16.3](/rulebook#16.3) |
|
|
7233
7373
|
| `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) |
|
|
7234
|
-
| `rpc.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to the handler's matching entries. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.12](/rulebook#13.12) |
|
|
7374
|
+
| `rpc.busy` | `(pattern?: Partial<Args>) => boolean` | `busy` narrowed to the handler's matching entries. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.12](/rulebook#13.12), [46.11](/rulebook#46.11) |
|
|
7235
7375
|
| `rpc.ready` | `(pattern?: Partial<Args>) => boolean` | `ready` narrowed to the handler's matching entries. | [13.11](/rulebook#13.11), [13.12](/rulebook#13.12) |
|
|
7376
|
+
| `rpc.synced` | `(pattern?: Partial<Args>) => boolean` | `synced` narrowed to the handler's matching entries. | [13.12](/rulebook#13.12), [13.18](/rulebook#13.18) |
|
|
7236
7377
|
| `rpc.invalidate` | `(pattern?: Partial<Args>) => void` | `invalidate` narrowed to the handler's matching entries. | [7.2](/rulebook#7.2), [7.3](/rulebook#7.3), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12) |
|
|
7237
7378
|
| `rpc.refresh` | `(pattern?: Partial<Args>) => void` | `refresh` narrowed to the handler's matching entries. | [7.4](/rulebook#7.4), [7.5](/rulebook#7.5), [13.4](/rulebook#13.4), [13.12](/rulebook#13.12) |
|
|
7238
7379
|
| `rpc.isError` | `<Name extends Failures['name']>(error: unknown, name: Name) => error is Extract<Failures, { name: Name }>` | Whether a caught failure is the one named. | [16.8](/rulebook#16.8) |
|
|
@@ -7241,11 +7382,18 @@ Reactive primitives: what is written in place rather than imported.
|
|
|
7241
7382
|
| `rpc.url` | `(args?: Args) => string` | The address, resolved against the mount. | [16.9](/rulebook#16.9), [16.10](/rulebook#16.10), [30.28](/rulebook#30.28) |
|
|
7242
7383
|
| `rpc.description` | `string \| undefined` | The human description. | [16.28](/rulebook#16.28) |
|
|
7243
7384
|
|
|
7385
|
+
## `outbox`
|
|
7386
|
+
|
|
7387
|
+
| Name | Signature | Meaning | Rules |
|
|
7388
|
+
| --- | --- | --- | --- |
|
|
7389
|
+
| `rpc.outbox` | `(pattern?: Partial<Args>) => QueuedCall[]` | `outbox` narrowed to the handler's calls. | [46.1](/rulebook#46.1), [13.12](/rulebook#13.12) |
|
|
7390
|
+
|
|
7244
7391
|
## Taught in
|
|
7245
7392
|
|
|
7246
7393
|
- [Talk to the server](/guide/talk-to-the-server.md)
|
|
7247
7394
|
- [Keep it current](/guide/keep-it-current.md)
|
|
7248
7395
|
- [Make it live](/guide/make-it-live.md)
|
|
7396
|
+
- [Run it](/guide/run-it.md)
|
|
7249
7397
|
|
|
7250
7398
|
---
|
|
7251
7399
|
|
|
@@ -7518,6 +7666,9 @@ Transports: what is written in place rather than imported.
|
|
|
7518
7666
|
| `cache-control: no-store` | `/__abide/openapi.json`, `/__abide/mcp` | Refuses caching for an answer about the app's surfaces. | [21.4](/rulebook#21.4) |
|
|
7519
7667
|
| `cache-control: public, max-age=31536000, immutable` | the built bundle | A chunk addressed by its own content hash. | [21.5](/rulebook#21.5) |
|
|
7520
7668
|
| `cache-control: public, max-age=0, must-revalidate` | a file under a `public` directory | A file at an address the build never chose. | [21.12](/rulebook#21.12) |
|
|
7669
|
+
| `cache-control: private, stale-while-revalidate=604800, stale-if-error=604800` | a page, a `GET`'s answer, where `ABIDE_OFFLINE` registers its worker | The default that lets a copy kept on the device stand in for the network. | [21.22](/rulebook#21.22), [21.11](/rulebook#21.11), [21.17](/rulebook#21.17) |
|
|
7670
|
+
| `cache-control: public, max-age=0, stale-while-revalidate=604800, stale-if-error=604800` | a file under a `public` directory, where `ABIDE_OFFLINE` registers its worker | The same, for a file at an address the build never chose. | [21.23](/rulebook#21.23) |
|
|
7671
|
+
| `clear-site-data: "cache"` | an answer carrying `abide-principal`, where pages are kept offline | Drops what the browser's own cache kept for the caller signed in before. | [21.24](/rulebook#21.24) |
|
|
7521
7672
|
| `etag` | a file under a `public` directory | The answered bytes, addressed by their content. | [21.13](/rulebook#21.13), [21.14](/rulebook#21.14) |
|
|
7522
7673
|
| `referrer-policy: strict-origin-when-cross-origin` | a page | The browsers' own default, written down. | [21.6](/rulebook#21.6) |
|
|
7523
7674
|
| `vary` | wherever an answer depends on a request header | Names the header the answer varied on. | [21.7](/rulebook#21.7) |
|
|
@@ -8020,6 +8171,45 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8020
8171
|
|
|
8021
8172
|
---
|
|
8022
8173
|
|
|
8174
|
+
<!-- reference/outbox.md -->
|
|
8175
|
+
|
|
8176
|
+
# `outbox`
|
|
8177
|
+
|
|
8178
|
+
`import { outbox } from '@abide/abide'`
|
|
8179
|
+
|
|
8180
|
+
## `outbox`
|
|
8181
|
+
|
|
8182
|
+
| Name | Signature | Meaning | Rules |
|
|
8183
|
+
| --- | --- | --- | --- |
|
|
8184
|
+
| `outbox` | `<Chosen extends Selection>(selection?: Chosen, pattern?: Partial<Args>) => QueuedCall[]` | The calls a browser has queued and not delivered, the refused ones included. | [46.1](/rulebook#46.1), [46.2](/rulebook#46.2), [46.3](/rulebook#46.3), [46.4](/rulebook#46.4), [46.5](/rulebook#46.5), [46.6](/rulebook#46.6), [46.7](/rulebook#46.7), [46.8](/rulebook#46.8), [46.9](/rulebook#46.9), [46.10](/rulebook#46.10), [46.14](/rulebook#46.14), [46.15](/rulebook#46.15), [46.16](/rulebook#46.16), [46.17](/rulebook#46.17), [46.20](/rulebook#46.20), [18.35](/rulebook#18.35) |
|
|
8185
|
+
| `call.value` | `Reactive<Value, Value, Failed<string, unknown>>` | The value a queued call settled. | [46.5](/rulebook#46.5), [46.7](/rulebook#46.7), [46.18](/rulebook#46.18), [46.19](/rulebook#46.19) |
|
|
8186
|
+
| `call.retry` | `() => void` | Sends a queued call again. | [46.6](/rulebook#46.6), [46.12](/rulebook#46.12) |
|
|
8187
|
+
| `call.discard` | `() => void` | Drops a queued call. | [46.13](/rulebook#46.13) |
|
|
8188
|
+
|
|
8189
|
+
## Taught in
|
|
8190
|
+
|
|
8191
|
+
- [Run it](/guide/run-it.md)
|
|
8192
|
+
|
|
8193
|
+
---
|
|
8194
|
+
|
|
8195
|
+
<!-- reference/types/QueuedCall.md -->
|
|
8196
|
+
|
|
8197
|
+
# `QueuedCall`
|
|
8198
|
+
|
|
8199
|
+
`import type { QueuedCall } from '@abide/abide'`
|
|
8200
|
+
|
|
8201
|
+
## `outbox`
|
|
8202
|
+
|
|
8203
|
+
| Name | Signature | Meaning | Rules |
|
|
8204
|
+
| --- | --- | --- | --- |
|
|
8205
|
+
| `QueuedCall` | `{ id: string; method: 'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'; url: string; args: Record<string, JsonValue> \| undefined; queuedAt: number; attempts: number; value: Reactive<Value, Value, Failed<string, unknown>>; retry: () => void; discard: () => void }` | One call `outbox` holds. | [46.1](/rulebook#46.1), [46.10](/rulebook#46.10) |
|
|
8206
|
+
|
|
8207
|
+
## Taught in
|
|
8208
|
+
|
|
8209
|
+
- [Run it](/guide/run-it.md)
|
|
8210
|
+
|
|
8211
|
+
---
|
|
8212
|
+
|
|
8023
8213
|
<!-- reference/reachable.md -->
|
|
8024
8214
|
|
|
8025
8215
|
# `reachable`
|
|
@@ -8821,7 +9011,7 @@ Pages and routing: what is written in place rather than imported.
|
|
|
8821
9011
|
|
|
8822
9012
|
| Name | Signature | Meaning | Rules |
|
|
8823
9013
|
| --- | --- | --- | --- |
|
|
8824
|
-
| `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) |
|
|
9014
|
+
| `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), [41.30](/rulebook#41.30) |
|
|
8825
9015
|
|
|
8826
9016
|
## Taught in
|
|
8827
9017
|
|
|
@@ -8853,7 +9043,7 @@ Configuration: what is written in place rather than imported.
|
|
|
8853
9043
|
| `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) |
|
|
8854
9044
|
| `ABIDE_MAX_SOCKET_MESSAGE_SIZE` | `number` | The ceiling on a frame received over the socket mux, in bytes. | [18.28](/rulebook#18.28) |
|
|
8855
9045
|
| `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) |
|
|
8856
|
-
| `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) |
|
|
9046
|
+
| `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), [45.17](/rulebook#45.17), [45.18](/rulebook#45.18), [45.19](/rulebook#45.19), [45.20](/rulebook#45.20), [45.21](/rulebook#45.21), [45.22](/rulebook#45.22), [45.23](/rulebook#45.23) |
|
|
8857
9047
|
| `ABIDE_STOP_TIMEOUT` | `number` | How long `App.stop` drains, in ms. | [42.17](/rulebook#42.17), [42.18](/rulebook#42.18) |
|
|
8858
9048
|
| `ABIDE_MAX_STREAM_BUFFER_SIZE` | `number` | The cap on a stream transcript held in memory. | [36.18](/rulebook#36.18) |
|
|
8859
9049
|
| `ABIDE_MAX_LOG_BUFFER_COUNT` | `number` | How many lines `log.records` is trimmed back to. | [28.1](/rulebook#28.1) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.115.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,7 +57,9 @@
|
|
|
57
57
|
"sideEffects": [
|
|
58
58
|
"./src/server/**",
|
|
59
59
|
"./src/shared/WORK.ts",
|
|
60
|
-
"./src/shared/
|
|
60
|
+
"./src/shared/outbox.ts",
|
|
61
|
+
"./src/shared/reactive/Reactive.ts",
|
|
62
|
+
"./src/ui/outboxQueue.ts"
|
|
61
63
|
],
|
|
62
64
|
"dependencies": {
|
|
63
65
|
"typescript": "^7.0.2"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// 21.22, 21.23 — HOW LONG A COPY KEPT ON THE DEVICE STAYS SERVABLE once stale, where `ABIDE_OFFLINE`
|
|
2
|
+
// registers its worker: a week, while the network answers or fails to — as long as Safari keeps an
|
|
3
|
+
// unvisited site's storage.
|
|
4
|
+
//
|
|
5
|
+
// NO `max-age` BESIDE IT on a page or a `GET`: a copy reads `max-age` as its own life (18.14), and
|
|
6
|
+
// a followed copy stale from the moment it landed would reload on every read. With no lifetime and
|
|
7
|
+
// no `Last-Modified`, a cache holds the answer stale at once, which is all the two windows need.
|
|
8
|
+
|
|
9
|
+
export const OFFLINE_STALE =
|
|
10
|
+
'stale-while-revalidate=604800, stale-if-error=604800'
|
|
@@ -26,6 +26,8 @@ import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
|
26
26
|
import { escapeAttribute } from '#shared/escapeText.ts'
|
|
27
27
|
import { type Failed, isFailed } from '#shared/Failed.ts'
|
|
28
28
|
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
29
|
+
import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
|
|
30
|
+
import { KEEP_OPENED } from '#shared/KEEP_OPENED.ts'
|
|
29
31
|
import { health } from '#shared/health.ts'
|
|
30
32
|
import { appName } from '#shared/log.ts'
|
|
31
33
|
import { compose } from '#shared/Middleware.ts'
|
|
@@ -59,6 +61,7 @@ import { callerOrMint } from './callerOf.ts'
|
|
|
59
61
|
import { settingOn } from './config.ts'
|
|
60
62
|
import { compressAnswer } from './compressAnswer.ts'
|
|
61
63
|
import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
|
|
64
|
+
import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
|
|
62
65
|
import { noteCallInRender } from './deferredBuilds.ts'
|
|
63
66
|
import { answerMcp, openApi } from './generatedSurfaces.ts'
|
|
64
67
|
import { type AppRung, failureOf, rungsOver } from './hooks.ts'
|
|
@@ -84,7 +87,7 @@ import {
|
|
|
84
87
|
within,
|
|
85
88
|
writtenStatus,
|
|
86
89
|
} from './requestScope.ts'
|
|
87
|
-
import { seedFor } from './seedBuffer.ts'
|
|
90
|
+
import { callsOnly, seedFor } from './seedBuffer.ts'
|
|
88
91
|
|
|
89
92
|
// One row of the server's table the compiler writes (`SERVER_TABLE`). The loader is a dynamic import
|
|
90
93
|
// so a page's markup is off the boot path until an address asks for it, which is the same reason
|
|
@@ -159,6 +162,8 @@ export type AppSources = {
|
|
|
159
162
|
errors: RankedError[]
|
|
160
163
|
/** The document the error pages are painted into. */
|
|
161
164
|
errorDocument: string
|
|
165
|
+
/** 21.22, 21.23 — whether a page registers the worker `ABIDE_OFFLINE` turns on (45.1). */
|
|
166
|
+
offline: boolean
|
|
162
167
|
/** Every handler, keyed by its method and address (`handlerKey`), to what answers it. 16.2. */
|
|
163
168
|
handlers: Map<string, Mounted>
|
|
164
169
|
/**
|
|
@@ -186,11 +191,11 @@ export async function answerRequest(
|
|
|
186
191
|
const method = request.method === 'HEAD' ? 'GET' : request.method
|
|
187
192
|
|
|
188
193
|
// 25.1, 25.3, 26.17 — the principal and the account of the app are handlers abide mounts,
|
|
189
|
-
// answered as the app's are.
|
|
194
|
+
// answered as the app's are — 21.4, save that neither is the app's to keep on the device.
|
|
190
195
|
const own = OWN_HANDLERS.get(asked)
|
|
191
196
|
if (own !== undefined)
|
|
192
197
|
return own.method === method
|
|
193
|
-
? answerRpc(own, {}, url, request, app)
|
|
198
|
+
? answerRpc(own, {}, url, request, app, false)
|
|
194
199
|
: methodRefused(app, asked, [own.method])
|
|
195
200
|
// 20.5, 20.9, 36.7 — a surface of abide's own, or a 404 where it answers nothing.
|
|
196
201
|
const surface = SURFACES.get(asked)
|
|
@@ -229,7 +234,7 @@ export async function answerRequest(
|
|
|
229
234
|
const file = app.published.get(decodedOrKept(asked))
|
|
230
235
|
if (file !== undefined)
|
|
231
236
|
return readsOnly(request)
|
|
232
|
-
? publicResponse(request, file)
|
|
237
|
+
? publicResponse(request, file, app.offline)
|
|
233
238
|
: methodRefused(app, asked, READS)
|
|
234
239
|
|
|
235
240
|
// 15.16 — an address that will not percent-decode names nothing, and is the caller's fault.
|
|
@@ -312,7 +317,8 @@ async function answerMatched(
|
|
|
312
317
|
const url = new URL(request.url)
|
|
313
318
|
const asked = url.pathname
|
|
314
319
|
const params = (currentRequestScope() as RequestScope).params as Params
|
|
315
|
-
if ('handler' in found)
|
|
320
|
+
if ('handler' in found)
|
|
321
|
+
return answerRpc(found, params, url, request, app, app.offline)
|
|
316
322
|
const page = found
|
|
317
323
|
|
|
318
324
|
// 30.16 — THE CLIENT ROUTER ASKING, not a reader loading the page. `middleware` has already run
|
|
@@ -333,6 +339,7 @@ async function answerMatched(
|
|
|
333
339
|
// that stored the identity answer for a reader who sent no `accept-encoding` must not hand it
|
|
334
340
|
// to one who did, nor the reverse.
|
|
335
341
|
const headers = new Headers(DOCUMENT_HEADERS)
|
|
342
|
+
if (app.offline) headers.set('cache-control', `private, ${OFFLINE_STALE}`)
|
|
336
343
|
// THE HEADERS A `GET` WOULD CARRY, and no render for a body nobody is sent — nor a caller
|
|
337
344
|
// minted for a request that never becomes a page.
|
|
338
345
|
if (request.method === 'HEAD')
|
|
@@ -350,9 +357,21 @@ async function answerMatched(
|
|
|
350
357
|
// 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
|
|
351
358
|
// and 41.13, every call it makes is left for the browser's own call to be answered from.
|
|
352
359
|
// 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
|
-
|
|
355
|
-
|
|
360
|
+
// next call this caller makes with what the render saw, a sign-in since included — save the one
|
|
361
|
+
// the worker fetched for the navigation it answers, which is the page that hydrates (45.23).
|
|
362
|
+
const keep = request.headers.get(KEEP_HEADER)
|
|
363
|
+
const hydrated =
|
|
364
|
+
keep === KEEP_OPENED ||
|
|
365
|
+
request.headers.get('sec-fetch-dest') !== 'empty'
|
|
366
|
+
// 41.30 — SAVE ONE THE WORKER KEEPS, whose early calls are what a later open of it starts.
|
|
367
|
+
beginRender({
|
|
368
|
+
seed: hydrated
|
|
369
|
+
? seedFor(caller)
|
|
370
|
+
: keep !== null
|
|
371
|
+
? callsOnly()
|
|
372
|
+
: SEEDS_NOTHING,
|
|
373
|
+
calls: [],
|
|
374
|
+
})
|
|
356
375
|
// 41.1, 35.2 — THE DOCUMENT IS THE BUILT PAGE, read whole before there is a `Response` to commit a
|
|
357
376
|
// status to: a refusal the page read, or a throw, is answered by the nearest error page at its own
|
|
358
377
|
// status rather than as a 200 the stream then abandons.
|
|
@@ -561,6 +580,7 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
561
580
|
},
|
|
562
581
|
published,
|
|
563
582
|
explained: parts.explained,
|
|
583
|
+
offline: client.offline,
|
|
564
584
|
...mounted,
|
|
565
585
|
}
|
|
566
586
|
for (const row of sources.routes) {
|
|
@@ -738,14 +758,23 @@ function laneFailure(
|
|
|
738
758
|
* authored, and the content-type off the extension, which is what `Bun.file` already derives
|
|
739
759
|
* rather than a table kept here.
|
|
740
760
|
*/
|
|
741
|
-
function publicResponse(
|
|
761
|
+
function publicResponse(
|
|
762
|
+
request: Request,
|
|
763
|
+
file: PublicFile,
|
|
764
|
+
offline: boolean,
|
|
765
|
+
): Response {
|
|
742
766
|
return answerBlobTagged(
|
|
743
767
|
Bun.file(file.path, { type: file.type }),
|
|
744
768
|
file.etag,
|
|
745
769
|
request,
|
|
746
770
|
// 21.12. Revalidated every time and cached in between, which is what an unhashed name
|
|
747
771
|
// earns — the etag is what makes that one conditional request rather than one transfer.
|
|
748
|
-
|
|
772
|
+
// 21.23 — and served stale where the device keeps it, while that request is made.
|
|
773
|
+
new Headers({
|
|
774
|
+
'cache-control': offline
|
|
775
|
+
? `public, max-age=0, ${OFFLINE_STALE}`
|
|
776
|
+
: 'public, max-age=0, must-revalidate',
|
|
777
|
+
}),
|
|
749
778
|
200,
|
|
750
779
|
)
|
|
751
780
|
}
|
package/src/server/answerRpc.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { followAsEvents } from './answerSocket.ts'
|
|
30
30
|
import { answerValue, namesEtag, sseHeaders } from './answerValue.ts'
|
|
31
31
|
import { argsFor } from './argsFor.ts'
|
|
32
|
+
import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
|
|
32
33
|
import { callerOf } from './callerOf.ts'
|
|
33
34
|
import {
|
|
34
35
|
argsDocumentOf,
|
|
@@ -233,6 +234,8 @@ export async function answerRpc(
|
|
|
233
234
|
request: Request,
|
|
234
235
|
// 16.38 — what paints a refusal for a request that came as a navigation.
|
|
235
236
|
pages: ErrorPages,
|
|
237
|
+
// 21.22 — whether the device keeps what is answered.
|
|
238
|
+
offline: boolean,
|
|
236
239
|
): Promise<Response> {
|
|
237
240
|
const pathname = url.pathname
|
|
238
241
|
const origin = request.headers.get('origin')
|
|
@@ -300,6 +303,7 @@ export async function answerRpc(
|
|
|
300
303
|
args,
|
|
301
304
|
key,
|
|
302
305
|
entry,
|
|
306
|
+
offline,
|
|
303
307
|
)
|
|
304
308
|
// A `Response` the handler built is its own — but the origin answer below is laid over it, so it
|
|
305
309
|
// varies on `Origin` too (21.7), which `handlerHeaders` never reached.
|
|
@@ -350,6 +354,7 @@ async function answerOutcome(
|
|
|
350
354
|
args: unknown,
|
|
351
355
|
key: string | undefined,
|
|
352
356
|
entry: unknown,
|
|
357
|
+
offline: boolean,
|
|
353
358
|
): Promise<Response> {
|
|
354
359
|
const handler = found.handler
|
|
355
360
|
if (!outcome.settled) return answered(failureOf(outcome.error))
|
|
@@ -380,13 +385,28 @@ async function answerOutcome(
|
|
|
380
385
|
headers.set(VERSION_HEADER, etag)
|
|
381
386
|
RPC_SERVER.served?.(handler, serving, key as string)
|
|
382
387
|
}
|
|
383
|
-
// 21.
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
|
|
388
|
+
// 21.22 — A `GET` THE DEVICE KEEPS, where the worker is there to keep it: a value, not a
|
|
389
|
+
// refusal (16.40) nor a stream with no end to keep (16.32) — a generator's, or the lines a
|
|
390
|
+
// `jsonl` or an `sse` fixed.
|
|
391
|
+
const framing = currentRequestScope()?.framing
|
|
392
|
+
const kept =
|
|
393
|
+
offline &&
|
|
394
|
+
found.method === 'GET' &&
|
|
395
|
+
!isFailed(value) &&
|
|
396
|
+
!streams(value) &&
|
|
397
|
+
framing !== 'jsonl' &&
|
|
398
|
+
framing !== 'sse' &&
|
|
399
|
+
writtenStatus() < 300
|
|
400
|
+
// 21.8, 21.9, 21.11 — WHAT IS LEFT OF THE LIFE OF THE PRODUCTION ANSWERED, in place of
|
|
401
|
+
// `no-store` or ahead of 21.22's directives, and nothing else. 21.17 — a `ttl` of Infinity is
|
|
402
|
+
// bounded by `invalidate` alone (11.35), which no cache downstream hears, so it keeps the default.
|
|
403
|
+
const life = Number.isFinite(deadline)
|
|
404
|
+
? `, max-age=${Math.max(0, Math.floor((deadline - Date.now()) / 1000))}`
|
|
405
|
+
: ''
|
|
406
|
+
if (life !== '' || kept)
|
|
387
407
|
headers.set(
|
|
388
408
|
'cache-control',
|
|
389
|
-
`private
|
|
409
|
+
`private${life}${kept ? `, ${OFFLINE_STALE}` : ''}`,
|
|
390
410
|
)
|
|
391
411
|
// 18.19 — A READER HOLDING THIS VERSION IS TOLD SO, and sent no body.
|
|
392
412
|
if (
|