@abide/abide 0.102.0 → 0.104.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 +23 -0
- package/bin/abide.ts +31 -28
- package/docs/REGISTRY.md +40 -36
- package/docs/RULEBOOK.md +113 -92
- package/docs/abide.md +788 -554
- package/package.json +2 -1
- package/scaffold/AGENTS.md +13 -6
- package/src/compiler/TypeSession.ts +108 -35
- package/src/compiler/appMap.ts +91 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +35 -18
- package/src/compiler/checkedShadow.ts +40 -7
- package/src/compiler/compileApp.ts +271 -76
- package/src/compiler/compiledEntry.ts +10 -6
- package/src/compiler/componentGraph.ts +6 -7
- package/src/compiler/declaredExports.ts +3 -3
- package/src/compiler/emitClient.ts +3 -3
- package/src/compiler/emitComponent.ts +9 -9
- package/src/compiler/emitSource.ts +5 -9
- package/src/compiler/handlersUnder.ts +114 -73
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/mountsOf.ts +30 -18
- package/src/compiler/parseAbide.ts +12 -12
- package/src/compiler/projectShadow.ts +71 -46
- package/src/compiler/readsIn.ts +2 -2
- package/src/compiler/scaffold.ts +24 -21
- package/src/compiler/serveLanguage.ts +0 -24
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/splitImports.ts +4 -4
- package/src/compiler/writeDefaultProject.ts +28 -1
- package/src/server/MODULE_QUERIES.ts +12 -3
- package/src/server/abideModules.ts +156 -53
- package/src/server/answerRequest.ts +9 -4
- package/src/server/answerRpc.ts +18 -16
- package/src/server/appTree.ts +92 -25
- package/src/server/commands.ts +5 -4
- package/src/server/componentModules.d.ts +1 -2
- package/src/server/createApp.ts +23 -11
- package/src/server/devWorker.ts +5 -1
- package/src/server/fenceServer.ts +36 -13
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/hooks.ts +17 -4
- package/src/server/index.ts +1 -0
- package/src/server/installSourceMaps.ts +6 -1
- package/src/server/launcherOptions.ts +12 -13
- package/src/server/launcherPage.ts +2 -2
- package/src/server/nativeMenu.ts +3 -3
- package/src/server/preload.ts +13 -0
- package/src/server/runDesktop.ts +11 -16
- package/src/server/serveDev.ts +8 -2
- package/src/server/surfaceOf.ts +2 -0
- package/src/server/versionOf.ts +1 -2
- package/src/server/withAbideSources.ts +1 -1
- package/src/shared/ADDRESSES.ts +3 -0
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +19 -4
- package/src/shared/index.ts +2 -1
- package/src/{ui → shared}/menuBridge.ts +1 -1
- package/src/shared/menuItem.ts +2 -2
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Cursor.ts +66 -123
- package/src/shared/reactive/Reactive.ts +74 -28
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/chunksOfStream.ts +10 -34
- package/src/{ui → shared}/reportMenu.ts +7 -9
- package/src/shared/routePattern.ts +74 -23
- package/src/shared/rpc.ts +1 -1
- package/src/ui/Router.ts +0 -6
- package/src/compiler/syncTypes.ts +0 -32
- package/src/compiler/writeTypes.ts +0 -82
- package/src/server/LAUNCHER_FILES.ts +0 -9
- /package/scaffold/src/{page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
package/docs/abide.md
CHANGED
|
@@ -135,7 +135,7 @@ In abide it is the handler, and a page that reads it. You declare what a value *
|
|
|
135
135
|
|
|
136
136
|
**Read an invoice** — A row from your database on the page, with nothing between them. One file: the handler you would have written anyway, and the markup that reads it.
|
|
137
137
|
|
|
138
|
-
```abide Invoice.abide
|
|
138
|
+
```abide lib/Invoice.abide
|
|
139
139
|
<script server>
|
|
140
140
|
import { GET, notFound } from '@abide/abide/server'
|
|
141
141
|
|
|
@@ -143,7 +143,7 @@ const INVOICES = new Map([
|
|
|
143
143
|
['42', { number: 'INV-0042', total: '$1,240.00', dueOn: '2026-09-14' }],
|
|
144
144
|
])
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
const getInvoice = GET(
|
|
147
147
|
async ({ id }: { id: string }) => {
|
|
148
148
|
await Bun.sleep(450) // a slow database, so the wait is on screen
|
|
149
149
|
const invoice = INVOICES.get(id)
|
|
@@ -179,7 +179,7 @@ Read on: [Templates](/templates.md) · [Values by name](/templates/read-and-writ
|
|
|
179
179
|
|
|
180
180
|
## Data from the server
|
|
181
181
|
|
|
182
|
-
A handler
|
|
182
|
+
A handler you declare is callable. The method you declare it with is the whole difference between a read and a write.
|
|
183
183
|
|
|
184
184
|
Read on: [Data from the server](/data.md) · [Reading data](/data/read-data-without-writing-an-api.md) · [Mutations](/data/change-something-on-the-server.md) · [Live updates](/data/keep-a-room-of-callers-in-sync.md)
|
|
185
185
|
|
|
@@ -243,7 +243,7 @@ abide has one answer.
|
|
|
243
243
|
|
|
244
244
|
**`Reactive` — four producers, one way to ask** — A contact and its notes. Watch which rows say “…” on arrival, then type a note and add it.
|
|
245
245
|
|
|
246
|
-
```abide Contact.abide
|
|
246
|
+
```abide lib/Contact.abide
|
|
247
247
|
<script server>
|
|
248
248
|
import { GET } from '@abide/abide/server'
|
|
249
249
|
|
|
@@ -251,7 +251,7 @@ const CONTACTS = new Map([
|
|
|
251
251
|
['1', { id: '1', name: 'Ada Lovelace', email: 'ada@example.com' }],
|
|
252
252
|
])
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
const getContact = GET(
|
|
255
255
|
async ({ id }: { id: string }) => {
|
|
256
256
|
await Bun.sleep(900) // a slow database, so the wait is on screen
|
|
257
257
|
const row = CONTACTS.get(id)
|
|
@@ -336,10 +336,8 @@ Address, method, argument and result schemas, description — all of it already
|
|
|
336
336
|
|
|
337
337
|
That is why failures are strict. A refusal is a **named value carrying data**, not a status code and a sentence:
|
|
338
338
|
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
export const join = POST(() => notMember({ group: 'staff' }))
|
|
342
|
-
</script>
|
|
339
|
+
```ts src/groups/join.ts — excerpt
|
|
340
|
+
export default POST(() => notMember({ group: 'staff' }))
|
|
343
341
|
```
|
|
344
342
|
|
|
345
343
|
The caller narrows it by name and gets `.data` typed — your own template and a model on the far side of an HTTP request, identically.
|
|
@@ -365,13 +363,13 @@ The caller narrows it by name and gets `.data` typed — your own template and a
|
|
|
365
363
|
|
|
366
364
|
*Put something on screen, change it, and see where the code that changed it lives.*
|
|
367
365
|
|
|
368
|
-
A page is a file
|
|
366
|
+
A page is a file, and its path below `src` is the address it answers on: `src/index.abide` is `/`, and `src/about.abide` is `/about`. There is no route table to register it in, and no build step to tell about it.
|
|
369
367
|
|
|
370
|
-
```abide src/
|
|
368
|
+
```abide src/index.abide
|
|
371
369
|
<script server>
|
|
372
370
|
import { GET } from '@abide/abide/server'
|
|
373
371
|
|
|
374
|
-
|
|
372
|
+
const getGreeting = GET(() => `Hello from Bun ${Bun.version}`)
|
|
375
373
|
</script>
|
|
376
374
|
|
|
377
375
|
<script>
|
|
@@ -385,30 +383,34 @@ let count = reactive(0)
|
|
|
385
383
|
<button onclick={() => count += 1}>Clicked {count} times</button>
|
|
386
384
|
```
|
|
387
385
|
|
|
388
|
-
*1 file, 15 lines* — and the app serves `/`, with `getGreeting`
|
|
386
|
+
*1 file, 15 lines* — and the app serves `/`, with `getGreeting` a handler only this page calls.
|
|
389
387
|
|
|
390
388
|
One file holds a function that runs on the server, the values, the markup that shows them, and the click that changes one. There is no API between the two halves, no store to declare, no component to register, and no re-render to ask for.
|
|
391
389
|
|
|
392
|
-
##
|
|
390
|
+
## A file's path is the address it answers
|
|
393
391
|
|
|
394
|
-
|
|
392
|
+
A file's path below `src` is its address. Nothing else names a route.
|
|
395
393
|
|
|
396
394
|
| File | Answers on |
|
|
397
395
|
| --- | --- |
|
|
398
|
-
| `src/
|
|
399
|
-
| `src/about
|
|
400
|
-
| `src/invoices/
|
|
401
|
-
| `src/invoices/[id]
|
|
396
|
+
| `src/index.abide` | `/` |
|
|
397
|
+
| `src/about.abide` | `/about` |
|
|
398
|
+
| `src/invoices/index.abide` | `/invoices` |
|
|
399
|
+
| `src/invoices/[id].abide` | `/invoices/42`, with `42` readable as `route.params.id` |
|
|
400
|
+
| `src/invoices/getInvoice.ts` | `GET /invoices/getInvoice`, the handler its default export declares |
|
|
401
|
+
| `src/invoices/lib/Row.abide` | no address: a component the pages import |
|
|
402
402
|
|
|
403
|
-
|
|
403
|
+
An `index.abide` answers its directory's own address, and any other `.abide` adds its name. A `.ts` beside the pages is a handler file, and its name is its last segment.
|
|
404
404
|
|
|
405
|
-
|
|
405
|
+
Code that is not an address goes in one of four directories that never are: `lib` for components and modules, `tests`, `server` and `public`. Anything else beside the pages fails the build, so listing a directory lists its routes. Group an app by subject, and the addresses follow.
|
|
406
406
|
|
|
407
407
|
Read on: [Routes](/pages/add-a-page.md) · [Sub-path mounting](/pages/serve-the-app-under-a-sub-path.md)
|
|
408
408
|
|
|
409
409
|
## `<script server>` runs on the server, `<script>` sets up the page
|
|
410
410
|
|
|
411
|
-
`<script server>` runs on the server alone, once. What it
|
|
411
|
+
`<script server>` runs on the server alone, once. What it declares with `GET` is a handler, and the rest of the file calls it by the name it was declared with. The browser is never sent the block: the build generates a call under the same name in its place, so `Bun.version` is read where Bun is.
|
|
412
|
+
|
|
413
|
+
No block of a `.abide` exports anything. The component is the file's one export, and a handler its `<script server>` declares is called by that file alone.
|
|
412
414
|
|
|
413
415
|
The `<script>` block is the page's setup: ordinary imports, ordinary TypeScript, run once for this instance of the page. Everything after it is the markup, and the file shares one scope — the template resolves names against its two scripts and nothing else.
|
|
414
416
|
|
|
@@ -426,7 +428,7 @@ Inside a `.abide` file, a reactive value is read and written under its own name.
|
|
|
426
428
|
|
|
427
429
|
Naming `count` in an expression **reads** it; the explicit `count()` and `count.set(v)` keep compiling everywhere the sugar does, because the sugar is over those forms rather than instead of them. A `.ts` file — which has no template around it — writes them out:
|
|
428
430
|
|
|
429
|
-
```ts src/
|
|
431
|
+
```ts src/lib/counters.ts
|
|
430
432
|
import { reactive } from '@abide/abide'
|
|
431
433
|
|
|
432
434
|
export const count = reactive(0)
|
|
@@ -441,12 +443,12 @@ Read on: [Local state](/values/show-a-value-that-changes.md) · [Values by name]
|
|
|
441
443
|
|
|
442
444
|
The same page reaches your database through a handler its own `<script server>` declares, and reads it like any other value.
|
|
443
445
|
|
|
444
|
-
```abide src/invoices/[id]
|
|
446
|
+
```abide src/invoices/[id].abide — excerpt
|
|
445
447
|
<script server>
|
|
446
448
|
import { GET } from '@abide/abide/server'
|
|
447
449
|
import { database } from '#src/server/database'
|
|
448
450
|
|
|
449
|
-
|
|
451
|
+
const getInvoice = GET(
|
|
450
452
|
({ id }: { id: string }) => database.invoice.find(id),
|
|
451
453
|
{ description: 'One invoice, by id.' },
|
|
452
454
|
)
|
|
@@ -466,7 +468,7 @@ No `fetch`, no client module, and no second copy of the invoice type. The browse
|
|
|
466
468
|
|
|
467
469
|
Reading `invoice` starts the load, and the document goes out before the row lands: the two values are holes that fill when it does. That is the default, and it is why neither line above has a loading branch in it.
|
|
468
470
|
|
|
469
|
-
|
|
471
|
+
That handler is this page's alone, and no other file can import it. One a second file calls goes in a handler file: `src/invoices/getInvoice.ts`, holding `export default GET(…)`, answers at `/invoices/getInvoice`, and a page imports it like any other module. It is the app's public surface, so the command line and MCP list it too.
|
|
470
472
|
|
|
471
473
|
Read on: [Reading data](/data/read-data-without-writing-an-api.md) · [Loading states](/values/show-a-value-that-isnt-there-yet.md)
|
|
472
474
|
|
|
@@ -508,7 +510,7 @@ A request lands, and bytes start leaving before anything it needs has arrived. T
|
|
|
508
510
|
| | Stage | What runs |
|
|
509
511
|
| --- | --- | --- |
|
|
510
512
|
| 1 | the request arrives | each `middleware` rung the root layout files |
|
|
511
|
-
| 2 | the route resolves | the
|
|
513
|
+
| 2 | the route resolves | the page file at that path, inside its layouts |
|
|
512
514
|
| 3 | the render starts | every load the page reads, all of them at once |
|
|
513
515
|
| 4 | the first bytes leave | the head, then the markup as far as it goes |
|
|
514
516
|
| 5 | the document closes | as soon as the walk is done |
|
|
@@ -547,7 +549,7 @@ Read on: [Auth & principal](/app/know-who-is-calling.md) · [Authorization](/dat
|
|
|
547
549
|
|
|
548
550
|
## 2. The route resolves
|
|
549
551
|
|
|
550
|
-
The path picks
|
|
552
|
+
The path picks the page file at it, and the layouts in the directories above it wrap it. All of that renders into one document, and the root `src/layout.abide` writes it: written inside `<html>`, its `<head>` is the document's head and its `<body>` holds the chrome and the slot every page goes in. The attributes on the two are the document's own, and live as on any element.
|
|
551
553
|
|
|
552
554
|
```abide src/layout.abide — excerpt
|
|
553
555
|
<html lang="en">
|
|
@@ -577,9 +579,9 @@ The total time is the slowest one, not the sum — and the first byte does not w
|
|
|
577
579
|
|
|
578
580
|
The head goes first, then the markup. A read of a value still in flight renders nothing and opens a **sink**, and the hole fills when the value lands. The markup after it keeps going out.
|
|
579
581
|
|
|
580
|
-
```abide src/invoices/[id]
|
|
582
|
+
```abide src/invoices/[id].abide — excerpt
|
|
581
583
|
<script server>
|
|
582
|
-
|
|
584
|
+
const getInvoice = GET(({ id }: { id: string }) =>
|
|
583
585
|
database.invoice.find(id),
|
|
584
586
|
)
|
|
585
587
|
</script>
|
|
@@ -652,7 +654,7 @@ Read on: [Links & navigation](/pages/link-to-another-page.md) · [View transitio
|
|
|
652
654
|
<head>
|
|
653
655
|
<meta charset="utf-8">
|
|
654
656
|
<script type="application/json" data-abide-seed>
|
|
655
|
-
[["GET","/invoices/
|
|
657
|
+
[["GET","/__abide/handlers/invoices/id/getInvoice",{"id":"42"}]]
|
|
656
658
|
</script>
|
|
657
659
|
<script>/* issues each call, stashes each Promise under its key */</script>
|
|
658
660
|
<link rel="stylesheet" href="/_abide/app.7f3c9a1e.css">
|
|
@@ -685,20 +687,26 @@ Those filler scripts are **byte-identical** — every one of them, in every docu
|
|
|
685
687
|
|
|
686
688
|
## The directory a file is in decides where it runs
|
|
687
689
|
|
|
688
|
-
`src/` is the app, and every file in it runs on both sides unless its directory says otherwise. Group it by subject — `src/billing/
|
|
690
|
+
`src/` is the app, and every file in it runs on both sides unless its directory says otherwise. Group it by subject — `src/billing/index.abide`, `src/billing/server`, `src/billing/lib` — and the build enforces the split wherever the directories sit.
|
|
689
691
|
|
|
690
692
|
| Directory | What is in it | Reached by |
|
|
691
693
|
| --- | --- | --- |
|
|
692
|
-
| `src/**/` | addresses: pages, layouts,
|
|
694
|
+
| `src/**/` | addresses: pages, layouts, error pages and handler files | anything; the browser gets a handler's client |
|
|
695
|
+
| `**/lib/**` | components and modules, which are no address | anything |
|
|
693
696
|
| `**/server/**` | code the browser never loads | the server alone; a page importing it is a compile error |
|
|
697
|
+
| `**/tests/**` | the app's tests | `bun test`; never served |
|
|
698
|
+
| `**/public/**` | files answered as they are | a request for one, at its path below the directory |
|
|
699
|
+
|
|
700
|
+
A directory holds addresses and those four alone. Any other file directly in it fails the build, naming the file and the `lib` to move it into, so what a directory lists is what it answers.
|
|
694
701
|
|
|
695
702
|
`#src/…` is the one import alias, mapping onto `src/`. Two files are addresses rather than names, and both are optional:
|
|
696
703
|
|
|
697
704
|
| File | What it is |
|
|
698
705
|
| --- | --- |
|
|
699
706
|
| `src/layout.abide` | the root layout: the document and the chrome every page shares, and the app's own `<script server>` |
|
|
707
|
+
| `src/launcher.abide` | the desktop app's launcher |
|
|
700
708
|
|
|
701
|
-
Read on: [Config](/app/configure-the-app.md) · [Lifecycle](/app/run-code-at-start-and-stop.md)
|
|
709
|
+
Read on: [Routes](/pages/add-a-page.md) · [Config](/app/configure-the-app.md) · [Lifecycle](/app/run-code-at-start-and-stop.md)
|
|
702
710
|
|
|
703
711
|
## Next
|
|
704
712
|
|
|
@@ -827,7 +835,7 @@ The options are `schema`, `transform`, `store`, `identity`, `by`, `ttl`, `thrott
|
|
|
827
835
|
|
|
828
836
|
**`reactive(v)` — a value the page owns** — A contact's handle. Type into the field and watch the heading.
|
|
829
837
|
|
|
830
|
-
```abide OwnedHandle.abide
|
|
838
|
+
```abide lib/OwnedHandle.abide
|
|
831
839
|
<script>
|
|
832
840
|
import { reactive } from '@abide/abide'
|
|
833
841
|
|
|
@@ -853,7 +861,7 @@ Read on: [Values by name](/templates/read-and-write-a-value-by-name.md)
|
|
|
853
861
|
|
|
854
862
|
**`reactive(load)` — the settled value, never the promise** — A contact fetched when the page is built. Reload the page to watch it land again.
|
|
855
863
|
|
|
856
|
-
```abide Record.abide
|
|
864
|
+
```abide lib/Record.abide
|
|
857
865
|
<script server>
|
|
858
866
|
import { refuse } from '@abide/abide'
|
|
859
867
|
import { GET } from '@abide/abide/server'
|
|
@@ -862,7 +870,7 @@ const CONTACTS: Record<string, { name: string; role: string }> = {
|
|
|
862
870
|
'42': { name: 'Ada Lovelace', role: 'Engineer' },
|
|
863
871
|
}
|
|
864
872
|
|
|
865
|
-
|
|
873
|
+
const getContact = GET(
|
|
866
874
|
async (args: { id: string }) => {
|
|
867
875
|
// A slow service, so the wait is on screen.
|
|
868
876
|
await Bun.sleep(900)
|
|
@@ -900,7 +908,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Reloa
|
|
|
900
908
|
|
|
901
909
|
**`reactive(v, { transform })` — what is stored is not what you typed** — A contact's handle, normalized on the way in. Type a name with capitals and a space.
|
|
902
910
|
|
|
903
|
-
```abide TransformHandle.abide
|
|
911
|
+
```abide lib/TransformHandle.abide
|
|
904
912
|
<script>
|
|
905
913
|
import { reactive } from '@abide/abide'
|
|
906
914
|
|
|
@@ -958,7 +966,7 @@ Both take `schema`, `transform`, `store`, `global`, `tags`, `throttle` and `debo
|
|
|
958
966
|
|
|
959
967
|
**`reactive(() => …)` — an auto-tracked derivation** — A deal, and the total that follows its seats. Step the seats.
|
|
960
968
|
|
|
961
|
-
```abide Deal.abide
|
|
969
|
+
```abide lib/Deal.abide
|
|
962
970
|
<script>
|
|
963
971
|
import { reactive } from '@abide/abide'
|
|
964
972
|
|
|
@@ -1007,7 +1015,7 @@ There is one `set` in the design and it is `Reactive`'s, so writing to a derived
|
|
|
1007
1015
|
|
|
1008
1016
|
**`reactive(() => call(args))` — a load the value adopts** — A profile panel behind two tabs. Open the other person, then come back to the first.
|
|
1009
1017
|
|
|
1010
|
-
```abide Profile.abide
|
|
1018
|
+
```abide lib/Profile.abide
|
|
1011
1019
|
<script server>
|
|
1012
1020
|
import { GET } from '@abide/abide/server'
|
|
1013
1021
|
|
|
@@ -1020,7 +1028,7 @@ const PEOPLE: Record<string, { name: string; role: string }> = {
|
|
|
1020
1028
|
const OPENED: Record<string, number> = {}
|
|
1021
1029
|
|
|
1022
1030
|
/** Runs once per request that reaches it, so `opened` counts loads. */
|
|
1023
|
-
|
|
1031
|
+
const getProfile = GET(
|
|
1024
1032
|
async (args: { id: string }) => {
|
|
1025
1033
|
// A slow service, so the wait is on screen.
|
|
1026
1034
|
await Bun.sleep(450)
|
|
@@ -1082,12 +1090,12 @@ Read on: [Reading data](/data/read-data-without-writing-an-api.md) · [Caching](
|
|
|
1082
1090
|
|
|
1083
1091
|
**A write to a derived value stands until the next recompute** — An edit box seeded from a person the server sent. Type into it, then reload.
|
|
1084
1092
|
|
|
1085
|
-
```abide Draft.abide
|
|
1093
|
+
```abide lib/Draft.abide
|
|
1086
1094
|
<script server>
|
|
1087
1095
|
import { GET } from '@abide/abide/server'
|
|
1088
1096
|
|
|
1089
1097
|
/** The same person on every call, so a refill changes no field. */
|
|
1090
|
-
|
|
1098
|
+
const getPerson = GET(
|
|
1091
1099
|
async (args: { id: string }) => {
|
|
1092
1100
|
// A slow service, so the wait is on screen.
|
|
1093
1101
|
await Bun.sleep(450)
|
|
@@ -1135,7 +1143,7 @@ A write of a settled value is not a load — `ready()` stays true and `busy()` n
|
|
|
1135
1143
|
|
|
1136
1144
|
**`reactive((args) => …)` — a factory, one entry per key** — The contact search, filtering as you type. Type toward Ada, then backspace over what you typed.
|
|
1137
1145
|
|
|
1138
|
-
```abide Search.abide
|
|
1146
|
+
```abide lib/Search.abide
|
|
1139
1147
|
<script server>
|
|
1140
1148
|
import { reactive } from '@abide/abide'
|
|
1141
1149
|
import { GET } from '@abide/abide/server'
|
|
@@ -1150,7 +1158,7 @@ const matching = reactive(async (args: { query: string }) => {
|
|
|
1150
1158
|
return PEOPLE.filter((one) => one.toLowerCase().includes(word))
|
|
1151
1159
|
})
|
|
1152
1160
|
|
|
1153
|
-
|
|
1161
|
+
const searchContacts = GET(matching, {
|
|
1154
1162
|
description: 'Contacts whose name contains a query.',
|
|
1155
1163
|
})
|
|
1156
1164
|
</script>
|
|
@@ -1187,7 +1195,7 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Throttle & deb
|
|
|
1187
1195
|
|
|
1188
1196
|
**A read after an `await` registers nothing** — Two counts under one contact filter, differing in one line. Change the filter.
|
|
1189
1197
|
|
|
1190
|
-
```abide Counts.abide
|
|
1198
|
+
```abide lib/Counts.abide
|
|
1191
1199
|
<script>
|
|
1192
1200
|
import { reactive } from '@abide/abide'
|
|
1193
1201
|
|
|
@@ -1239,12 +1247,12 @@ Read on: [Reloading](/values/decide-when-a-value-reloads.md)
|
|
|
1239
1247
|
|
|
1240
1248
|
**`reactive(() => gate ? … : …)` — a branch on a gate that has not landed** — A contact an auditor may read and not edit. Watch the top Save button, and reload the page to see it again.
|
|
1241
1249
|
|
|
1242
|
-
```abide Contact.abide
|
|
1250
|
+
```abide lib/Contact.abide
|
|
1243
1251
|
<script server>
|
|
1244
1252
|
import { GET } from '@abide/abide/server'
|
|
1245
1253
|
|
|
1246
1254
|
/** What this caller may do with the record, which only the server knows. */
|
|
1247
|
-
|
|
1255
|
+
const getViewer = GET(
|
|
1248
1256
|
async () => {
|
|
1249
1257
|
// A slow service, so the wait is on screen.
|
|
1250
1258
|
await Bun.sleep(1600)
|
|
@@ -1277,7 +1285,7 @@ The hole is where the wait shows. `mode === 'locked'` throws the same way, and a
|
|
|
1277
1285
|
|
|
1278
1286
|
Only you know which answer is safe before there is one, so say it with the probe:
|
|
1279
1287
|
|
|
1280
|
-
```abide Contact.abide — excerpt
|
|
1288
|
+
```abide lib/Contact.abide — excerpt
|
|
1281
1289
|
<button disabled={!mode.ready() || mode === 'locked'}>Save</button></li>
|
|
1282
1290
|
```
|
|
1283
1291
|
|
|
@@ -1327,7 +1335,7 @@ Every other option is the one any reactive value takes. `args` refuses a key (11
|
|
|
1327
1335
|
|
|
1328
1336
|
**`reactive<Turn[]>([])` — a channel anything can reach** — A conversation's composer, and the line above it that reports who spoke last. Press Send and watch a component the composer never names.
|
|
1329
1337
|
|
|
1330
|
-
```ts onboarding.ts
|
|
1338
|
+
```ts lib/onboarding.ts
|
|
1331
1339
|
import { reactive } from '@abide/abide'
|
|
1332
1340
|
|
|
1333
1341
|
export type Turn = { id: number; speaker: string; text: string }
|
|
@@ -1336,7 +1344,7 @@ export type Turn = { id: number; speaker: string; text: string }
|
|
|
1336
1344
|
export const onboarding = reactive<Turn[]>([], { by: 'id' })
|
|
1337
1345
|
```
|
|
1338
1346
|
|
|
1339
|
-
```abide Chat.abide
|
|
1347
|
+
```abide lib/Chat.abide
|
|
1340
1348
|
<script>
|
|
1341
1349
|
import { reactive } from '@abide/abide'
|
|
1342
1350
|
import { onboarding } from './onboarding.ts'
|
|
@@ -1359,7 +1367,7 @@ const draft = reactive('How do I rotate an API key?')
|
|
|
1359
1367
|
</div>
|
|
1360
1368
|
```
|
|
1361
1369
|
|
|
1362
|
-
```abide LastSpoken.abide
|
|
1370
|
+
```abide lib/LastSpoken.abide
|
|
1363
1371
|
<script>
|
|
1364
1372
|
import { reactive } from '@abide/abide'
|
|
1365
1373
|
// The same import reaches the same channel. No prop, no provider.
|
|
@@ -1395,7 +1403,7 @@ Read on: [Live updates](/data/keep-a-room-of-callers-in-sync.md) · [Loading sta
|
|
|
1395
1403
|
|
|
1396
1404
|
**`reactive((args) => [])` — a channel per key** — Two conversations behind one declaration. Send something in onboarding, switch to billing, send there, and come back.
|
|
1397
1405
|
|
|
1398
|
-
```abide Conversation.abide
|
|
1406
|
+
```abide lib/Conversation.abide
|
|
1399
1407
|
<script>
|
|
1400
1408
|
import { reactive } from '@abide/abide'
|
|
1401
1409
|
|
|
@@ -1449,7 +1457,7 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Schemas](/data
|
|
|
1449
1457
|
|
|
1450
1458
|
**`patch` with a trim — a channel that keeps three turns** — A transcript pane over a conversation already three turns old. Send a fourth and watch the top of it.
|
|
1451
1459
|
|
|
1452
|
-
```abide TrimmedChat.abide
|
|
1460
|
+
```abide lib/TrimmedChat.abide
|
|
1453
1461
|
<script>
|
|
1454
1462
|
import { reactive } from '@abide/abide'
|
|
1455
1463
|
|
|
@@ -1512,7 +1520,7 @@ Read on: [Recent values](/values/keep-the-last-few-values.md) · [Lists](/templa
|
|
|
1512
1520
|
|
|
1513
1521
|
**`{ transform }` on a channel — a message refused by name** — The composer holding a message longer than the conversation takes. Press Send, then cut it down and press again.
|
|
1514
1522
|
|
|
1515
|
-
```abide LimitedChat.abide
|
|
1523
|
+
```abide lib/LimitedChat.abide
|
|
1516
1524
|
<script>
|
|
1517
1525
|
import { reactive, refuse } from '@abide/abide'
|
|
1518
1526
|
import { z } from 'zod'
|
|
@@ -1589,11 +1597,11 @@ Read on: [Throttle & debounce](/values/slow-down-a-value-that-changes-too-fast.m
|
|
|
1589
1597
|
|
|
1590
1598
|
## Following a channel sees turns, not every message
|
|
1591
1599
|
|
|
1592
|
-
`
|
|
1600
|
+
`onboarding.watch((turns) => …)` follows the channel. The effect runs once at the end of a turn, however many writes landed in it, and a patch in place is one of them (12.10). A burst of three messages in one turn is one run, holding all three.
|
|
1593
1601
|
|
|
1594
|
-
So there is no exactly-once tail of a channel. Code that has to act on every message acts where it writes it — in `say`, beside the push.
|
|
1602
|
+
So there is no exactly-once tail of a channel. Code that has to act on every message acts where it writes it — in `say`, beside the push.
|
|
1595
1603
|
|
|
1596
|
-
|
|
1604
|
+
`for await` is not a follow. Over a value it yields the settled value once and ends, as `await` does (2.5). Over a stream it yields each chunk of the run and ends at the close (2.6), and a capped window still delivers every chunk (5.10).
|
|
1597
1605
|
|
|
1598
1606
|
Read on: [Watching values](/values/do-something-when-a-value-changes.md) · [Streaming data](/data/send-data-as-it-arrives.md)
|
|
1599
1607
|
|
|
@@ -1659,7 +1667,7 @@ A list arrives once and then changes for the rest of the session. Refetching it
|
|
|
1659
1667
|
|
|
1660
1668
|
**`s.patch(mutate)` — change in place, and wake the readers** — A regional dashboard whose order counts arrive one region at a time.
|
|
1661
1669
|
|
|
1662
|
-
```abide Regions.abide
|
|
1670
|
+
```abide lib/Regions.abide
|
|
1663
1671
|
<script>
|
|
1664
1672
|
import { reactive } from '@abide/abide'
|
|
1665
1673
|
|
|
@@ -1716,7 +1724,7 @@ Read on: [Local state](/values/show-a-value-that-changes.md) · [Watching values
|
|
|
1716
1724
|
|
|
1717
1725
|
**`applyOrderCount(byId, …)` — the same fold, reached without `patch`** — The same dashboard, and a helper handed the held table directly.
|
|
1718
1726
|
|
|
1719
|
-
```abide UnpatchedRegions.abide
|
|
1727
|
+
```abide lib/UnpatchedRegions.abide
|
|
1720
1728
|
<script>
|
|
1721
1729
|
import { reactive } from '@abide/abide'
|
|
1722
1730
|
|
|
@@ -1787,7 +1795,7 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md) · [Watch
|
|
|
1787
1795
|
|
|
1788
1796
|
**`reactive(() => rows, { transform })` — an array in, an index held** — The regions table arriving from the server as a list, on a dashboard whose updates name ids.
|
|
1789
1797
|
|
|
1790
|
-
```abide LoadedRegions.abide
|
|
1798
|
+
```abide lib/LoadedRegions.abide
|
|
1791
1799
|
<script server>
|
|
1792
1800
|
import { GET } from '@abide/abide/server'
|
|
1793
1801
|
|
|
@@ -1800,7 +1808,7 @@ const BOOKED: Region[] = [
|
|
|
1800
1808
|
{ id: 'west', name: 'West', orders: 91 },
|
|
1801
1809
|
]
|
|
1802
1810
|
|
|
1803
|
-
|
|
1811
|
+
const getRegions = GET(
|
|
1804
1812
|
async (): Promise<Region[]> => {
|
|
1805
1813
|
// A slow service, so the wait is on screen.
|
|
1806
1814
|
await Bun.sleep(450)
|
|
@@ -1915,7 +1923,7 @@ Two booleans, four states, and the states an interface actually shows are the co
|
|
|
1915
1923
|
|
|
1916
1924
|
**A read starts the load and leaves a hole** — An invoice with no loading branch anywhere in it. Reload the page and watch the three fields fill.
|
|
1917
1925
|
|
|
1918
|
-
```abide Invoice.abide
|
|
1926
|
+
```abide lib/Invoice.abide
|
|
1919
1927
|
<script server>
|
|
1920
1928
|
import { GET } from '@abide/abide/server'
|
|
1921
1929
|
|
|
@@ -1929,7 +1937,7 @@ const INVOICES: Record<string, Invoice> = {
|
|
|
1929
1937
|
},
|
|
1930
1938
|
}
|
|
1931
1939
|
|
|
1932
|
-
|
|
1940
|
+
const getInvoice = GET(
|
|
1933
1941
|
async ({ id }: { id: string }) => {
|
|
1934
1942
|
// A slow database, so the wait is on screen.
|
|
1935
1943
|
await Bun.sleep(900)
|
|
@@ -1977,7 +1985,7 @@ Read on: [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) · [Exp
|
|
|
1977
1985
|
|
|
1978
1986
|
**`NotLanded` — a read before the value arrives** — A contact's email, with a Copy button. Open another contact and press Copy before the address lands.
|
|
1979
1987
|
|
|
1980
|
-
```abide CopyEmail.abide
|
|
1988
|
+
```abide lib/CopyEmail.abide
|
|
1981
1989
|
<script server>
|
|
1982
1990
|
import { GET } from '@abide/abide/server'
|
|
1983
1991
|
|
|
@@ -1987,7 +1995,7 @@ const EMAILS: Record<string, string> = {
|
|
|
1987
1995
|
katherine: 'katherine@example.com',
|
|
1988
1996
|
}
|
|
1989
1997
|
|
|
1990
|
-
|
|
1998
|
+
const getContact = GET(
|
|
1991
1999
|
async ({ id }: { id: string }) => {
|
|
1992
2000
|
// A slow directory, so there is time to press Copy before it lands.
|
|
1993
2001
|
await Bun.sleep(1500)
|
|
@@ -2042,14 +2050,14 @@ Outside the render flow there is no hole to leave, so the throw reaches your cod
|
|
|
2042
2050
|
|
|
2043
2051
|
**The first load and every one after are `ready()` apart** — An invoice with a Refresh beside it. Wait for the figures, then press Refresh and watch what does not disappear.
|
|
2044
2052
|
|
|
2045
|
-
```abide RefreshInvoice.abide
|
|
2053
|
+
```abide lib/RefreshInvoice.abide
|
|
2046
2054
|
<script server>
|
|
2047
2055
|
import { GET } from '@abide/abide/server'
|
|
2048
2056
|
|
|
2049
2057
|
// Counts the loads: the total never moves, so this is what shows a refresh.
|
|
2050
2058
|
let sent = 0
|
|
2051
2059
|
|
|
2052
|
-
|
|
2060
|
+
const getInvoiceTotal = GET(
|
|
2053
2061
|
async (_args: { id: string }) => {
|
|
2054
2062
|
// A slow database, so the wait is on screen.
|
|
2055
2063
|
await Bun.sleep(900)
|
|
@@ -2098,7 +2106,7 @@ Read on: [Reloading](/values/decide-when-a-value-reloads.md)
|
|
|
2098
2106
|
|
|
2099
2107
|
**A derived value is busy while what it reads is** — A count of unpaid invoices, derived from a list still loading. Reload the page and watch the figure before the list lands.
|
|
2100
2108
|
|
|
2101
|
-
```abide Unpaid.abide
|
|
2109
|
+
```abide lib/Unpaid.abide
|
|
2102
2110
|
<script server>
|
|
2103
2111
|
import { GET } from '@abide/abide/server'
|
|
2104
2112
|
|
|
@@ -2108,7 +2116,7 @@ const INVOICES = [
|
|
|
2108
2116
|
{ id: '4023', total: '$76.50', paid: false },
|
|
2109
2117
|
]
|
|
2110
2118
|
|
|
2111
|
-
|
|
2119
|
+
const listInvoices = GET(
|
|
2112
2120
|
async () => {
|
|
2113
2121
|
// A slow database, so the wait is on screen.
|
|
2114
2122
|
await Bun.sleep(900)
|
|
@@ -2150,7 +2158,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md)
|
|
|
2150
2158
|
|
|
2151
2159
|
**`s.error()` hands the failure back rather than throwing** — An invoice whose billing service is down. The page renders the message instead of letting it reach the error page.
|
|
2152
2160
|
|
|
2153
|
-
```abide UnreachableInvoice.abide
|
|
2161
|
+
```abide lib/UnreachableInvoice.abide
|
|
2154
2162
|
<script server>
|
|
2155
2163
|
import { refuse } from '@abide/abide'
|
|
2156
2164
|
import { GET } from '@abide/abide/server'
|
|
@@ -2170,7 +2178,7 @@ const INVOICES: Record<string, { total: string }> = {
|
|
|
2170
2178
|
'4020': { total: '$980.00' },
|
|
2171
2179
|
}
|
|
2172
2180
|
|
|
2173
|
-
|
|
2181
|
+
const getBilledInvoice = GET(
|
|
2174
2182
|
async ({ id }: { id: string }) => {
|
|
2175
2183
|
// A slow billing service, so the wait is on screen.
|
|
2176
2184
|
await Bun.sleep(600)
|
|
@@ -2278,8 +2286,7 @@ Calling it hands back a `Reactive` for that key, so a probe on the result asks a
|
|
|
2278
2286
|
|
|
2279
2287
|
**`reactive((args) => …)` — identity per set of arguments** — Two tiles on one dashboard, each on its own range. Put them both on 30d, then send one back to 7d.
|
|
2280
2288
|
|
|
2281
|
-
```
|
|
2282
|
-
<script server>
|
|
2289
|
+
```ts getMetrics.ts
|
|
2283
2290
|
import { reactive } from '@abide/abide'
|
|
2284
2291
|
import { GET } from '@abide/abide/server'
|
|
2285
2292
|
|
|
@@ -2300,13 +2307,13 @@ const metricsFor = reactive(async ({ range }: { range: string }) => {
|
|
|
2300
2307
|
return { orders: found.orders, average: found.average, computed }
|
|
2301
2308
|
})
|
|
2302
2309
|
|
|
2303
|
-
export
|
|
2310
|
+
export default GET(metricsFor, {
|
|
2304
2311
|
description: 'Metrics for a range.',
|
|
2305
2312
|
})
|
|
2306
|
-
|
|
2313
|
+
```
|
|
2307
2314
|
|
|
2315
|
+
```abide lib/Tiles.abide
|
|
2308
2316
|
<script>
|
|
2309
|
-
import { reactive } from '@abide/abide'
|
|
2310
2317
|
import Average from './Average.abide'
|
|
2311
2318
|
import Total from './Total.abide'
|
|
2312
2319
|
</script>
|
|
@@ -2315,10 +2322,10 @@ import Total from './Total.abide'
|
|
|
2315
2322
|
<Average/>
|
|
2316
2323
|
```
|
|
2317
2324
|
|
|
2318
|
-
```abide Total.abide
|
|
2325
|
+
```abide lib/Total.abide
|
|
2319
2326
|
<script>
|
|
2320
2327
|
import { reactive } from '@abide/abide'
|
|
2321
|
-
import
|
|
2328
|
+
import getMetrics from '../getMetrics.ts'
|
|
2322
2329
|
|
|
2323
2330
|
const RANGES = ['7d', '30d']
|
|
2324
2331
|
|
|
@@ -2344,10 +2351,10 @@ const metrics = reactive(() => getMetrics({ range }))
|
|
|
2344
2351
|
</ul>
|
|
2345
2352
|
```
|
|
2346
2353
|
|
|
2347
|
-
```abide Average.abide
|
|
2354
|
+
```abide lib/Average.abide
|
|
2348
2355
|
<script>
|
|
2349
2356
|
import { reactive } from '@abide/abide'
|
|
2350
|
-
import
|
|
2357
|
+
import getMetrics from '../getMetrics.ts'
|
|
2351
2358
|
|
|
2352
2359
|
const RANGES = ['7d', '30d']
|
|
2353
2360
|
|
|
@@ -2386,7 +2393,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md) · [Reloa
|
|
|
2386
2393
|
|
|
2387
2394
|
**One entry per canonical args key** — One panel, and three ways to spell the query behind it. Change the spelling, then change the region.
|
|
2388
2395
|
|
|
2389
|
-
```abide Query.abide
|
|
2396
|
+
```abide lib/Query.abide
|
|
2390
2397
|
<script server>
|
|
2391
2398
|
import { reactive } from '@abide/abide'
|
|
2392
2399
|
import { GET } from '@abide/abide/server'
|
|
@@ -2406,7 +2413,7 @@ const queryMetricsFor = reactive(async (query: Query) => {
|
|
|
2406
2413
|
return { orders: ORDERS[query.region] ?? 0, computed }
|
|
2407
2414
|
})
|
|
2408
2415
|
|
|
2409
|
-
|
|
2416
|
+
const getQueryMetrics = GET(queryMetricsFor, {
|
|
2410
2417
|
description: 'Metrics for a range and a region.',
|
|
2411
2418
|
})
|
|
2412
2419
|
</script>
|
|
@@ -2499,7 +2506,7 @@ Two costs, and both are yours rather than abide's.
|
|
|
2499
2506
|
|
|
2500
2507
|
And a global value is bounded by its own `ttl` and the app's `invalidate` and by nothing else. There is no byte ceiling and no eviction count: measuring an arbitrary value costs an O(size) walk, and an LRU would evict an entry a reader is mid-flight on. `global: true` with `ttl: Infinity` is an unbounded cache — unbounded because the app asked for it, the bound being a number the app knows and abide does not.
|
|
2501
2508
|
|
|
2502
|
-
A global value over a stream fans one producer out to many readers, and it needs no retention to do it. A reader arriving mid-stream gets the run in flight from its
|
|
2509
|
+
A global value over a stream fans one producer out to many readers, and it needs no retention to do it. A reader arriving mid-stream gets the run in flight from its first chunk to its close (2.6), and a reader leaving does not cancel the producer for the ones that remain (11.39).
|
|
2503
2510
|
|
|
2504
2511
|
Read on: [Recent values](/values/keep-the-last-few-values.md) · [Auth & principal](/app/know-who-is-calling.md) · [Authorization](/data/decide-who-may-call-what.md) · [Response types](/data/answer-with-something-other-than-json.md)
|
|
2505
2512
|
|
|
@@ -2507,7 +2514,7 @@ Read on: [Recent values](/values/keep-the-last-few-values.md) · [Auth & princip
|
|
|
2507
2514
|
|
|
2508
2515
|
**`tags` — a selection that reaches across keyed values** — Two regions, each with revenue and recent orders behind keyed values that never mention one another. Press one region's button and watch the other stand still.
|
|
2509
2516
|
|
|
2510
|
-
```abide Regions.abide
|
|
2517
|
+
```abide lib/Regions.abide
|
|
2511
2518
|
<script>
|
|
2512
2519
|
import Region from './Region.abide'
|
|
2513
2520
|
</script>
|
|
@@ -2516,7 +2523,7 @@ import Region from './Region.abide'
|
|
|
2516
2523
|
<Region region="south"/>
|
|
2517
2524
|
```
|
|
2518
2525
|
|
|
2519
|
-
```abide Region.abide
|
|
2526
|
+
```abide lib/Region.abide
|
|
2520
2527
|
<script server>
|
|
2521
2528
|
import { GET } from '@abide/abide/server'
|
|
2522
2529
|
|
|
@@ -2530,7 +2537,7 @@ const COUNT: Record<string, number> = { north: 128, south: 94 }
|
|
|
2530
2537
|
const computed: Record<string, number> = { north: 0, south: 0 }
|
|
2531
2538
|
const loaded: Record<string, number> = { north: 0, south: 0 }
|
|
2532
2539
|
|
|
2533
|
-
|
|
2540
|
+
const getTotals = GET(
|
|
2534
2541
|
async ({ region }: { region: string }) => {
|
|
2535
2542
|
// A slow database, so the wait is on screen.
|
|
2536
2543
|
await Bun.sleep(450)
|
|
@@ -2543,7 +2550,7 @@ export const getTotals = GET(
|
|
|
2543
2550
|
{ description: 'Order totals for a region.' },
|
|
2544
2551
|
)
|
|
2545
2552
|
|
|
2546
|
-
|
|
2553
|
+
const getOrders = GET(
|
|
2547
2554
|
async ({ region }: { region: string }) => {
|
|
2548
2555
|
await Bun.sleep(450)
|
|
2549
2556
|
loaded[region] = (loaded[region] ?? 0) + 1
|
|
@@ -2637,7 +2644,7 @@ There are three answers, and they are three because they fail differently.
|
|
|
2637
2644
|
|
|
2638
2645
|
**`invalidate()` marks stale; `refresh()` reloads now** — A shelf count with both triggers under it. Press Mark stale and watch nothing happen, then press Reload now.
|
|
2639
2646
|
|
|
2640
|
-
```abide Level.abide
|
|
2647
|
+
```abide lib/Level.abide
|
|
2641
2648
|
<script server>
|
|
2642
2649
|
import { reactive } from '@abide/abide'
|
|
2643
2650
|
import { GET } from '@abide/abide/server'
|
|
@@ -2654,7 +2661,7 @@ const levelOfShelf = reactive(async (_shelf: Shelf) => {
|
|
|
2654
2661
|
return { onHand: 12, counted }
|
|
2655
2662
|
})
|
|
2656
2663
|
|
|
2657
|
-
|
|
2664
|
+
const getLevel = GET(levelOfShelf, {
|
|
2658
2665
|
description: 'Stock on a shelf, by warehouse and sku.',
|
|
2659
2666
|
})
|
|
2660
2667
|
</script>
|
|
@@ -2747,7 +2754,7 @@ There is no bare form wider than the scope. A probe reports and a trigger acts,
|
|
|
2747
2754
|
|
|
2748
2755
|
**`k.refresh(Partial<Args>)` — a slice of the args space** — Two warehouses holding the same shelf. Book one out of east and watch which of the two loads again.
|
|
2749
2756
|
|
|
2750
|
-
```abide Shelves.abide
|
|
2757
|
+
```abide lib/Shelves.abide
|
|
2751
2758
|
<script server>
|
|
2752
2759
|
import { reactive } from '@abide/abide'
|
|
2753
2760
|
import { GET, POST } from '@abide/abide/server'
|
|
@@ -2766,11 +2773,11 @@ const stockForShelf = reactive(async ({ warehouse, sku }: Shelf) => {
|
|
|
2766
2773
|
return { onHand: ON_HAND[shelf] ?? 0, loads: loads[shelf] }
|
|
2767
2774
|
})
|
|
2768
2775
|
|
|
2769
|
-
|
|
2776
|
+
const getStock = GET(stockForShelf, {
|
|
2770
2777
|
description: 'Stock on a shelf, by warehouse and sku.',
|
|
2771
2778
|
})
|
|
2772
2779
|
|
|
2773
|
-
|
|
2780
|
+
const bookOut = POST(
|
|
2774
2781
|
({ warehouse, sku }: Shelf) => {
|
|
2775
2782
|
const shelf = `${warehouse}/${sku}`
|
|
2776
2783
|
ON_HAND[shelf] = (ON_HAND[shelf] ?? 0) - 1
|
|
@@ -2835,7 +2842,7 @@ Read on: [Offline](/app/know-when-the-browser-goes-offline.md) · [Watching valu
|
|
|
2835
2842
|
|
|
2836
2843
|
**`{ ttl }` — expires with nobody asking** — A shelf count that keeps for three seconds. Press Read it again straight away, then wait and press it once more.
|
|
2837
2844
|
|
|
2838
|
-
```abide Shelf.abide
|
|
2845
|
+
```abide lib/Shelf.abide
|
|
2839
2846
|
<script server>
|
|
2840
2847
|
import { reactive } from '@abide/abide'
|
|
2841
2848
|
import { GET } from '@abide/abide/server'
|
|
@@ -2856,7 +2863,7 @@ const stockForShelf = reactive(
|
|
|
2856
2863
|
{ ttl: 3_000 },
|
|
2857
2864
|
)
|
|
2858
2865
|
|
|
2859
|
-
|
|
2866
|
+
const getCachedStock = GET(stockForShelf, {
|
|
2860
2867
|
description: 'Stock on a shelf, by warehouse and sku.',
|
|
2861
2868
|
})
|
|
2862
2869
|
</script>
|
|
@@ -2953,7 +2960,7 @@ Declaring both is a build error naming the value, there being no reading under w
|
|
|
2953
2960
|
|
|
2954
2961
|
**`{ debounce }` — one load once the typing stops** — A figure filtered by a region you type. Clear the box and type north, and watch the figure rather than the field.
|
|
2955
2962
|
|
|
2956
|
-
```abide Orders.abide
|
|
2963
|
+
```abide lib/Orders.abide
|
|
2957
2964
|
<script server>
|
|
2958
2965
|
import { GET } from '@abide/abide/server'
|
|
2959
2966
|
|
|
@@ -2965,7 +2972,7 @@ const BY_REGION: Record<string, number> = {
|
|
|
2965
2972
|
north: 128,
|
|
2966
2973
|
}
|
|
2967
2974
|
|
|
2968
|
-
|
|
2975
|
+
const countOrders = GET(
|
|
2969
2976
|
async (args: { region: string }) => {
|
|
2970
2977
|
// A slow service, so the wait is on screen.
|
|
2971
2978
|
await Bun.sleep(450)
|
|
@@ -3013,7 +3020,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Reloa
|
|
|
3013
3020
|
|
|
3014
3021
|
**`{ throttle }` — the first arrival, then a ceiling** — A feed arriving twenty times a second beside the tile that shows it. Watch the two figures drift apart.
|
|
3015
3022
|
|
|
3016
|
-
```abide Live.abide
|
|
3023
|
+
```abide lib/Live.abide
|
|
3017
3024
|
<script>
|
|
3018
3025
|
import { reactive, watch } from '@abide/abide'
|
|
3019
3026
|
|
|
@@ -3070,13 +3077,13 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Form binding](
|
|
|
3070
3077
|
|
|
3071
3078
|
**`identity: structural` — an equal reload wakes nobody** — Two regional totals and the chart drawn from them. Record a sale and the chart redraws; refresh with nothing new and it does not.
|
|
3072
3079
|
|
|
3073
|
-
```abide Chart.abide
|
|
3080
|
+
```abide lib/Chart.abide
|
|
3074
3081
|
<script server>
|
|
3075
3082
|
import { GET, POST } from '@abide/abide/server'
|
|
3076
3083
|
|
|
3077
3084
|
const totals = { north: 1240, south: 980 }
|
|
3078
3085
|
|
|
3079
|
-
|
|
3086
|
+
const getSales = GET(
|
|
3080
3087
|
async () => {
|
|
3081
3088
|
await Bun.sleep(150)
|
|
3082
3089
|
// A fresh record on every load, the way a query result is.
|
|
@@ -3085,7 +3092,7 @@ export const getSales = GET(
|
|
|
3085
3092
|
{ description: 'Sales by region.' },
|
|
3086
3093
|
)
|
|
3087
3094
|
|
|
3088
|
-
|
|
3095
|
+
const recordSale = POST(
|
|
3089
3096
|
() => {
|
|
3090
3097
|
totals.north += 120
|
|
3091
3098
|
},
|
|
@@ -3174,7 +3181,7 @@ What comes back is whatever the thunk built, which is an ordinary `Reactive` wit
|
|
|
3174
3181
|
|
|
3175
3182
|
**`reactive.share(key, build)` — get-or-create in this scope** — A now-playing bar over a library of tracks. Press a title and watch the bar, which the library never speaks to.
|
|
3176
3183
|
|
|
3177
|
-
```abide Library.abide
|
|
3184
|
+
```abide lib/Library.abide
|
|
3178
3185
|
<script>
|
|
3179
3186
|
import { reactive } from '@abide/abide'
|
|
3180
3187
|
import NowPlaying from './NowPlaying.abide'
|
|
@@ -3195,7 +3202,7 @@ reactive.share('nowPlaying', () => reactive('nothing'))
|
|
|
3195
3202
|
</div>
|
|
3196
3203
|
```
|
|
3197
3204
|
|
|
3198
|
-
```abide NowPlaying.abide
|
|
3205
|
+
```abide lib/NowPlaying.abide
|
|
3199
3206
|
<script>
|
|
3200
3207
|
import { reactive } from '@abide/abide'
|
|
3201
3208
|
|
|
@@ -3208,7 +3215,7 @@ const playing = reactive.share('nowPlaying', () => reactive('nothing'))
|
|
|
3208
3215
|
</ul>
|
|
3209
3216
|
```
|
|
3210
3217
|
|
|
3211
|
-
```abide TrackRow.abide
|
|
3218
|
+
```abide lib/TrackRow.abide
|
|
3212
3219
|
<script>
|
|
3213
3220
|
import { props, reactive } from '@abide/abide'
|
|
3214
3221
|
|
|
@@ -3234,7 +3241,7 @@ Read on: [Components](/templates/reuse-a-piece-of-markup.md) · [Form binding](/
|
|
|
3234
3241
|
|
|
3235
3242
|
**`reactive.share` hands back the one that won** — One key asked for twice, once properly and once with the value built outside the call. Step the volume and watch the third tile.
|
|
3236
3243
|
|
|
3237
|
-
```abide Volume.abide
|
|
3244
|
+
```abide lib/Volume.abide
|
|
3238
3245
|
<script>
|
|
3239
3246
|
import { type Reactive, reactive } from '@abide/abide'
|
|
3240
3247
|
|
|
@@ -3265,7 +3272,7 @@ Building the value outside the call and passing it in fails silently, which is w
|
|
|
3265
3272
|
|
|
3266
3273
|
`Shared` is your app's own registry, widened by declaration merging — and an **opt-in tightening**, not a precondition. A key it declares is checked against it; a key it does not is inferred from the thunk, so `reactive.share` works before you have written one.
|
|
3267
3274
|
|
|
3268
|
-
```ts src/shared.ts — excerpt
|
|
3275
|
+
```ts src/lib/shared.ts — excerpt
|
|
3269
3276
|
declare module '@abide/abide' {
|
|
3270
3277
|
interface Shared {
|
|
3271
3278
|
nowPlaying: Reactive<string>
|
|
@@ -3283,7 +3290,7 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md) · [Live
|
|
|
3283
3290
|
|
|
3284
3291
|
**A shared value lives where it was created, and below** — Two shelves rendered from one component, each with a control below it asking for the same key. Queue on one shelf and watch the other.
|
|
3285
3292
|
|
|
3286
|
-
```abide Shelves.abide
|
|
3293
|
+
```abide lib/Shelves.abide
|
|
3287
3294
|
<script>
|
|
3288
3295
|
import Shelf from './Shelf.abide'
|
|
3289
3296
|
</script>
|
|
@@ -3292,7 +3299,7 @@ import Shelf from './Shelf.abide'
|
|
|
3292
3299
|
<Shelf name="ambient"/>
|
|
3293
3300
|
```
|
|
3294
3301
|
|
|
3295
|
-
```abide Shelf.abide
|
|
3302
|
+
```abide lib/Shelf.abide
|
|
3296
3303
|
<script>
|
|
3297
3304
|
import { props, reactive } from '@abide/abide'
|
|
3298
3305
|
import QueueControl from './QueueControl.abide'
|
|
@@ -3311,7 +3318,7 @@ let queued = reactive.share('queued', () => reactive('nothing'))
|
|
|
3311
3318
|
<QueueControl name={name}/>
|
|
3312
3319
|
```
|
|
3313
3320
|
|
|
3314
|
-
```abide QueueControl.abide
|
|
3321
|
+
```abide lib/QueueControl.abide
|
|
3315
3322
|
<script>
|
|
3316
3323
|
import { props, reactive } from '@abide/abide'
|
|
3317
3324
|
|
|
@@ -3370,7 +3377,7 @@ The effect is an `Effect`, and what it may return is a `Disposer`. Both calls ha
|
|
|
3370
3377
|
|
|
3371
3378
|
**`watch(effect)` — run something outside the page on change** — A contact's handle, and the browser tab that names what you are editing. Type into the field, then press Reset.
|
|
3372
3379
|
|
|
3373
|
-
```abide TabTitle.abide
|
|
3380
|
+
```abide lib/TabTitle.abide
|
|
3374
3381
|
<script>
|
|
3375
3382
|
import { reactive, watch } from '@abide/abide'
|
|
3376
3383
|
|
|
@@ -3412,7 +3419,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md) · [Scrip
|
|
|
3412
3419
|
|
|
3413
3420
|
**`() => Disposer` — each rerun tears down the one before it** — Notes on a contact, saved a beat after you stop typing. Type a few characters quickly and watch the saved line.
|
|
3414
3421
|
|
|
3415
|
-
```abide Autosave.abide
|
|
3422
|
+
```abide lib/Autosave.abide
|
|
3416
3423
|
<script>
|
|
3417
3424
|
import { reactive, watch } from '@abide/abide'
|
|
3418
3425
|
|
|
@@ -3457,7 +3464,7 @@ Read on: [Throttle & debounce](/values/slow-down-a-value-that-changes-too-fast.m
|
|
|
3457
3464
|
|
|
3458
3465
|
**`watch(sources, effect)` — reads both, wakes on one** — An audit line that should follow a reassignment and not every keystroke. Type into the notes, then change the owner.
|
|
3459
3466
|
|
|
3460
|
-
```abide Assignment.abide
|
|
3467
|
+
```abide lib/Assignment.abide
|
|
3461
3468
|
<script>
|
|
3462
3469
|
import { reactive, watch } from '@abide/abide'
|
|
3463
3470
|
|
|
@@ -3536,7 +3543,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md)
|
|
|
3536
3543
|
|
|
3537
3544
|
**`flushSync()` — measure the page the write left** — A contact's notes in a box that scrolls. Add a note — the handler measures the list straight after the write, and lands on the note it just added.
|
|
3538
3545
|
|
|
3539
|
-
```abide Notes.abide
|
|
3546
|
+
```abide lib/Notes.abide
|
|
3540
3547
|
<script>
|
|
3541
3548
|
import { flushSync, reactive } from '@abide/abide'
|
|
3542
3549
|
|
|
@@ -3604,7 +3611,7 @@ The window is an array the value holds. You append to it and trim it in one `pat
|
|
|
3604
3611
|
|
|
3605
3612
|
**`patch` with a trim — a window onto the recent past** — A pane over an answer arriving one token at a time. Press Next token a few times and watch the top of the list.
|
|
3606
3613
|
|
|
3607
|
-
```abide TokenWindow.abide
|
|
3614
|
+
```abide lib/TokenWindow.abide
|
|
3608
3615
|
<script>
|
|
3609
3616
|
import { reactive } from '@abide/abide'
|
|
3610
3617
|
|
|
@@ -3645,7 +3652,7 @@ Read on: [Patching a list](/values/patch-a-list-from-a-live-feed.md) · [Lists](
|
|
|
3645
3652
|
|
|
3646
3653
|
Every `Reactive` holds its standing value and nothing before it. Where you want the ones before it, hold them: make the value the array, and append where you write.
|
|
3647
3654
|
|
|
3648
|
-
A reader cannot build the window for you.
|
|
3655
|
+
A reader cannot build the window for you. A `watch` runs once per turn, and a burst of writes in one turn is one run (12.10), so a window a reader assembles misses whatever landed in the same turn. The writer sees every value, which is why the window is built where the write is.
|
|
3649
3656
|
|
|
3650
3657
|
A channel is this same shape with many writers — a `reactive` over an array that everyone connected reads, trimmed the same way.
|
|
3651
3658
|
|
|
@@ -3655,7 +3662,7 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md)
|
|
|
3655
3662
|
|
|
3656
3663
|
**An undo stack is history the app keeps** — A composer draft with an Undo beside it. Type a few edits, then press Undo more than once.
|
|
3657
3664
|
|
|
3658
|
-
```ts undoStack.ts
|
|
3665
|
+
```ts lib/undoStack.ts
|
|
3659
3666
|
/** An undo is itself a write, so the position lives over a snapshot. */
|
|
3660
3667
|
export function undoStack<T>(limit: number) {
|
|
3661
3668
|
const held: T[] = []
|
|
@@ -3682,7 +3689,7 @@ export function undoStack<T>(limit: number) {
|
|
|
3682
3689
|
}
|
|
3683
3690
|
```
|
|
3684
3691
|
|
|
3685
|
-
```abide Draft.abide
|
|
3692
|
+
```abide lib/Draft.abide
|
|
3686
3693
|
<script>
|
|
3687
3694
|
import { reactive, watch } from '@abide/abide'
|
|
3688
3695
|
import { undoStack } from './undoStack.ts'
|
|
@@ -3758,14 +3765,14 @@ A `Reactive` lives and dies with its scope unless it is told otherwise. A `store
|
|
|
3758
3765
|
|
|
3759
3766
|
**`{ store }` — where the value lives when the process does not** — A volume the player keeps between visits. Step it up, then reload the page.
|
|
3760
3767
|
|
|
3761
|
-
```abide Volume.abide
|
|
3768
|
+
```abide lib/Volume.abide
|
|
3762
3769
|
<script server>
|
|
3763
3770
|
import { GET, POST } from '@abide/abide/server'
|
|
3764
3771
|
|
|
3765
3772
|
// A setting never written answers `null`, the miss.
|
|
3766
3773
|
const SETTINGS: Record<string, string> = {}
|
|
3767
3774
|
|
|
3768
|
-
|
|
3775
|
+
const readSetting = GET(
|
|
3769
3776
|
async ({ name }: { name: string }) => {
|
|
3770
3777
|
// A slow database, so the wait is on screen.
|
|
3771
3778
|
await Bun.sleep(450)
|
|
@@ -3774,7 +3781,7 @@ export const readSetting = GET(
|
|
|
3774
3781
|
{ description: 'A setting, by name.' },
|
|
3775
3782
|
)
|
|
3776
3783
|
|
|
3777
|
-
|
|
3784
|
+
const writeSetting = POST(
|
|
3778
3785
|
(args: { name: string; value: string }) => {
|
|
3779
3786
|
SETTINGS[args.name] = args.value
|
|
3780
3787
|
},
|
|
@@ -3786,7 +3793,7 @@ export const writeSetting = POST(
|
|
|
3786
3793
|
import type { Store } from '@abide/abide'
|
|
3787
3794
|
|
|
3788
3795
|
/** Both halves or neither: `Store` has no spelling for one alone. */
|
|
3789
|
-
|
|
3796
|
+
function settingsStore<T>(name: string): Store<T> {
|
|
3790
3797
|
return {
|
|
3791
3798
|
// Stored as JSON; a setting never written answers `null`, the miss.
|
|
3792
3799
|
get: async () => {
|
|
@@ -3865,7 +3872,7 @@ A deploy that restarts every instance no longer stampedes the thing behind it.
|
|
|
3865
3872
|
|
|
3866
3873
|
**One store per args key, derived from the key** — Two profiles whose settings live at two addresses. Both are read at once, and neither reads back the other's.
|
|
3867
3874
|
|
|
3868
|
-
```abide Profiles.abide
|
|
3875
|
+
```abide lib/Profiles.abide
|
|
3869
3876
|
<script server>
|
|
3870
3877
|
import { reactive, type Store } from '@abide/abide'
|
|
3871
3878
|
import { GET } from '@abide/abide/server'
|
|
@@ -3899,7 +3906,7 @@ const settingsCache = reactive(
|
|
|
3899
3906
|
{ store: ({ profile }) => redisStore<Settings>(`settings:${profile}`) },
|
|
3900
3907
|
)
|
|
3901
3908
|
|
|
3902
|
-
|
|
3909
|
+
const getSettings = GET(settingsCache, {
|
|
3903
3910
|
description: 'The settings of a profile.',
|
|
3904
3911
|
})
|
|
3905
3912
|
</script>
|
|
@@ -3967,9 +3974,9 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md) · [Recen
|
|
|
3967
3974
|
|
|
3968
3975
|
# Data from the server
|
|
3969
3976
|
|
|
3970
|
-
*A handler
|
|
3977
|
+
*A handler is callable, and the method you declare it with is the difference.*
|
|
3971
3978
|
|
|
3972
|
-
A handler
|
|
3979
|
+
A handler you declare is callable. The method you declare it with is the whole difference between a read and a write.
|
|
3973
3980
|
|
|
3974
3981
|
| You declare | Means |
|
|
3975
3982
|
| --- | --- |
|
|
@@ -3977,13 +3984,8 @@ A handler a `<script server>` exports is callable. The method you declare it wit
|
|
|
3977
3984
|
| `POST` / `PUT` / `PATCH` / `DELETE` | a mutation, retaining nothing by default |
|
|
3978
3985
|
| a `GET` over a `global` value, patched by a `POST` | live state every caller follows |
|
|
3979
3986
|
|
|
3980
|
-
```
|
|
3981
|
-
|
|
3982
|
-
export const listInvoices = GET(() => database.invoice.all())
|
|
3983
|
-
export const payInvoice = POST(
|
|
3984
|
-
({ id }: { id: string }) => database.invoice.pay(id),
|
|
3985
|
-
)
|
|
3986
|
-
</script>
|
|
3987
|
+
```ts src/invoices/listInvoices.ts — excerpt
|
|
3988
|
+
export default GET(() => database.invoice.all())
|
|
3987
3989
|
```
|
|
3988
3990
|
|
|
3989
3991
|
## Reading data
|
|
@@ -4068,14 +4070,13 @@ Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
|
|
|
4068
4070
|
|
|
4069
4071
|
You have a database and a page that needs a row. Most stacks make that four artifacts: a handler, a route registration, a client function, and a shared type nothing enforces. Three exist only because the halves are in different processes.
|
|
4070
4072
|
|
|
4071
|
-
|
|
4073
|
+
A `.ts` beside your pages is a handler, and its path is its address. A handler only one file calls can stay in that file instead. Either way, the page calls it by name.
|
|
4072
4074
|
|
|
4073
|
-
## A handler is
|
|
4075
|
+
## A handler is a file's default export
|
|
4074
4076
|
|
|
4075
4077
|
**`GET(fn)` — a handler a page imports** — An invoice read straight out of the database by a component. Reload the page and watch it land.
|
|
4076
4078
|
|
|
4077
|
-
```
|
|
4078
|
-
<script server>
|
|
4079
|
+
```ts getInvoice.ts
|
|
4079
4080
|
import { GET } from '@abide/abide/server'
|
|
4080
4081
|
|
|
4081
4082
|
type InvoiceRow = { number: string; dueOn: string; total: string }
|
|
@@ -4084,8 +4085,8 @@ const INVOICES: Record<string, InvoiceRow> = {
|
|
|
4084
4085
|
'4021': { number: 'INV-4021', dueOn: '2026-09-14', total: '$1,240.00' },
|
|
4085
4086
|
}
|
|
4086
4087
|
|
|
4087
|
-
/** Address from the
|
|
4088
|
-
export
|
|
4088
|
+
/** Address from the file's path; schemas from the annotation. */
|
|
4089
|
+
export default GET(
|
|
4089
4090
|
async ({ id }: { id: string }) => {
|
|
4090
4091
|
await Bun.sleep(700) // a slow database, so the wait is on screen
|
|
4091
4092
|
const row = INVOICES[id]
|
|
@@ -4094,10 +4095,12 @@ export const getInvoice = GET(
|
|
|
4094
4095
|
},
|
|
4095
4096
|
{ description: 'An invoice, by id.' },
|
|
4096
4097
|
)
|
|
4097
|
-
|
|
4098
|
+
```
|
|
4098
4099
|
|
|
4100
|
+
```abide lib/Invoice.abide
|
|
4099
4101
|
<script>
|
|
4100
4102
|
import { reactive } from '@abide/abide'
|
|
4103
|
+
import getInvoice from '../getInvoice.ts'
|
|
4101
4104
|
|
|
4102
4105
|
const invoice = reactive(() => getInvoice({ id: '4021' }))
|
|
4103
4106
|
</script>
|
|
@@ -4109,28 +4112,28 @@ const invoice = reactive(() => getInvoice({ id: '4021' }))
|
|
|
4109
4112
|
</ul>
|
|
4110
4113
|
```
|
|
4111
4114
|
|
|
4112
|
-
*
|
|
4115
|
+
*2 files, 0 API in them* — against a route, a fetch, a client wrapper and a type kept in step by hand.
|
|
4113
4116
|
|
|
4114
|
-
A
|
|
4117
|
+
A handler file runs on the server alone. Put a `.ts` in a directory under `src`, wrap a function in `GET` and make it the default export. `GET` derives the argument and result schemas from the annotations you already wrote.
|
|
4115
4118
|
|
|
4116
|
-
The address is the
|
|
4119
|
+
The address is the file's path, and the file's name is its last segment:
|
|
4117
4120
|
|
|
4118
|
-
| File |
|
|
4119
|
-
| --- | --- |
|
|
4120
|
-
| `src/data/read-data-without-writing-an-api/
|
|
4121
|
-
| `src/users/
|
|
4122
|
-
| `src/
|
|
4121
|
+
| File | Answers at |
|
|
4122
|
+
| --- | --- |
|
|
4123
|
+
| `src/data/read-data-without-writing-an-api/getInvoice.ts` | `/data/read-data-without-writing-an-api/getInvoice` |
|
|
4124
|
+
| `src/users/getUser.ts` | `/users/getUser` |
|
|
4125
|
+
| `src/health.ts` | `/health` |
|
|
4123
4126
|
|
|
4124
|
-
|
|
4127
|
+
One file is one handler, and exporting anything else from it is a build error (16.73). A handler file is the app's API: a page imports it, a script calls its address, and the CLI and MCP offer it as a command and a tool.
|
|
4125
4128
|
|
|
4126
|
-
|
|
4129
|
+
Inside the app you never write that address down. It is there so a log line or a network tab tells you which handler you are looking at.
|
|
4127
4130
|
|
|
4128
4131
|
## A page reaches the handler through an ordinary import
|
|
4129
4132
|
|
|
4130
|
-
```abide src/invoices/[id]/
|
|
4133
|
+
```abide src/invoices/[id]/index.abide — excerpt
|
|
4131
4134
|
<script>
|
|
4132
4135
|
import { reactive, route } from '@abide/abide'
|
|
4133
|
-
import
|
|
4136
|
+
import getInvoice from '../getInvoice.ts'
|
|
4134
4137
|
|
|
4135
4138
|
const invoice = reactive(() => getInvoice({ id: route.params.id }))
|
|
4136
4139
|
</script>
|
|
@@ -4143,23 +4146,65 @@ The import is real — name, argument type and return type are the ones you just
|
|
|
4143
4146
|
|
|
4144
4147
|
The page reads `invoice`, which starts the load. The document goes out immediately with holes where the values are. The holes fill when the row lands.
|
|
4145
4148
|
|
|
4146
|
-
##
|
|
4149
|
+
## A handler only one file calls is declared in that file
|
|
4150
|
+
|
|
4151
|
+
**`const getTerms = GET(fn)` — a handler its own file calls** — An invoice's payment terms, which only this card shows. The handler sits in the card's own server block, and nothing else in the app can import it.
|
|
4152
|
+
|
|
4153
|
+
```abide lib/InvoiceTerms.abide
|
|
4154
|
+
<script server>
|
|
4155
|
+
import { GET } from '@abide/abide/server'
|
|
4156
|
+
|
|
4157
|
+
const TERMS: Record<string, { due: string; lateFee: string }> = {
|
|
4158
|
+
'4021': { due: 'Net 30', lateFee: '1.5% a month' },
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
// No export: this file is its one caller.
|
|
4162
|
+
const getTerms = GET(
|
|
4163
|
+
async ({ id }: { id: string }) => {
|
|
4164
|
+
await Bun.sleep(400) // a slow database, so the wait is on screen
|
|
4165
|
+
const row = TERMS[id]
|
|
4166
|
+
if (row === undefined) throw new Error(`no terms for ${id}`)
|
|
4167
|
+
return row
|
|
4168
|
+
},
|
|
4169
|
+
{ description: "An invoice's payment terms, by id." },
|
|
4170
|
+
)
|
|
4171
|
+
</script>
|
|
4172
|
+
|
|
4173
|
+
<script>
|
|
4174
|
+
import { reactive } from '@abide/abide'
|
|
4175
|
+
|
|
4176
|
+
const terms = reactive(() => getTerms({ id: '4021' }))
|
|
4177
|
+
</script>
|
|
4178
|
+
|
|
4179
|
+
<ul class="fields stacked">
|
|
4180
|
+
<li><span>terms</span><strong>{terms.due}</strong></li>
|
|
4181
|
+
<li><span>late fee</span><strong>{terms.lateFee}</strong></li>
|
|
4182
|
+
</ul>
|
|
4183
|
+
```
|
|
4147
4184
|
|
|
4148
|
-
|
|
4185
|
+
A `<script server>` declares `getTerms` as a `const`, and the same file's `<script>` and template call it by name. A `.abide` exports nothing (34.17), so no other file can reach it.
|
|
4186
|
+
|
|
4187
|
+
It still answers over http — the browser's copy of the card calls it — at its file's path below `/__abide/handlers` and then its name: `/__abide/handlers/data/read-data-without-writing-an-api/lib/InvoiceTerms/getTerms` (16.70). That address is listed on no surface: not OpenAPI, not MCP, not the CLI (16.71). It takes nothing from the path, and it runs under the middleware filed at this page's route, the directory holding the card's `lib` (16.72).
|
|
4188
|
+
|
|
4189
|
+
When a second file needs it, or a script does, move it into a handler file. The call is unchanged: `const getTerms =` becomes `export default`, and the file's name becomes its address.
|
|
4190
|
+
|
|
4191
|
+
## The browser receives a generated client, not your server code
|
|
4192
|
+
|
|
4193
|
+
The browser never sees a handler file or a `<script server>`. Importing a handler file into a page hands the browser a **generated** call in its place (16.74), and a `.abide`'s own copy carries one per handler its server block declares (16.70). Each carries:
|
|
4149
4194
|
|
|
4150
4195
|
- the HTTP method
|
|
4151
4196
|
- the mount-relative address
|
|
4152
4197
|
- the description
|
|
4153
4198
|
|
|
4154
|
-
Nothing else. Your database import and the
|
|
4199
|
+
Nothing else. Your database import and the module-level work around the handler were never part of the module the browser got — so "no server code shipped" is how the build works, not something an optimizer is trusted to have achieved.
|
|
4155
4200
|
|
|
4156
|
-
|
|
4201
|
+
Import a module under a `server` directory into a page and you get a compile error naming it. Never a stub that silently does nothing.
|
|
4157
4202
|
|
|
4158
4203
|
## You get a `Reactive`, not a promise
|
|
4159
4204
|
|
|
4160
4205
|
**A call hands back a `Reactive`, not a promise** — An invoice and the reminder button that must not fire before it lands. Wait for the total, then press Reload.
|
|
4161
4206
|
|
|
4162
|
-
```abide InvoiceReminder.abide
|
|
4207
|
+
```abide lib/InvoiceReminder.abide
|
|
4163
4208
|
<script server>
|
|
4164
4209
|
import { GET } from '@abide/abide/server'
|
|
4165
4210
|
|
|
@@ -4167,7 +4212,7 @@ const INVOICES: Record<string, { total: string }> = {
|
|
|
4167
4212
|
'4021': { total: '$1,240.00' },
|
|
4168
4213
|
}
|
|
4169
4214
|
|
|
4170
|
-
|
|
4215
|
+
const getInvoiceTotal = GET(
|
|
4171
4216
|
async ({ id }: { id: string }) => {
|
|
4172
4217
|
await Bun.sleep(700) // a slow database, so the wait is on screen
|
|
4173
4218
|
const row = INVOICES[id]
|
|
@@ -4207,7 +4252,7 @@ See [Loading states](/values/show-a-value-that-isnt-there-yet.md).
|
|
|
4207
4252
|
|
|
4208
4253
|
**A `GET` over a `global` value follows the server** — The open deals, and a button that moves one on the server. Open this page in a second tab and press it in either: both move, and nothing on the page reloads the list.
|
|
4209
4254
|
|
|
4210
|
-
```abide Deals.abide
|
|
4255
|
+
```abide lib/Deals.abide
|
|
4211
4256
|
<script server>
|
|
4212
4257
|
import { GET, POST, reactive } from '@abide/abide/server'
|
|
4213
4258
|
|
|
@@ -4222,11 +4267,11 @@ const pipeline = reactive(
|
|
|
4222
4267
|
{ global: true },
|
|
4223
4268
|
)
|
|
4224
4269
|
|
|
4225
|
-
|
|
4270
|
+
const openDeals = GET(pipeline, {
|
|
4226
4271
|
description: 'The open deals, and the stage each is at.',
|
|
4227
4272
|
})
|
|
4228
4273
|
|
|
4229
|
-
|
|
4274
|
+
const advanceDeal = POST(
|
|
4230
4275
|
({ id }: { id: string }) => {
|
|
4231
4276
|
pipeline.patch((deals) => {
|
|
4232
4277
|
const deal = deals.find((each) => each.id === id)
|
|
@@ -4261,9 +4306,9 @@ A browser that reads a `GET` stays subscribed to it over one web socket the whol
|
|
|
4261
4306
|
The first read and the socket are two paths, and a change landing between them would be lost. So the answer names its version as an ordinary `ETag`, the browser names that version when it subscribes, and the server sends the value only if it has moved since (18.6). The same `ETag` serves any reader: a `GET` carrying `If-None-Match` with the version it holds is answered `304 Not
|
|
4262
4307
|
Modified` and no body (18.19), so `curl` and a browser's cache revalidate the way the page does.
|
|
4263
4308
|
|
|
4264
|
-
The follow itself is standard too.
|
|
4265
|
-
|
|
4266
|
-
|
|
4309
|
+
The follow itself is standard too. `openDeals` is the card's own, so its address is one no listing names. To hand the follow to a script, make it a handler file — `src/deals/openDeals.ts`, answering at `/deals/openDeals` — with the pipeline in a module under `server` that it and `advanceDeal` both import. Ask that address for `text/event-stream` and it is a stream of server-sent events: `event: value` with the value and its version as the event `id`, then `event: patch` with each change as JSON Patch (18.20). An `EventSource` reconnecting sends the last `id` back as `Last-Event-ID` and is sent only what moved after it.
|
|
4310
|
+
|
|
4311
|
+
So `curl -N -H 'Accept: text/event-stream'` follows a value with nothing abide-shaped on its side, and `abide watch` does the same from a shell.
|
|
4267
4312
|
|
|
4268
4313
|
A trigger reaches browsers too, for any `GET`: `getContact.invalidate({ id })` inside a handler marks every browser's copy of that contact stale, whether or not the value is `global`.
|
|
4269
4314
|
|
|
@@ -4275,16 +4320,14 @@ The `reactive` a handler becomes is not ceremony. A keyed value is identity per
|
|
|
4275
4320
|
|
|
4276
4321
|
Name the caching yourself in the same object as the description:
|
|
4277
4322
|
|
|
4278
|
-
```
|
|
4279
|
-
|
|
4280
|
-
export const getInvoiceTotals = GET(
|
|
4323
|
+
```ts src/invoices/getInvoiceTotals.ts — excerpt
|
|
4324
|
+
export default GET(
|
|
4281
4325
|
({ id }: { id: string }) => database.invoice.totals(id),
|
|
4282
4326
|
{
|
|
4283
4327
|
ttl: 30_000,
|
|
4284
4328
|
description: "One invoice's totals, by id. Cached for 30 seconds.",
|
|
4285
4329
|
},
|
|
4286
4330
|
)
|
|
4287
|
-
</script>
|
|
4288
4331
|
```
|
|
4289
4332
|
|
|
4290
4333
|
`GET` accepts three things:
|
|
@@ -4333,20 +4376,20 @@ A read is safe to repeat. A write is not — and a browser repeats one for you:
|
|
|
4333
4376
|
| `PATCH(handler, options?)` | the same, for a partial change |
|
|
4334
4377
|
| `DELETE(handler, options?)` | a mutation whose arguments travel in the URL |
|
|
4335
4378
|
|
|
4336
|
-
Each takes exactly what [`GET`](/data/read-data-without-writing-an-api.md) takes — a `reactive`, a channel, or a plain function that becomes a `reactive` —
|
|
4379
|
+
Each takes exactly what [`GET`](/data/read-data-without-writing-an-api.md) takes — a `reactive`, a channel, or a plain function that becomes a `reactive` — answers where a `GET` would, and accepts the same options. The method is the whole per-method difference, and what it changes is retention, how the arguments travel, and whether a browser may cache the answer. Every signature is in the [Transports](/reference/transports.md) reference.
|
|
4337
4380
|
|
|
4338
4381
|
## Two presses inside one flight are one write
|
|
4339
4382
|
|
|
4340
4383
|
**`POST(handler)` — a write the browser cannot repeat** — An invoice being paid. Press Pay twice, quickly — one payment lands on the record, not two.
|
|
4341
4384
|
|
|
4342
|
-
```abide PayInvoice.abide
|
|
4385
|
+
```abide lib/PayInvoice.abide
|
|
4343
4386
|
<script server>
|
|
4344
4387
|
import { POST } from '@abide/abide/server'
|
|
4345
4388
|
|
|
4346
4389
|
// The writes that landed: a double press must not bill twice.
|
|
4347
4390
|
let payments = 0
|
|
4348
4391
|
|
|
4349
|
-
|
|
4392
|
+
const payInvoice = POST(
|
|
4350
4393
|
async ({ id }: { id: string }) => {
|
|
4351
4394
|
await Bun.sleep(1500) // a slow database, so the wait is on screen
|
|
4352
4395
|
payments += 1
|
|
@@ -4392,14 +4435,12 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Failures](/dat
|
|
|
4392
4435
|
|
|
4393
4436
|
Where a write genuinely must not run twice in a period, declare the `ttl` on the `reactive` and hand that to the method:
|
|
4394
4437
|
|
|
4395
|
-
```
|
|
4396
|
-
<script server>
|
|
4438
|
+
```ts src/invoices/remindAboutInvoice.ts — excerpt
|
|
4397
4439
|
const sendReminder = reactive(({ id }: { id: string }) => mailer.remind(id), {
|
|
4398
4440
|
ttl: 3_600_000,
|
|
4399
4441
|
})
|
|
4400
4442
|
|
|
4401
|
-
export
|
|
4402
|
-
</script>
|
|
4443
|
+
export default POST(sendReminder)
|
|
4403
4444
|
```
|
|
4404
4445
|
|
|
4405
4446
|
One reminder per invoice per hour, whoever asks and however often.
|
|
@@ -4414,7 +4455,7 @@ The shapes wanted the repair anyway. A read and a write have different `Args`.
|
|
|
4414
4455
|
|
|
4415
4456
|
**`PATCH`, `PUT` and `DELETE` — the verb says what the write does** — A contact's phone changed, a tag taken off, the tags put back: three writes, each declared with the method that names it.
|
|
4416
4457
|
|
|
4417
|
-
```abide Contact.abide
|
|
4458
|
+
```abide lib/Contact.abide
|
|
4418
4459
|
<script server>
|
|
4419
4460
|
import { DELETE, GET, PATCH, PUT } from '@abide/abide/server'
|
|
4420
4461
|
|
|
@@ -4442,23 +4483,23 @@ const contacts = {
|
|
|
4442
4483
|
},
|
|
4443
4484
|
}
|
|
4444
4485
|
|
|
4445
|
-
|
|
4486
|
+
const getContact = GET(({ id }: { id: string }) => contacts.find(id), {
|
|
4446
4487
|
description: 'A contact, by id.',
|
|
4447
4488
|
})
|
|
4448
4489
|
|
|
4449
|
-
|
|
4490
|
+
const updatePhone = PATCH(
|
|
4450
4491
|
({ id, phone }: { id: string; phone: string }) =>
|
|
4451
4492
|
contacts.update(id, { phone }),
|
|
4452
4493
|
{ description: 'Changes the phone of a contact.' },
|
|
4453
4494
|
)
|
|
4454
4495
|
|
|
4455
|
-
|
|
4496
|
+
const replaceTags = PUT(
|
|
4456
4497
|
({ id, tags }: { id: string; tags: string[] }) =>
|
|
4457
4498
|
contacts.update(id, { tags }),
|
|
4458
4499
|
{ description: 'Replaces the tags of a contact.' },
|
|
4459
4500
|
)
|
|
4460
4501
|
|
|
4461
|
-
|
|
4502
|
+
const removeTag = DELETE(
|
|
4462
4503
|
async ({ id, tag }: { id: string; tag: string }) => {
|
|
4463
4504
|
const { tags } = await contacts.find(id)
|
|
4464
4505
|
return contacts.update(id, {
|
|
@@ -4517,16 +4558,18 @@ Three body encodings are accepted, which lets a plain `<form>` reach the same ad
|
|
|
4517
4558
|
| `multipart/form-data` | a form with a file in it |
|
|
4518
4559
|
| `application/x-www-form-urlencoded` | a form without one |
|
|
4519
4560
|
|
|
4520
|
-
## A
|
|
4561
|
+
## A write answers where its file says
|
|
4521
4562
|
|
|
4522
|
-
|
|
4563
|
+
Both cards declare their writes in their own `<script server>`, so each write is that card's alone. `payInvoice` answers at `/__abide/handlers/data/change-something-on-the-server/lib/PayInvoice/payInvoice`, listed on no surface, and runs under this page's middleware (16.70, 16.71, 16.72). A write only the button beside it sends belongs there.
|
|
4523
4564
|
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
|
4527
|
-
|
|
|
4565
|
+
A write another page, a script or an agent sends is a handler file, and answers at its path. Nesting nests the address, and nothing else changes:
|
|
4566
|
+
|
|
4567
|
+
| File | Answers at |
|
|
4568
|
+
| --- | --- |
|
|
4569
|
+
| `src/invoices/payInvoice.ts` | `/invoices/payInvoice` |
|
|
4570
|
+
| `src/admin/recent.ts` | `/admin/recent` |
|
|
4528
4571
|
|
|
4529
|
-
An address answers each method once.
|
|
4572
|
+
An address answers each method once. A page `src/admin/recent.abide` beside a `GET` in `recent.ts` answers at the same address, and that is a build error naming both files rather than whichever one won at the first request. A `POST` at a page's address is another method, and is the form that page posts to.
|
|
4530
4573
|
|
|
4531
4574
|
Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
|
|
4532
4575
|
|
|
@@ -4595,7 +4638,7 @@ Read on: [Response types](/data/answer-with-something-other-than-json.md)
|
|
|
4595
4638
|
|
|
4596
4639
|
**`rpc.isError(e, name)` — the refusal, matched by name** — Three invoices on one account. Open the second and the third — each is refused for a different reason.
|
|
4597
4640
|
|
|
4598
|
-
```abide AccountInvoice.abide
|
|
4641
|
+
```abide lib/AccountInvoice.abide
|
|
4599
4642
|
<script server>
|
|
4600
4643
|
import { refuse } from '@abide/abide'
|
|
4601
4644
|
import { GET } from '@abide/abide/server'
|
|
@@ -4624,7 +4667,7 @@ const INVOICES: Record<string, Row> = {
|
|
|
4624
4667
|
'4312': { total: '$512.00', mine: true, owner: 'ada', replacedBy: '4313' },
|
|
4625
4668
|
}
|
|
4626
4669
|
|
|
4627
|
-
|
|
4670
|
+
const getInvoice = GET(
|
|
4628
4671
|
async ({ id }: { id: string }) => {
|
|
4629
4672
|
await Bun.sleep(450) // a slow database, so the wait is on screen
|
|
4630
4673
|
const invoice = INVOICES[id]
|
|
@@ -4685,7 +4728,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Condi
|
|
|
4685
4728
|
|
|
4686
4729
|
**`s.isError(e, name)` — a refusal the page made itself** — A contact's phone, normalized on the way in. Take a digit out and press Save.
|
|
4687
4730
|
|
|
4688
|
-
```abide Phone.abide
|
|
4731
|
+
```abide lib/Phone.abide
|
|
4689
4732
|
<script>
|
|
4690
4733
|
import { reactive, refuse } from '@abide/abide'
|
|
4691
4734
|
import { z } from 'zod'
|
|
@@ -4788,11 +4831,11 @@ Read on: [Schemas](/data/check-what-callers-send-you.md)
|
|
|
4788
4831
|
|
|
4789
4832
|
**`aborted` — what a load reads once every caller stopped waiting** — A contact's year statement from a slow ledger. Load it and cancel before it lands.
|
|
4790
4833
|
|
|
4791
|
-
```abide Statement.abide
|
|
4834
|
+
```abide lib/Statement.abide
|
|
4792
4835
|
<script server>
|
|
4793
4836
|
import { GET } from '@abide/abide/server'
|
|
4794
4837
|
|
|
4795
|
-
|
|
4838
|
+
const getStatement = GET(
|
|
4796
4839
|
async ({ id }: { id: string }) => {
|
|
4797
4840
|
// A slow ledger, so there is time to cancel.
|
|
4798
4841
|
await Bun.sleep(2500)
|
|
@@ -4895,7 +4938,7 @@ That is the default framing and not the only one.
|
|
|
4895
4938
|
|
|
4896
4939
|
**`sse(values)` — events, whoever asks** — A wallboard by the sales floor reads the visits feed. It sends no Accept header, and what comes back is events anyway.
|
|
4897
4940
|
|
|
4898
|
-
```abide VisitsFeed.abide
|
|
4941
|
+
```abide lib/VisitsFeed.abide
|
|
4899
4942
|
<script server>
|
|
4900
4943
|
import { GET, sse } from '@abide/abide/server'
|
|
4901
4944
|
|
|
@@ -4913,7 +4956,7 @@ async function* latestVisits() {
|
|
|
4913
4956
|
}
|
|
4914
4957
|
}
|
|
4915
4958
|
|
|
4916
|
-
|
|
4959
|
+
const visits = GET(() => sse(latestVisits()), {
|
|
4917
4960
|
description: 'Live visits, as server-sent events.',
|
|
4918
4961
|
})
|
|
4919
4962
|
</script>
|
|
@@ -4951,7 +4994,7 @@ Read on: [Response types](/data/answer-with-something-other-than-json.md)
|
|
|
4951
4994
|
|
|
4952
4995
|
**`jsonl(values)` — a list sent one value per line** — The warehouse loads yesterday's sales a line at a time. The database hands back an array, and the export still goes out as lines.
|
|
4953
4996
|
|
|
4954
|
-
```abide WarehouseExport.abide
|
|
4997
|
+
```abide lib/WarehouseExport.abide
|
|
4955
4998
|
<script server>
|
|
4956
4999
|
import { GET, jsonl } from '@abide/abide/server'
|
|
4957
5000
|
|
|
@@ -4965,7 +5008,7 @@ const BY_DAY: Record<string, Sale[]> = {
|
|
|
4965
5008
|
],
|
|
4966
5009
|
}
|
|
4967
5010
|
|
|
4968
|
-
|
|
5011
|
+
const salesForWarehouse = GET(
|
|
4969
5012
|
async ({ day }: { day: string }) => {
|
|
4970
5013
|
// Read whole: the export is a list before it is a stream.
|
|
4971
5014
|
await Bun.sleep(400)
|
|
@@ -5004,7 +5047,7 @@ An array is a value, so returned bare it goes out as one JSON array (16.30) —
|
|
|
5004
5047
|
|
|
5005
5048
|
**`bytes(values)` — a file written as its rows are read** — Sales by region as a CSV download. Each line goes into the file as the cursor reaches its row.
|
|
5006
5049
|
|
|
5007
|
-
```abide SalesCsv.abide
|
|
5050
|
+
```abide lib/SalesCsv.abide
|
|
5008
5051
|
<script server>
|
|
5009
5052
|
import { bytes, GET } from '@abide/abide/server'
|
|
5010
5053
|
|
|
@@ -5027,7 +5070,7 @@ async function* csvOf(year: number): AsyncGenerator<Uint8Array> {
|
|
|
5027
5070
|
}
|
|
5028
5071
|
}
|
|
5029
5072
|
|
|
5030
|
-
|
|
5073
|
+
const salesCsv = GET(
|
|
5031
5074
|
({ year }: { year: number }) => bytes(csvOf(year)),
|
|
5032
5075
|
{ description: 'Sales by region for a year, as CSV.' },
|
|
5033
5076
|
)
|
|
@@ -5071,7 +5114,7 @@ One input type across `page`, `jsonl`, `sse` and `bytes`. A source that is alrea
|
|
|
5071
5114
|
|
|
5072
5115
|
**`{#for row of call()}` — the rows, and the whole of them** — Sales per region, a row at a time. Watch the list fill while the count still has nothing to say.
|
|
5073
5116
|
|
|
5074
|
-
```abide RegionReport.abide
|
|
5117
|
+
```abide lib/RegionReport.abide
|
|
5075
5118
|
<script server>
|
|
5076
5119
|
import { GET } from '@abide/abide/server'
|
|
5077
5120
|
|
|
@@ -5083,7 +5126,7 @@ const BY_YEAR: Record<string, { name: string; total: string }[]> = {
|
|
|
5083
5126
|
],
|
|
5084
5127
|
}
|
|
5085
5128
|
|
|
5086
|
-
|
|
5129
|
+
const salesByRegion = GET(
|
|
5087
5130
|
async function* ({ year }: { year: number }) {
|
|
5088
5131
|
for (const row of BY_YEAR[String(year)] ?? []) {
|
|
5089
5132
|
await Bun.sleep(1200) // a slow database: each row lands alone
|
|
@@ -5133,7 +5176,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Lists
|
|
|
5133
5176
|
|
|
5134
5177
|
## A reader arriving mid-run replays the run
|
|
5135
5178
|
|
|
5136
|
-
There is no retention option on the transport and none is owed. What a late reader needs is the answer being built, and a
|
|
5179
|
+
There is no retention option on the transport and none is owed. What a late reader needs is the answer being built, and a `for await` over the stream hands it the run in flight from its first chunk to its close (2.6). So a reader arriving halfway through still paints every row.
|
|
5137
5180
|
|
|
5138
5181
|
## A stream is not a channel
|
|
5139
5182
|
|
|
@@ -5141,7 +5184,7 @@ Both change more than once, and how the producer produces separates them. A chan
|
|
|
5141
5184
|
|
|
5142
5185
|
What they share is keying. A `{#for}` over a stream's accumulation is positional unless something names a key (32.13) — the block's own `by`, or a `by` the value declares (32.32) — and a row with an identity of its own says which:
|
|
5143
5186
|
|
|
5144
|
-
```abide src/reports/
|
|
5187
|
+
```abide src/reports/index.abide — excerpt
|
|
5145
5188
|
{#for region of salesByRegion({ year: 2026 }) by region.code}
|
|
5146
5189
|
<tr><td>{region.name}</td><td>{region.total}</td></tr>
|
|
5147
5190
|
{/for}
|
|
@@ -5179,8 +5222,8 @@ Nothing on the page polls and nothing on it opens a connection. A page that read
|
|
|
5179
5222
|
| You write | What it is |
|
|
5180
5223
|
| --- | --- |
|
|
5181
5224
|
| `reactive(fn, { global: true })` | the value every caller shares, one per args key where `fn` declares a parameter |
|
|
5182
|
-
| `
|
|
5183
|
-
| `
|
|
5225
|
+
| `const room = GET(rooms)` | its address — a browser reading it follows it |
|
|
5226
|
+
| `const say = POST(…)` | the way in, a handler that patches the value |
|
|
5184
5227
|
|
|
5185
5228
|
`global: true` puts the value in one process-wide cache that outlives every request (11.34). Leave it out and the value is request-local on a server (11.32), so every caller gets one of their own and nobody hears anybody. Every signature is in the [Transports](/reference/transports.md) reference, and what a channel is before it has an address is [Channels](/values/let-anything-publish-and-anything-read.md).
|
|
5186
5229
|
|
|
@@ -5188,7 +5231,7 @@ Nothing on the page polls and nothing on it opens a connection. A page that read
|
|
|
5188
5231
|
|
|
5189
5232
|
**`GET` + `POST` — your room, every tab** — A support room per browser: a `GET` every tab follows and a `POST` that adds a turn. Open this page in a second tab, send from either, and the last turn changes in both — and in nobody else's.
|
|
5190
5233
|
|
|
5191
|
-
```abide SupportRoom.abide
|
|
5234
|
+
```abide lib/SupportRoom.abide
|
|
5192
5235
|
<script server>
|
|
5193
5236
|
import { reactive } from '@abide/abide'
|
|
5194
5237
|
import { GET, POST } from '@abide/abide/server'
|
|
@@ -5208,10 +5251,10 @@ const rooms = reactive(
|
|
|
5208
5251
|
)
|
|
5209
5252
|
|
|
5210
5253
|
/** Read, and followed by every tab reading it. */
|
|
5211
|
-
|
|
5254
|
+
const room = GET(rooms, { description: "A support room's turns." })
|
|
5212
5255
|
|
|
5213
5256
|
/** The server writes the turn, so what a tab sends is only its text. */
|
|
5214
|
-
|
|
5257
|
+
const say = POST(
|
|
5215
5258
|
({ id, text }: { id: string; text: string }) => {
|
|
5216
5259
|
rooms({ id }).patch((turns) => {
|
|
5217
5260
|
turns.push({ id: crypto.randomUUID(), speaker: 'you', text })
|
|
@@ -5263,7 +5306,7 @@ Each turn carries an id the server gives it, and the room declares `by: 'id'`. A
|
|
|
5263
5306
|
`say` takes `{ id, text }` and nothing else. The turn's id, its speaker and anything else it carries are written by the handler, so a caller can only ever add a turn that says what they typed. The example writes `'you'`; an app with sign-in writes who is signed in:
|
|
5264
5307
|
|
|
5265
5308
|
```ts server
|
|
5266
|
-
|
|
5309
|
+
const say = POST(({ id, text }: { id: string; text: string }) => {
|
|
5267
5310
|
const { name } = principal.resolved()
|
|
5268
5311
|
rooms({ id }).patch((turns) => {
|
|
5269
5312
|
turns.push({ id: crypto.randomUUID(), speaker: name, text })
|
|
@@ -5286,7 +5329,7 @@ Read on: [Mutations](/data/change-something-on-the-server.md) · [Auth & princip
|
|
|
5286
5329
|
A room anyone can follow is a room anyone can read. Put a rung on the `GET`:
|
|
5287
5330
|
|
|
5288
5331
|
```ts server
|
|
5289
|
-
|
|
5332
|
+
const room = GET(rooms, {
|
|
5290
5333
|
middleware: [
|
|
5291
5334
|
async (next, ctx) =>
|
|
5292
5335
|
(await isMember(ctx.request, ctx.args())) ? next(ctx) : refuse(403),
|
|
@@ -5313,11 +5356,11 @@ A `patch` on the room is sent to every follower standing at the version it was m
|
|
|
5313
5356
|
|
|
5314
5357
|
A write to a followed copy stays in that tab — a `say` is how a turn reaches the room — and lasts until the server next changes the room.
|
|
5315
5358
|
|
|
5316
|
-
The same follow is open to anything that speaks http.
|
|
5359
|
+
The same follow is open to anything that speaks http, from a handler file. The card's `room` is its own, at an address no listing names; to hand the room to a script, put the `GET` in `src/support/room.ts` as its default export, with `rooms` in a module under `server` that it and `say` both import. Ask `/support/room` for `text/event-stream` and it is server-sent events, each frame's op its `event`, its version the `id`, and the value or the JSON Patch its `data` (18.20):
|
|
5317
5360
|
|
|
5318
5361
|
```
|
|
5319
5362
|
curl -N -H 'Accept: text/event-stream' \
|
|
5320
|
-
'https://app.example/
|
|
5363
|
+
'https://app.example/support/room?id=lobby'
|
|
5321
5364
|
```
|
|
5322
5365
|
|
|
5323
5366
|
An `EventSource` reconnecting sends the last `id` back as `Last-Event-ID`, and is sent only what moved after it.
|
|
@@ -5343,7 +5386,7 @@ The call hands back the same `Reactive` the handler had, `Produced` included (16
|
|
|
5343
5386
|
|
|
5344
5387
|
Writing to that copy is not how to send. A `set` on it stands only until the next production replaces it (9.4), so a turn goes through `say`.
|
|
5345
5388
|
|
|
5346
|
-
`room.invalidate({ id })` and `room.refresh({ id })` inside a handler reach every browser's copy of that room (7.10, 7.18). A `
|
|
5389
|
+
`room.invalidate({ id })` and `room.refresh({ id })` inside a handler reach every browser's copy of that room (7.10, 7.18). A `watch` on the room runs once per turn, a burst collapsing to the latest (12.10), so code that has to act on every message acts in `say`.
|
|
5347
5390
|
|
|
5348
5391
|
## Each instance holds its own room
|
|
5349
5392
|
|
|
@@ -5386,8 +5429,7 @@ Deriving from an annotation means the build resolves types, so build time is typ
|
|
|
5386
5429
|
|
|
5387
5430
|
That is the trade "the shape is never declared twice" is bought with, and the repair is an option at each **position**. `args` gates the arguments a keyed value is called with:
|
|
5388
5431
|
|
|
5389
|
-
```
|
|
5390
|
-
<script server>
|
|
5432
|
+
```ts src/invoices/createInvoice.ts — excerpt
|
|
5391
5433
|
import { z } from 'zod'
|
|
5392
5434
|
|
|
5393
5435
|
const invoices = reactive(
|
|
@@ -5395,13 +5437,12 @@ const invoices = reactive(
|
|
|
5395
5437
|
{ args: z.object({ lines: z.array(lineSchema).min(1) }) },
|
|
5396
5438
|
)
|
|
5397
5439
|
|
|
5398
|
-
export
|
|
5399
|
-
</script>
|
|
5440
|
+
export default POST(invoices)
|
|
5400
5441
|
```
|
|
5401
5442
|
|
|
5402
5443
|
`transform` shapes what gets stored:
|
|
5403
5444
|
|
|
5404
|
-
```ts src/ledger.ts — excerpt
|
|
5445
|
+
```ts src/lib/ledger.ts — excerpt
|
|
5405
5446
|
const paidIds = reactive(() => listInvoices({ year }), {
|
|
5406
5447
|
transform: (all) => new Set(all.filter((r) => r.paid).map((r) => r.id)),
|
|
5407
5448
|
})
|
|
@@ -5419,7 +5460,7 @@ Either option pins a shape you will not have widened, and either is where a cons
|
|
|
5419
5460
|
|
|
5420
5461
|
**`reactive(v, { schema })` — a check with no server in it** — A contact's email. Take the dot out of the domain, and watch the record rather than the field.
|
|
5421
5462
|
|
|
5422
|
-
```abide Email.abide
|
|
5463
|
+
```abide lib/Email.abide
|
|
5423
5464
|
<script>
|
|
5424
5465
|
import { reactive } from '@abide/abide'
|
|
5425
5466
|
import { z } from 'zod'
|
|
@@ -5477,7 +5518,7 @@ A JSON Schema document is the **native** shape, and it enters as the Standard fo
|
|
|
5477
5518
|
|
|
5478
5519
|
**`jsonSchema(document)` — a JSON Schema document as a gate** — The seats on a deal, capped at fifty by a JSON Schema document. Press + past the cap and the deal keeps what it had.
|
|
5479
5520
|
|
|
5480
|
-
```abide Seats.abide
|
|
5521
|
+
```abide lib/Seats.abide
|
|
5481
5522
|
<script>
|
|
5482
5523
|
import { jsonSchema, reactive } from '@abide/abide'
|
|
5483
5524
|
|
|
@@ -5511,7 +5552,7 @@ A document is data, so something has to read it, and `jsonSchema` is what brings
|
|
|
5511
5552
|
|
|
5512
5553
|
**`Issues<T>` — a message under the field it belongs to** — The invoice search. Empty the query, or ask for four — each message lands under its own field.
|
|
5513
5554
|
|
|
5514
|
-
```abide InvoiceSearch.abide
|
|
5555
|
+
```abide lib/InvoiceSearch.abide
|
|
5515
5556
|
<script server>
|
|
5516
5557
|
import { reactive } from '@abide/abide'
|
|
5517
5558
|
import { GET } from '@abide/abide/server'
|
|
@@ -5540,7 +5581,7 @@ const matching = reactive(
|
|
|
5540
5581
|
},
|
|
5541
5582
|
)
|
|
5542
5583
|
|
|
5543
|
-
|
|
5584
|
+
const searchInvoices = GET(matching, {
|
|
5544
5585
|
description: 'Invoices matching a query.',
|
|
5545
5586
|
})
|
|
5546
5587
|
</script>
|
|
@@ -5602,7 +5643,7 @@ Read on: [OpenAPI](/machines/describe-your-api-without-writing-a-spec.md)
|
|
|
5602
5643
|
|
|
5603
5644
|
**`validateJson(schema, value)` — the issues, with no value to gate** — A contact row pasted from a partner's export, checked before it is imported. Fix the plan and the seats, and watch each issue leave its own field.
|
|
5604
5645
|
|
|
5605
|
-
```abide PastedRow.abide
|
|
5646
|
+
```abide lib/PastedRow.abide
|
|
5606
5647
|
<script>
|
|
5607
5648
|
import { reactive, validateJson } from '@abide/abide'
|
|
5608
5649
|
|
|
@@ -5698,7 +5739,7 @@ Read on: [Failures](/data/refuse-a-request-and-say-why.md)
|
|
|
5698
5739
|
|
|
5699
5740
|
**`reactive(fn, { args })` — the gate refuses, the arguments key** — One invoice, spelled two ways. Open the second spelling, then go back to the first.
|
|
5700
5741
|
|
|
5701
|
-
```abide InvoiceKey.abide
|
|
5742
|
+
```abide lib/InvoiceKey.abide
|
|
5702
5743
|
<script server>
|
|
5703
5744
|
import { reactive } from '@abide/abide'
|
|
5704
5745
|
import { GET } from '@abide/abide/server'
|
|
@@ -5720,7 +5761,7 @@ const invoiceById = reactive(
|
|
|
5720
5761
|
{ args: z.object({ id: z.string().toLowerCase() }) },
|
|
5721
5762
|
)
|
|
5722
5763
|
|
|
5723
|
-
|
|
5764
|
+
const getInvoice = GET(invoiceById, {
|
|
5724
5765
|
description: 'An invoice, by id.',
|
|
5725
5766
|
})
|
|
5726
5767
|
</script>
|
|
@@ -5760,7 +5801,7 @@ In channels keyed by their args the same cost is sharper: two spellings of a cha
|
|
|
5760
5801
|
|
|
5761
5802
|
A channel is an array, so its `schema` checks the whole array on every `set` — the app's own included, because validity is not a question about who is asking:
|
|
5762
5803
|
|
|
5763
|
-
```ts src/channels.ts — excerpt
|
|
5804
|
+
```ts src/lib/channels.ts — excerpt
|
|
5764
5805
|
export const announcements = reactive<Post[]>([], {
|
|
5765
5806
|
schema: z.array(messageSchema),
|
|
5766
5807
|
global: true,
|
|
@@ -5771,7 +5812,7 @@ What it does not see is a `patch`. An append through `patch` mutates the array i
|
|
|
5771
5812
|
|
|
5772
5813
|
A channel keyed by its args also has a **channel key**, and that is the parameter its body declares, so it is `args`. The key picks which channel while the messages flow:
|
|
5773
5814
|
|
|
5774
|
-
```ts src/channels.ts — excerpt
|
|
5815
|
+
```ts src/lib/channels.ts — excerpt
|
|
5775
5816
|
export const thread = reactive((_args: { id: string }): Post[] => [], {
|
|
5776
5817
|
args: z.object({ id: z.string().min(1) }),
|
|
5777
5818
|
schema: z.array(messageSchema),
|
|
@@ -5814,12 +5855,12 @@ type Middleware<Ctx, Result> = (
|
|
|
5814
5855
|
|
|
5815
5856
|
| Lane | `Ctx` | Result | Runs |
|
|
5816
5857
|
| --- | --- | --- | --- |
|
|
5817
|
-
| app | `{ request }` | `Response` |
|
|
5858
|
+
| app | `{ request }` | `Response` | over every request its file's route answers, before routing at the root |
|
|
5818
5859
|
| rpc | `{ request, args }` | `Value \| Failures` | on every call to that handler |
|
|
5819
5860
|
|
|
5820
|
-
Two lanes, one type. `Ctx` is a record naming what that lane actually **has** — the app lane runs
|
|
5861
|
+
Two lanes, one type. `Ctx` is a record naming what that lane actually **has** — the app lane runs around a whole route rather than one handler, so it has a request and no arguments; the rpc lane is typed to one handler's own arguments.
|
|
5821
5862
|
|
|
5822
|
-
The **Result** column is the difference that matters. The rpc lane hands back what the handler would have handed back, not a `Response`, and that lets the same rungs run on an **in-process** call. A page rendering on the server reads through the handler, so a rung that authorizes on an id in the arguments cannot be reachable by a browser and missed by the render. The app lane stays `Response`-shaped because
|
|
5863
|
+
The **Result** column is the difference that matters. The rpc lane hands back what the handler would have handed back, not a `Response`, and that lets the same rungs run on an **in-process** call. A page rendering on the server reads through the handler, so a rung that authorizes on an id in the arguments cannot be reachable by a browser and missed by the render. The app lane stays `Response`-shaped because what it wraps is a whole answer rather than a value, which also makes it the lane where a rung that must set a header belongs.
|
|
5823
5864
|
|
|
5824
5865
|
A rung's refusals join the handler's own in what the caller can narrow, so `rpc.isError(error, 'NotYours')` reaches a name the rung declared and never the handler.
|
|
5825
5866
|
|
|
@@ -5850,7 +5891,7 @@ A rung that wants to observe both — a timer, a log line — calls `next` insid
|
|
|
5850
5891
|
|
|
5851
5892
|
**`middleware(rung)` — one rung in front of a route's answers** — A CRM behind a load balancer, and the question of which instance answered. Ask, and read a header the handler never wrote.
|
|
5852
5893
|
|
|
5853
|
-
```abide Ping.abide
|
|
5894
|
+
```abide lib/Ping.abide
|
|
5854
5895
|
<script server>
|
|
5855
5896
|
import { GET, middleware } from '@abide/abide/server'
|
|
5856
5897
|
|
|
@@ -5864,7 +5905,7 @@ middleware(async (next) => {
|
|
|
5864
5905
|
})
|
|
5865
5906
|
|
|
5866
5907
|
/** Writes no header of its own. */
|
|
5867
|
-
|
|
5908
|
+
const ping = GET(() => 'pong', { description: 'Answers pong.' })
|
|
5868
5909
|
</script>
|
|
5869
5910
|
|
|
5870
5911
|
<script>
|
|
@@ -5886,11 +5927,15 @@ let servedBy = reactive('—')
|
|
|
5886
5927
|
```
|
|
5887
5928
|
|
|
5888
5929
|
`middleware(...)` is a hook you **call** from a `<script
|
|
5889
|
-
server
|
|
5930
|
+
server>` or a handler file, and it runs over the route of the file that called it and every route below. The root layout's rungs run before routing, over every request. A section's `layout.abide` files rungs that run after routing, over that section's pages and handlers alone — which is where a guard on `/admin` goes.
|
|
5931
|
+
|
|
5932
|
+
The card's file sits in this page's `lib`, so its rung is filed at this page's route (37.1). `ping` is the card's private handler, and its address, `/__abide/handlers/data/decide-who-may-call-what/lib/Ping/ping`, is below no route of this page's. The rung runs over it anyway, because a private handler runs under the middleware filed where its file's hooks are (16.72). No handler wrote the header.
|
|
5933
|
+
|
|
5934
|
+
So a guard on a section covers every private handler its files declare, wherever their addresses point. Filed in the root layout, the same rung would stamp every answer the app gives — a page, a built asset, the health check. `abide routes` lists each hook with the route it is filed at.
|
|
5890
5935
|
|
|
5891
5936
|
Rungs compose in registration order. The call hands back the way off again, so a module that is torn down can remove its own.
|
|
5892
5937
|
|
|
5893
|
-
|
|
5938
|
+
Around a route this lane has a request and nothing else. Anything resource-aware — authorizing by an id in the arguments — is necessarily a handler's own rung, which is where `ctx.args()` exists.
|
|
5894
5939
|
|
|
5895
5940
|
Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [CSP](/app/lock-down-what-the-page-may-load.md)
|
|
5896
5941
|
|
|
@@ -5898,7 +5943,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [CSP](/app/lock-down
|
|
|
5898
5943
|
|
|
5899
5944
|
**`middleware: [rung]` — a check that runs before the parse** — The recent invoices. Ask for nine while signed out, then sign in and ask again.
|
|
5900
5945
|
|
|
5901
|
-
```abide Recent.abide
|
|
5946
|
+
```abide lib/Recent.abide
|
|
5902
5947
|
<script server>
|
|
5903
5948
|
import { principal, refuse } from '@abide/abide'
|
|
5904
5949
|
import { GET, POST } from '@abide/abide/server'
|
|
@@ -5911,7 +5956,7 @@ const INVOICES = [
|
|
|
5911
5956
|
{ id: '4023', total: '$76.50' },
|
|
5912
5957
|
]
|
|
5913
5958
|
|
|
5914
|
-
|
|
5959
|
+
const searchInvoices = GET(
|
|
5915
5960
|
async ({ limit }: { limit: number }) => {
|
|
5916
5961
|
await Bun.sleep(450) // a slow database, so the wait is on screen
|
|
5917
5962
|
return INVOICES.slice(0, limit)
|
|
@@ -5929,11 +5974,11 @@ export const searchInvoices = GET(
|
|
|
5929
5974
|
},
|
|
5930
5975
|
)
|
|
5931
5976
|
|
|
5932
|
-
|
|
5977
|
+
const signIn = POST(() => principal.set({ id: 'ada' }), {
|
|
5933
5978
|
description: 'Signs in as Ada.',
|
|
5934
5979
|
})
|
|
5935
5980
|
|
|
5936
|
-
|
|
5981
|
+
const signOut = POST(() => principal.clear(), {
|
|
5937
5982
|
description: 'Signs out.',
|
|
5938
5983
|
})
|
|
5939
5984
|
</script>
|
|
@@ -6003,6 +6048,8 @@ Read on: [CORS](/data/let-another-origin-call-you.md) · [Health](/app/tell-a-lo
|
|
|
6003
6048
|
|
|
6004
6049
|
`clients` withholds a handler from a **surface** — an OpenAPI listing, an MCP tool list. It is not access control: the http address answers exactly as it did. This rung decides who **may** call, the same one that decides it for a browser.
|
|
6005
6050
|
|
|
6051
|
+
A private handler is the same. It is listed nowhere and still answers http at its address, so the rungs over its file are what guard it.
|
|
6052
|
+
|
|
6006
6053
|
Read on: [Withholding a surface](/machines/keep-a-handler-off-a-surface.md) · [Auth & principal](/app/know-who-is-calling.md)
|
|
6007
6054
|
|
|
6008
6055
|
## Next
|
|
@@ -6131,7 +6178,7 @@ Read on: [Mutations](/data/change-something-on-the-server.md)
|
|
|
6131
6178
|
|
|
6132
6179
|
**`timeout` — milliseconds without progress, not since the first byte** — A sales report streaming a row a second. North takes three seconds under a two-second bound; south has stopped producing.
|
|
6133
6180
|
|
|
6134
|
-
```abide SalesReport.abide
|
|
6181
|
+
```abide lib/SalesReport.abide
|
|
6135
6182
|
<script server>
|
|
6136
6183
|
import { GET } from '@abide/abide/server'
|
|
6137
6184
|
|
|
@@ -6145,7 +6192,7 @@ const BY_REGION: Record<string, { name: string; total: string }[]> = {
|
|
|
6145
6192
|
south: [{ name: 'week 1', total: '$2,140.00' }],
|
|
6146
6193
|
}
|
|
6147
6194
|
|
|
6148
|
-
|
|
6195
|
+
const salesByRegion = GET(
|
|
6149
6196
|
async function* ({ region }: { region: string }) {
|
|
6150
6197
|
for (const row of BY_REGION[region] ?? []) yield row
|
|
6151
6198
|
// The stall the per-chunk timeout is against, on one region only.
|
|
@@ -6264,7 +6311,7 @@ There is one form and no options. It is a server name, imported from `@abide/abi
|
|
|
6264
6311
|
|
|
6265
6312
|
**`reachable(host)` — ask once, refuse at once** — Invoice 4310, and a billing service that has stopped answering. Send it twice — the first send waits out the check, the second is refused before it leaves.
|
|
6266
6313
|
|
|
6267
|
-
```abide SendInvoice.abide
|
|
6314
|
+
```abide lib/SendInvoice.abide
|
|
6268
6315
|
<script server>
|
|
6269
6316
|
import { POST, reachable, refuse } from '@abide/abide/server'
|
|
6270
6317
|
|
|
@@ -6283,7 +6330,7 @@ function billing(): string {
|
|
|
6283
6330
|
return billingOrigin
|
|
6284
6331
|
}
|
|
6285
6332
|
|
|
6286
|
-
|
|
6333
|
+
const syncInvoice = POST(
|
|
6287
6334
|
async ({ id }: { id: string }) => {
|
|
6288
6335
|
const origin = billing()
|
|
6289
6336
|
const asked = performance.now()
|
|
@@ -6390,13 +6437,11 @@ A binary decodes back to a `Uint8Array` on the caller's side, so an rpc that han
|
|
|
6390
6437
|
|
|
6391
6438
|
A `Blob` has `size` and `slice`, which is the whole of what range serving needs — so abide reads `Range`, honors `If-Range` against the ETag it derives, and answers `206` with `content-range`, `416` where the range is unsatisfiable and `304` on a matching `If-None-Match`. `Bun.file(path)` IS a `Blob`, so serving a file that seeks is returning one:
|
|
6392
6439
|
|
|
6393
|
-
```
|
|
6394
|
-
|
|
6395
|
-
export const playFile = GET(async ({ id }: { id: number }) => {
|
|
6440
|
+
```ts src/media/playFile.ts — excerpt
|
|
6441
|
+
export default GET(async ({ id }: { id: number }) => {
|
|
6396
6442
|
const path = await pathFor(id)
|
|
6397
6443
|
return Bun.file(path)
|
|
6398
6444
|
})
|
|
6399
|
-
</script>
|
|
6400
6445
|
```
|
|
6401
6446
|
|
|
6402
6447
|
*0 lines of range parsing* — against the sixty every media server writes once and gets `content-length` on a `206` wrong the first time, a player that will not seek being the only symptom. Reach it from markup with [`playFile.url({ id })`](/data/find-the-url-a-handler-answers-on.md).
|
|
@@ -6409,7 +6454,7 @@ Read on: [Reading data](/data/read-data-without-writing-an-api.md) · [Streaming
|
|
|
6409
6454
|
|
|
6410
6455
|
**`json(value)` — `null` for nobody, not an empty answer** — A sync script reads each contact's account manager as JSON. Grace has none — switch to her.
|
|
6411
6456
|
|
|
6412
|
-
```abide AccountManager.abide
|
|
6457
|
+
```abide lib/AccountManager.abide
|
|
6413
6458
|
<script server>
|
|
6414
6459
|
import { GET, json } from '@abide/abide/server'
|
|
6415
6460
|
|
|
@@ -6418,7 +6463,7 @@ const MANAGERS: Record<string, { name: string } | undefined> = {
|
|
|
6418
6463
|
'4022': undefined,
|
|
6419
6464
|
}
|
|
6420
6465
|
|
|
6421
|
-
|
|
6466
|
+
const accountManager = GET(
|
|
6422
6467
|
async ({ id }: { id: string }) => {
|
|
6423
6468
|
await Bun.sleep(300) // a slow database, so the wait is on screen
|
|
6424
6469
|
return json(MANAGERS[id])
|
|
@@ -6466,7 +6511,7 @@ A bare `undefined` is a 204 with no body (16.31), which is right for a write wit
|
|
|
6466
6511
|
|
|
6467
6512
|
**`page(body)` — an invoice as a document of its own** — The invoice exactly as it was issued, kept as the HTML the customer was sent.
|
|
6468
6513
|
|
|
6469
|
-
```abide IssuedInvoice.abide
|
|
6514
|
+
```abide lib/IssuedInvoice.abide
|
|
6470
6515
|
<script server>
|
|
6471
6516
|
import { GET, page } from '@abide/abide/server'
|
|
6472
6517
|
|
|
@@ -6501,7 +6546,7 @@ Analytical Engines Ltd · issued 2026-09-14 · due 2026-10-14</p>
|
|
|
6501
6546
|
</html>`,
|
|
6502
6547
|
}
|
|
6503
6548
|
|
|
6504
|
-
|
|
6549
|
+
const issuedInvoice = GET(
|
|
6505
6550
|
async ({ id }: { id: string }) => {
|
|
6506
6551
|
await Bun.sleep(300) // a slow database, so the wait is on screen
|
|
6507
6552
|
const html = ISSUED[id]
|
|
@@ -6524,7 +6569,7 @@ export const issuedInvoice = GET(
|
|
|
6524
6569
|
|
|
6525
6570
|
**`redirect(to)` — one address, sent on to another** — Every contact's avatar has one address that never changes. It sends the browser on to the photo's current version.
|
|
6526
6571
|
|
|
6527
|
-
```abide Avatar.abide
|
|
6572
|
+
```abide lib/Avatar.abide
|
|
6528
6573
|
<script server>
|
|
6529
6574
|
import { GET, redirect } from '@abide/abide/server'
|
|
6530
6575
|
|
|
@@ -6542,7 +6587,7 @@ function contactOf(id: string) {
|
|
|
6542
6587
|
return contact
|
|
6543
6588
|
}
|
|
6544
6589
|
|
|
6545
|
-
|
|
6590
|
+
const photo = GET(
|
|
6546
6591
|
({ id, version }: { id: string; version: number }) => {
|
|
6547
6592
|
const { initials, color } = contactOf(id)
|
|
6548
6593
|
const svg = [
|
|
@@ -6560,7 +6605,7 @@ export const photo = GET(
|
|
|
6560
6605
|
{ description: 'A contact photo, by id and version.' },
|
|
6561
6606
|
)
|
|
6562
6607
|
|
|
6563
|
-
|
|
6608
|
+
const avatar = GET(
|
|
6564
6609
|
({ id }: { id: string }) =>
|
|
6565
6610
|
redirect(photo.url({ id, version: contactOf(id).photoVersion })),
|
|
6566
6611
|
{ description: 'Redirects to the current photo of a contact.' },
|
|
@@ -6627,7 +6672,7 @@ Read on: [Mutations](/data/change-something-on-the-server.md) · [Health](/app/t
|
|
|
6627
6672
|
|
|
6628
6673
|
## An app route answers with the same helpers
|
|
6629
6674
|
|
|
6630
|
-
`src/layout.abide`
|
|
6675
|
+
A `middleware` rung the root `src/layout.abide` files runs before routing, so it can answer an address no page, handler or public file has, and it uses this same set — there is no second one. Calling `next()` hands the request on.
|
|
6631
6676
|
|
|
6632
6677
|
Read on: [Lifecycle](/app/run-code-at-start-and-stop.md)
|
|
6633
6678
|
|
|
@@ -6647,7 +6692,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md)
|
|
|
6647
6692
|
|
|
6648
6693
|
Inside the app you never write an address down: you import the handler and call it. Something outside the app cannot — a payment provider that needs a webhook URL, a `<form action>`, a colleague who wants a `curl` line.
|
|
6649
6694
|
|
|
6650
|
-
|
|
6695
|
+
A handler file's address is its path, so the string is derived rather than typed. Move the file and the address moves with it, there being no path written down to miss. `abide routes` prints every one.
|
|
6651
6696
|
|
|
6652
6697
|
## The four members an `Rpc` carries
|
|
6653
6698
|
|
|
@@ -6661,6 +6706,49 @@ The handler carries the address, so the string is derived rather than typed. Ren
|
|
|
6661
6706
|
None of the four is a call to the handler. They describe the address rather than reaching it, which is what makes them the surface for everything outside the app — a `<form
|
|
6662
6707
|
action>`, an `<img src>`, a webhook registration, a `curl` line. Every signature is in the [Transports](/reference/transports.md) reference.
|
|
6663
6708
|
|
|
6709
|
+
## The address is the handler file's path
|
|
6710
|
+
|
|
6711
|
+
| Handler file | Answers at |
|
|
6712
|
+
| --- | --- |
|
|
6713
|
+
| `src/users/getUser.ts` | `/users/getUser` |
|
|
6714
|
+
| `src/admin/recent.ts` | `/admin/recent` |
|
|
6715
|
+
| `src/media/[id]/save.ts` | `/media/42/save`, with `id` in its args |
|
|
6716
|
+
|
|
6717
|
+
The directories are the address and the file's name is its last segment, so the address is readable from the source tree — which is what a log line or a network panel is for. A parameter in the route reaches the handler's args from the path, so its `Args` declares it — one it leaves out is a build error naming the parameter (16.67).
|
|
6718
|
+
|
|
6719
|
+
The address is not a thing to write out. The route table is a build artifact, and two files that answer one method at one address are a build error naming both.
|
|
6720
|
+
|
|
6721
|
+
## A private handler answers below `/__abide/handlers`
|
|
6722
|
+
|
|
6723
|
+
| Declared in | Answers at |
|
|
6724
|
+
| --- | --- |
|
|
6725
|
+
| `addComment` in `src/media/[id]/lib/Comments.abide` | `/__abide/handlers/media/id/lib/Comments/addComment` |
|
|
6726
|
+
| `markPaid` in `src/crm/[id].abide` | `/__abide/handlers/crm/id/markPaid` |
|
|
6727
|
+
|
|
6728
|
+
A handler a `<script server>` declares is its file's alone, and its address says which file: the file's path without `.abide`, each segment's brackets and dots dropped, then the handler's name (16.70). It takes nothing from the path, so `markPaid` under `[id]` is one address for every id, and the id travels in its args.
|
|
6729
|
+
|
|
6730
|
+
That address is for reading, in a log line or a network panel. It is listed on no surface (16.71), so it is not one to hand to a webhook or a colleague. The file can still put `markPaid.url()` in a `<form action>` of its own. Anything outside the file gets a handler file instead.
|
|
6731
|
+
|
|
6732
|
+
## `abide routes` prints every address
|
|
6733
|
+
|
|
6734
|
+
In an app with a page per contact, a handler file and two private handlers, it prints each address with its method and the file answering it, and each hook with the route it is filed at (38.108):
|
|
6735
|
+
|
|
6736
|
+
```
|
|
6737
|
+
GET / index.abide
|
|
6738
|
+
POST /__abide/handlers/crm/id/markPaid crm/[id].abide (private)
|
|
6739
|
+
POST /__abide/handlers/crm/lib/Notes/addNote crm/lib/Notes.abide (private)
|
|
6740
|
+
GET /crm crm/index.abide
|
|
6741
|
+
GET /crm/[id] crm/[id].abide
|
|
6742
|
+
GET /crm/getInvoice crm/getInvoice.ts
|
|
6743
|
+
|
|
6744
|
+
hooks
|
|
6745
|
+
middleware /crm crm/layout.abide
|
|
6746
|
+
```
|
|
6747
|
+
|
|
6748
|
+
Each private handler is marked. Read its file against the hooks: neither address is below `/crm`, and the `middleware` filed there runs over both anyway, their files being under `crm` (16.72). `abide routes --json` prints the same as one document.
|
|
6749
|
+
|
|
6750
|
+
Read on: [Authorization](/data/decide-who-may-call-what.md)
|
|
6751
|
+
|
|
6664
6752
|
## `rpc.url` is derived, not baked
|
|
6665
6753
|
|
|
6666
6754
|
The generated wrapper carries a **mount-relative** address. `url` resolves it at read time:
|
|
@@ -6676,7 +6764,7 @@ A mount is chosen after the build — `APP_URL` of `https://abide.com/v2` serves
|
|
|
6676
6764
|
|
|
6677
6765
|
An address without its arguments is not something a browser can fetch, so the call takes them. They are written in the same canonical wire form the request itself uses, rather than a query string you assemble and the server parses back differently:
|
|
6678
6766
|
|
|
6679
|
-
```abide src/media/
|
|
6767
|
+
```abide src/media/index.abide — excerpt
|
|
6680
6768
|
<video src={playFile.url({ id: file.id })} controls></video>
|
|
6681
6769
|
<img src={getPoster.url({ path, width: 250 })} alt="">
|
|
6682
6770
|
```
|
|
@@ -6691,17 +6779,6 @@ The method the handler was declared with, as a string. Reading it rather than wr
|
|
|
6691
6779
|
|
|
6692
6780
|
Read on: [Mutations](/data/change-something-on-the-server.md)
|
|
6693
6781
|
|
|
6694
|
-
## The address comes from the directory and the export name
|
|
6695
|
-
|
|
6696
|
-
| File | Export | Answers at |
|
|
6697
|
-
| --- | --- | --- |
|
|
6698
|
-
| `src/users/page.abide` | `getUser` | `/users/getUser` |
|
|
6699
|
-
| `src/admin/page.abide` | `recent` | `/admin/recent` |
|
|
6700
|
-
| `src/media/[id]/page.abide` | `save` | `/media/42/save`, with `id` in its args |
|
|
6701
|
-
| `src/media/[id]/comments.abide` | `addComment` | `/media/42/addComment` |
|
|
6702
|
-
|
|
6703
|
-
So the address is readable from the source tree, which is what a log line or a network panel is for. A parameter in the route reaches the handler's args from the path, so its `Args` declares it. The address is not a thing to write out: the route table is a build artifact, and two files that answer one method at one address are a build error naming both.
|
|
6704
|
-
|
|
6705
6782
|
## `rpc.raw` hands back the `Response`
|
|
6706
6783
|
|
|
6707
6784
|
Where you want the `Response` rather than a decoded value — a header to read, a `Blob` to hand to a download, a body you will frame yourself:
|
|
@@ -6725,7 +6802,7 @@ Read on: [Streaming data](/data/send-data-as-it-arrives.md) · [Reading data](/d
|
|
|
6725
6802
|
The string you passed as `description`, readable back off the handler. The same one rides onto the OpenAPI operation, the MCP tool and the CLI command — one place to write it, and no surface can carry a different sentence than another.
|
|
6726
6803
|
|
|
6727
6804
|
```ts server
|
|
6728
|
-
export
|
|
6805
|
+
export default GET(
|
|
6729
6806
|
({ id }: { id: string }) => database.invoice.find(id),
|
|
6730
6807
|
{ description: 'One invoice, by id.' },
|
|
6731
6808
|
)
|
|
@@ -6766,8 +6843,8 @@ A file is a route. The parts of the path in brackets become values you can read.
|
|
|
6766
6843
|
|
|
6767
6844
|
| File | Serves |
|
|
6768
6845
|
| --- | --- |
|
|
6769
|
-
| `src/invoices/
|
|
6770
|
-
| `src/invoices/[id]/
|
|
6846
|
+
| `src/invoices/index.abide` | `/invoices` |
|
|
6847
|
+
| `src/invoices/[id]/index.abide` | `/invoices/42`, as `route.params.id` |
|
|
6771
6848
|
| `src/layout.abide` | the chrome for everything below it |
|
|
6772
6849
|
| `src/error.abide` | where a failure renders, 404 included |
|
|
6773
6850
|
|
|
@@ -6813,38 +6890,87 @@ Read on: [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) · [Hea
|
|
|
6813
6890
|
|
|
6814
6891
|
*A file becomes a route, and the parts of the path become values you can read.*
|
|
6815
6892
|
|
|
6816
|
-
A route is a file
|
|
6893
|
+
A route is a file, and its path below `src` is the path it answers on. There is no table to register it in and nothing to keep in step with the filesystem — listing a directory lists its routes.
|
|
6894
|
+
|
|
6895
|
+
## A file's path is its address
|
|
6896
|
+
|
|
6897
|
+
| File | Answers on |
|
|
6898
|
+
| --- | --- |
|
|
6899
|
+
| `src/index.abide` | `/` |
|
|
6900
|
+
| `src/invoices/index.abide` | `/invoices` |
|
|
6901
|
+
| `src/invoices/new.abide` | `/invoices/new` |
|
|
6902
|
+
| `src/invoices/[id].abide` | `/invoices/4310` |
|
|
6903
|
+
| `src/invoices/[id]/index.abide` | `/invoices/4310`, the same route |
|
|
6904
|
+
| `src/docs/[...path].abide` | `/docs/start/install` |
|
|
6905
|
+
| `src/invoices/listInvoices.ts` | `GET /invoices/listInvoices` |
|
|
6906
|
+
|
|
6907
|
+
An `index.abide` answers its directory's own address, and any other `.abide` adds its name. A `layout.abide` and an `error.abide` are not pages: they take their directory's route, and wrap or catch what is below it.
|
|
6908
|
+
|
|
6909
|
+
`[id].abide` and `[id]/index.abide` are two spellings of one route, so writing both fails the build, naming the two files. The directory is the one with room. A handler file inside `[id]/` answers below the invoice, at `/invoices/[id]/markPaid`, and `id` reaches its arguments from the path. One beside `[id].abide` answers for the whole subject. Start with the file, and move it into a directory once the invoice has files of its own.
|
|
6910
|
+
|
|
6911
|
+
A `.ts` beside the pages is a handler file. Its default export is the `GET` or the `POST` it declares, it exports nothing else, and its name is its last segment.
|
|
6912
|
+
|
|
6913
|
+
Read on: [Reading data](/data/read-data-without-writing-an-api.md)
|
|
6914
|
+
|
|
6915
|
+
## A directory lists its routes
|
|
6916
|
+
|
|
6917
|
+
Four directory names are never a segment: `lib` for components and modules, `tests` for tests, `server` for code the browser never loads, and `public` for files served as they are. Nothing under one has an address. Every other file directly in a directory is a page, a layout, an error page or a handler file.
|
|
6918
|
+
|
|
6919
|
+
```
|
|
6920
|
+
src/
|
|
6921
|
+
├── index.abide /
|
|
6922
|
+
├── layout.abide wraps every page
|
|
6923
|
+
└── invoices/
|
|
6924
|
+
├── index.abide /invoices
|
|
6925
|
+
├── [id].abide /invoices/[id]
|
|
6926
|
+
├── layout.abide wraps both
|
|
6927
|
+
├── listInvoices.ts GET /invoices/listInvoices
|
|
6928
|
+
├── lib/Row.abide a component, no address
|
|
6929
|
+
└── tests/Row.test.ts a test, no address
|
|
6930
|
+
```
|
|
6931
|
+
|
|
6932
|
+
So the component a page places goes in a `lib` beside it, and is imported as `./lib/Row.abide`. A stylesheet, a helper or a test written straight beside the pages fails the build, naming the file and the `lib` to move it into. That refusal is what keeps a listing honest: what a directory holds is what it answers.
|
|
6933
|
+
|
|
6934
|
+
## `abide routes` prints every address and its file
|
|
6935
|
+
|
|
6936
|
+
Run in the app above, it prints each route with the method it answers and the file answering it, then the file filing each hook and the route it covers:
|
|
6937
|
+
|
|
6938
|
+
```
|
|
6939
|
+
$ abide routes
|
|
6940
|
+
GET / index.abide
|
|
6941
|
+
POST /__abide/handlers/invoices/id/markPaid invoices/[id].abide (private)
|
|
6942
|
+
GET /invoices invoices/index.abide
|
|
6943
|
+
GET /invoices/[id] invoices/[id].abide
|
|
6944
|
+
GET /invoices/listInvoices invoices/listInvoices.ts
|
|
6945
|
+
|
|
6946
|
+
hooks
|
|
6947
|
+
middleware /invoices invoices/layout.abide
|
|
6948
|
+
onStart / layout.abide
|
|
6949
|
+
```
|
|
6950
|
+
|
|
6951
|
+
The `(private)` line is the `markPaid` that `[id].abide`'s own `<script server>` declares: it is that file's alone, so it answers below `/__abide/handlers/` rather than at an address of the app's. `--json` prints the same map as one document.
|
|
6952
|
+
|
|
6953
|
+
Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
|
|
6817
6954
|
|
|
6818
6955
|
## The three dynamic segments
|
|
6819
6956
|
|
|
6820
|
-
| The directory is named | It matches | In `route.params` |
|
|
6957
|
+
| The file or directory is named | It matches | In `route.params` |
|
|
6821
6958
|
| --- | --- | --- |
|
|
6822
6959
|
| `[id]` | exactly one segment, required | a required key |
|
|
6823
6960
|
| `[[tab]]` | one segment or none | an optional key, absent when it did not match |
|
|
6824
6961
|
| `[...rest]` | the remaining segments, terminal | a list, empty when it matched nothing |
|
|
6825
6962
|
|
|
6826
|
-
A rest segment is terminal because there is nothing a segment after it could match against. The other two compose freely, so `invoices/[id]/[[tab]]` is one route answering two addresses rather than two routes agreeing about an invoice.
|
|
6963
|
+
A rest segment is terminal because there is nothing a segment after it could match against. The other two compose freely, so `invoices/[id]/[[tab]].abide` is one route answering two addresses rather than two routes agreeing about an invoice.
|
|
6827
6964
|
|
|
6828
6965
|
An optional segment that did not match is **absent** rather than empty. That is the difference between `const { tab = 'summary' } = route.params` working and a default that never fires, which is a distinction an empty string would quietly lose.
|
|
6829
6966
|
|
|
6830
|
-
##
|
|
6831
|
-
|
|
6832
|
-
| | |
|
|
6833
|
-
| --- | --- |
|
|
6834
|
-
| `src/page.abide` | `/` |
|
|
6835
|
-
| `src/invoices/page.abide` | `/invoices` |
|
|
6836
|
-
| `src/invoices/[id]/page.abide` | `/invoices/4310` |
|
|
6837
|
-
| `src/docs/[...path]/page.abide` | `/docs/start/install` |
|
|
6838
|
-
| `src/billing/page.abide` | `/billing` |
|
|
6839
|
-
|
|
6840
|
-
A subject's pages live with the rest of it, and a directory named `server` or `public` is never a segment of an address. Any other file beside a `page.abide` — a component, a helper — is yours, and is no address.
|
|
6967
|
+
## `route.params` and `route.name` follow the address
|
|
6841
6968
|
|
|
6842
6969
|
`route.url` comes from the file path and `route.name` is the **resolution path** — the pattern that matched, not the address that arrived. So a log line, a metric and an `{#if}` over the current route all key on `invoices/[id]` for every invoice rather than on one address each.
|
|
6843
6970
|
|
|
6844
|
-
```abide src/invoices/[id]
|
|
6971
|
+
```abide src/invoices/[id].abide — excerpt
|
|
6845
6972
|
<script>
|
|
6846
6973
|
import { reactive, route } from '@abide/abide'
|
|
6847
|
-
import { getInvoice } from '#src/invoices/api.abide'
|
|
6848
6974
|
|
|
6849
6975
|
const invoice = reactive(() => getInvoice({ id: route.params.id }))
|
|
6850
6976
|
</script>
|
|
@@ -6855,28 +6981,22 @@ const invoice = reactive(() => getInvoice({ id: route.params.id }))
|
|
|
6855
6981
|
|
|
6856
6982
|
Reading `route.params` is a live read like any other, so a navigation from one invoice to another moves the value rather than rebuilding the page.
|
|
6857
6983
|
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
```abide abide
|
|
6861
|
-
<script module>
|
|
6862
|
-
import 'admin-kit/admin' with { type: 'routes' }
|
|
6863
|
-
</script>
|
|
6864
|
-
|
|
6865
|
-
<section class="tools"><slot/></section>
|
|
6866
|
-
```
|
|
6867
|
-
|
|
6868
|
-
Written in `src/tools/layout.abide`, that line makes `admin-kit/admin/users/[id]/page.abide` answer at `/tools/users/[id]`, inside this layout. Moving the line moves the package. Its files route by their path below the directory named, as yours do, and compile with your app on its abide. A directory mounts once, and naming one of your own directories, or one that is not there, fails the build.
|
|
6869
|
-
|
|
6870
|
-
Read on: [Links & navigation](/pages/link-to-another-page.md) · [Reading data](/data/read-data-without-writing-an-api.md)
|
|
6984
|
+
Read on: [Links & navigation](/pages/link-to-another-page.md)
|
|
6871
6985
|
|
|
6872
6986
|
## `Params` is what the segments matched
|
|
6873
6987
|
|
|
6874
|
-
`Params` carries what `[param]`, `[[optional]]` and `[...rest]` matched: text, or a list for a rest. In a page it is typed from the page's own route rather than declared beside it — `abide check` reads `route.params()` in `invoices/[id]
|
|
6988
|
+
`Params` carries what `[param]`, `[[optional]]` and `[...rest]` matched: text, or a list for a rest. In a page it is typed from the page's own route rather than declared beside it — `abide check` reads `route.params()` in `invoices/[id].abide` as `RouteParams<'/invoices/[id]'>`, so `id` is text and a key the route does not have is an error (23.9). A layout gets its own segments, and whatever a page below it adds.
|
|
6875
6989
|
|
|
6876
6990
|
Nothing else is in it. A query string is not a param — it is not part of what resolved the route, and treating it as one is how a route starts depending on something that never selected it.
|
|
6877
6991
|
|
|
6878
6992
|
Read on: [Links & navigation](/pages/link-to-another-page.md)
|
|
6879
6993
|
|
|
6994
|
+
## A package's pages answer below the file importing the package
|
|
6995
|
+
|
|
6996
|
+
A package can ship pages, layouts, error pages and handler files as source, and the address they answer at is yours to choose. Written in `src/tools/layout.abide`, `import 'admin-kit/admin' with { type: 'routes' }` puts all of them below that layout: `admin-kit/admin/users/[id].abide` answers at `/tools/users/[id]`, inside it.
|
|
6997
|
+
|
|
6998
|
+
Moving the line moves the package. Its files route by their path below the directory named, as yours do, and compile with your app on its abide. A directory mounts once, and naming one of your own directories, or one that is not there, fails the build.
|
|
6999
|
+
|
|
6880
7000
|
## Next
|
|
6881
7001
|
|
|
6882
7002
|
- [Layouts](/pages/give-pages-the-same-chrome.md) — the chrome a page renders inside
|
|
@@ -6985,7 +7105,7 @@ That is the one case where a quiet fallback would be the wrong kindness: an erro
|
|
|
6985
7105
|
|
|
6986
7106
|
**`onError(fn)` — the tracker hears what the caller is not told** — A contacts export whose worker has stopped answering. Run it: the page gets a bare 500, and the hook beside it gets the reason.
|
|
6987
7107
|
|
|
6988
|
-
```abide ContactsExport.abide
|
|
7108
|
+
```abide lib/ContactsExport.abide
|
|
6989
7109
|
<script server>
|
|
6990
7110
|
import { GET, onError, POST } from '@abide/abide/server'
|
|
6991
7111
|
|
|
@@ -7008,12 +7128,12 @@ const exportWorker = {
|
|
|
7008
7128
|
|
|
7009
7129
|
onError((error) => tracker.capture(error))
|
|
7010
7130
|
|
|
7011
|
-
|
|
7131
|
+
const exportContacts = POST(() => exportWorker.run(), {
|
|
7012
7132
|
description: 'Exports every contact.',
|
|
7013
7133
|
})
|
|
7014
7134
|
|
|
7015
7135
|
/** What the tracker was sent, which the caller of the export never is. */
|
|
7016
|
-
|
|
7136
|
+
const capturedErrors = GET(() => tracker.captured, {
|
|
7017
7137
|
description: 'The errors the tracker was sent.',
|
|
7018
7138
|
})
|
|
7019
7139
|
</script>
|
|
@@ -7062,7 +7182,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [Logging](/app/recor
|
|
|
7062
7182
|
|
|
7063
7183
|
A `<head>` element in a `.abide` file contributes its children to the document head. It is markup rather than a call, so what a page adds to the head sits in the page, next to the values it is built from.
|
|
7064
7184
|
|
|
7065
|
-
```abide src/invoices/[id]/
|
|
7185
|
+
```abide src/invoices/[id]/index.abide — excerpt
|
|
7066
7186
|
<head>
|
|
7067
7187
|
<title>Invoice {route.params.id}</title>
|
|
7068
7188
|
<meta name="description" content={invoice.summary}/>
|
|
@@ -7088,7 +7208,7 @@ The head goes out with the page once it is built, and a value still in flight do
|
|
|
7088
7208
|
|
|
7089
7209
|
Nothing is opt-in here either: a `<title>` is a sink like any other, and it fills when the value lands. Metadata a crawler reads — which does not come back for the second version — is the one case that wants the value before the first flush, and that is the server's business rather than the head's.
|
|
7090
7210
|
|
|
7091
|
-
```abide src/invoices/[id]/
|
|
7211
|
+
```abide src/invoices/[id]/index.abide — excerpt
|
|
7092
7212
|
<head>
|
|
7093
7213
|
<title>Invoice {invoice.number}</title>
|
|
7094
7214
|
</head>
|
|
@@ -7120,7 +7240,7 @@ Read on: [Links & navigation](/pages/link-to-another-page.md)
|
|
|
7120
7240
|
|
|
7121
7241
|
`url` builds an address from a **route literal** — the pattern, not the address. So the thing you write down is the thing the router already knows, and a route that moves takes its links with it.
|
|
7122
7242
|
|
|
7123
|
-
```abide src/invoices/
|
|
7243
|
+
```abide src/invoices/index.abide — excerpt
|
|
7124
7244
|
<a href={url('/invoices/[id]', { id: invoice.id })}>{invoice.number}</a>
|
|
7125
7245
|
```
|
|
7126
7246
|
|
|
@@ -7159,7 +7279,7 @@ Read on: [Routes](/pages/add-a-page.md)
|
|
|
7159
7279
|
|
|
7160
7280
|
**`navigate(url)` — a new address, the same page** — Three invoices on one account, each with its own address. Type a note, then open another invoice — the address moves and the note is still there.
|
|
7161
7281
|
|
|
7162
|
-
```abide Invoice.abide
|
|
7282
|
+
```abide lib/Invoice.abide
|
|
7163
7283
|
<script>
|
|
7164
7284
|
import { navigate, reactive, route } from '@abide/abide'
|
|
7165
7285
|
|
|
@@ -7227,7 +7347,7 @@ A view transition is a browser feature, and abide's part in it is to get out of
|
|
|
7227
7347
|
|
|
7228
7348
|
## The stylesheet is the switch
|
|
7229
7349
|
|
|
7230
|
-
```abide src/invoices/[id]/
|
|
7350
|
+
```abide src/invoices/[id]/index.abide — excerpt
|
|
7231
7351
|
<style>
|
|
7232
7352
|
.invoice-total {
|
|
7233
7353
|
view-transition-name: invoice-total;
|
|
@@ -7367,7 +7487,7 @@ Read on: [Request to paint](/start/how-a-page-becomes-html.md) · [Loading state
|
|
|
7367
7487
|
|
|
7368
7488
|
**`render(component)` — an invoice preview from the component itself** — The invoice a customer will be sent, previewed before it is issued: the handler renders the app's own invoice component into a document of its own.
|
|
7369
7489
|
|
|
7370
|
-
```abide InvoicePreview.abide
|
|
7490
|
+
```abide lib/InvoicePreview.abide
|
|
7371
7491
|
<script server>
|
|
7372
7492
|
import { GET, page, render } from '@abide/abide/server'
|
|
7373
7493
|
import Document from './InvoiceDocument.abide'
|
|
@@ -7403,7 +7523,7 @@ async function findInvoice(id: string) {
|
|
|
7403
7523
|
return invoice
|
|
7404
7524
|
}
|
|
7405
7525
|
|
|
7406
|
-
|
|
7526
|
+
const previewInvoice = GET(
|
|
7407
7527
|
async ({ id }: { id: string }) => {
|
|
7408
7528
|
const invoice = await findInvoice(id)
|
|
7409
7529
|
return page(await render(Document({ invoice })))
|
|
@@ -7428,7 +7548,7 @@ let id = reactive('4021')
|
|
|
7428
7548
|
class="h-56 w-full rounded-md border border-rule"></iframe>
|
|
7429
7549
|
```
|
|
7430
7550
|
|
|
7431
|
-
```abide InvoiceDocument.abide
|
|
7551
|
+
```abide lib/InvoiceDocument.abide
|
|
7432
7552
|
<script>
|
|
7433
7553
|
import { props } from '@abide/abide'
|
|
7434
7554
|
|
|
@@ -7473,7 +7593,7 @@ The component `render` is given is the whole document when it is written inside
|
|
|
7473
7593
|
|
|
7474
7594
|
An email body wants a document of its own — no bundle, no font preloads, inline styles — and that is the whole of what writing one means. Nothing else about the render changes. A component that writes `<html>` is a whole document, so placing one inside another is refused.
|
|
7475
7595
|
|
|
7476
|
-
```abide InvoiceDocument.abide — excerpt
|
|
7596
|
+
```abide lib/InvoiceDocument.abide — excerpt
|
|
7477
7597
|
<html lang="en">
|
|
7478
7598
|
<body style="margin: 0; padding: 1.25rem; background: #fff; color: #1d2a28;
|
|
7479
7599
|
font: 14px/1.5 system-ui, sans-serif">
|
|
@@ -7604,7 +7724,7 @@ Read on: [Local state](/values/show-a-value-that-changes.md) · [Loading states]
|
|
|
7604
7724
|
|
|
7605
7725
|
**`contact.address.city = v` — a write through a path** — A contact's city, two levels down. Type into the field and watch the line above it.
|
|
7606
7726
|
|
|
7607
|
-
```abide Contact.abide
|
|
7727
|
+
```abide lib/Contact.abide
|
|
7608
7728
|
<script>
|
|
7609
7729
|
import { reactive } from '@abide/abide'
|
|
7610
7730
|
|
|
@@ -7676,7 +7796,7 @@ Each of them updates the **one thing it names** and nothing around it. That is t
|
|
|
7676
7796
|
|
|
7677
7797
|
**`{expr}` and `{raw(expr)}` — escaped, and not** — A note imported with markup in it. Type a tag into the field and watch the two rows disagree.
|
|
7678
7798
|
|
|
7679
|
-
```abide Note.abide
|
|
7799
|
+
```abide lib/Note.abide
|
|
7680
7800
|
<script>
|
|
7681
7801
|
import { reactive } from '@abide/abide'
|
|
7682
7802
|
|
|
@@ -7717,7 +7837,7 @@ Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
|
|
|
7717
7837
|
|
|
7718
7838
|
**`class:name={cond}` and `style:prop={value}` — one class, one property** — An invoice's age badge. Press + until it is late, and back down again.
|
|
7719
7839
|
|
|
7720
|
-
```abide Invoice.abide
|
|
7840
|
+
```abide lib/Invoice.abide
|
|
7721
7841
|
<script>
|
|
7722
7842
|
import { reactive } from '@abide/abide'
|
|
7723
7843
|
|
|
@@ -7799,7 +7919,7 @@ An event handler is an attribute whose value is a function. There is no syntheti
|
|
|
7799
7919
|
|
|
7800
7920
|
**`bind:element` and `on<event>` — the node, and what happens on it** — A contact's notes. Press New note and the field is cleared and waiting for you.
|
|
7801
7921
|
|
|
7802
|
-
```abide Note.abide
|
|
7922
|
+
```abide lib/Note.abide
|
|
7803
7923
|
<script>
|
|
7804
7924
|
import { reactive } from '@abide/abide'
|
|
7805
7925
|
|
|
@@ -7866,7 +7986,7 @@ A form is a value you can see. The usual cost of saying so is a handler per fiel
|
|
|
7866
7986
|
|
|
7867
7987
|
**`bind:value` — the field and the value are one copy** — A contact's email and plan, with the line underneath showing what a save would send. Type into the field.
|
|
7868
7988
|
|
|
7869
|
-
```abide Contact.abide
|
|
7989
|
+
```abide lib/Contact.abide
|
|
7870
7990
|
<script>
|
|
7871
7991
|
import { reactive } from '@abide/abide'
|
|
7872
7992
|
|
|
@@ -7903,7 +8023,7 @@ Read on: [Local state](/values/show-a-value-that-changes.md)
|
|
|
7903
8023
|
|
|
7904
8024
|
**`bind:checked` and `bind:open` — a boolean, not a string** — A reminders checkbox and a disclosure. Tick the box and open the panel, and watch the two lines underneath.
|
|
7905
8025
|
|
|
7906
|
-
```abide Advanced.abide
|
|
8026
|
+
```abide lib/Advanced.abide
|
|
7907
8027
|
<script>
|
|
7908
8028
|
import { reactive } from '@abide/abide'
|
|
7909
8029
|
|
|
@@ -7947,7 +8067,7 @@ The readout reads `showAdvanced` like any other value, which is the point of bin
|
|
|
7947
8067
|
|
|
7948
8068
|
**`{#if}` over a bound value — the field decides what renders** — A contact form that only asks for a billing address when you say there is one. Tick the box.
|
|
7949
8069
|
|
|
7950
|
-
```abide BillingContact.abide
|
|
8070
|
+
```abide lib/BillingContact.abide
|
|
7951
8071
|
<script>
|
|
7952
8072
|
import { reactive } from '@abide/abide'
|
|
7953
8073
|
|
|
@@ -7972,7 +8092,7 @@ What was typed into the address goes with it, the input being unbound. Bind it t
|
|
|
7972
8092
|
|
|
7973
8093
|
**`bind:group` — one value for radios, an array for checkboxes** — A contact's tier and the regions they cover. Change the tier, then tick and untick a region.
|
|
7974
8094
|
|
|
7975
|
-
```abide Assign.abide
|
|
8095
|
+
```abide lib/Assign.abide
|
|
7976
8096
|
<script>
|
|
7977
8097
|
import { reactive } from '@abide/abide'
|
|
7978
8098
|
|
|
@@ -8042,7 +8162,7 @@ A derived value is writable too, and where its body hands back another `Reactive
|
|
|
8042
8162
|
|
|
8043
8163
|
A `schema` runs on the way into a value, and a write it refuses is rejected at the boundary rather than stored and checked later:
|
|
8044
8164
|
|
|
8045
|
-
```abide src/account/
|
|
8165
|
+
```abide src/account/index.abide — excerpt
|
|
8046
8166
|
<script>
|
|
8047
8167
|
import { reactive } from '@abide/abide'
|
|
8048
8168
|
import { z } from 'zod'
|
|
@@ -8068,7 +8188,7 @@ The field opens empty. An opening `undefined` lands without passing the schema (
|
|
|
8068
8188
|
|
|
8069
8189
|
Both gates refuse. What you refuse **with** picks between them: a schema refuses as `ValidationError`, and a `transform` refuses as a name of your own — the only way a failure arrives carrying data you chose. `refuse.named` declares it once and resolves on both sides, so the same name serves a form field and a handler.
|
|
8070
8190
|
|
|
8071
|
-
```ts src/failures.ts
|
|
8191
|
+
```ts src/lib/failures.ts
|
|
8072
8192
|
import { refuse } from '@abide/abide'
|
|
8073
8193
|
import { z } from 'zod'
|
|
8074
8194
|
|
|
@@ -8077,10 +8197,10 @@ export const notAnEmail = refuse.named('NotAnEmail', 422, undefined, {
|
|
|
8077
8197
|
})
|
|
8078
8198
|
```
|
|
8079
8199
|
|
|
8080
|
-
```abide src/account/
|
|
8200
|
+
```abide src/account/index.abide — excerpt
|
|
8081
8201
|
<script>
|
|
8082
8202
|
import { reactive } from '@abide/abide'
|
|
8083
|
-
import { notAnEmail } from '#src/failures'
|
|
8203
|
+
import { notAnEmail } from '#src/lib/failures'
|
|
8084
8204
|
|
|
8085
8205
|
const email = reactive(undefined, {
|
|
8086
8206
|
transform: (value: string) =>
|
|
@@ -8106,7 +8226,7 @@ The call site decides this, not the child.
|
|
|
8106
8226
|
|
|
8107
8227
|
A prop is a **live read** of the caller's expression. `count={total}` tracks `total`, `count={total * 2}` tracks it as a derived read, and neither reinstantiates the component. The child declares the prop as the type of its value and writes it by name:
|
|
8108
8228
|
|
|
8109
|
-
```abide src/
|
|
8229
|
+
```abide src/lib/Stepper.abide
|
|
8110
8230
|
<script>
|
|
8111
8231
|
import { props } from '@abide/abide'
|
|
8112
8232
|
|
|
@@ -8167,7 +8287,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Strea
|
|
|
8167
8287
|
|
|
8168
8288
|
**`{#try}` — a boundary over a region, not over a line** — An invoice converted at a rate you type. Clear the field and watch what goes with it.
|
|
8169
8289
|
|
|
8170
|
-
```abide Total.abide
|
|
8290
|
+
```abide lib/Total.abide
|
|
8171
8291
|
<script>
|
|
8172
8292
|
import { reactive } from '@abide/abide'
|
|
8173
8293
|
|
|
@@ -8236,7 +8356,7 @@ It iterates **zero times** over an absent subject. A list still loading is not a
|
|
|
8236
8356
|
|
|
8237
8357
|
**`{#for row of rows by row.id}` — a reorder moves nodes** — Three contacts. Swap the top two, then reverse the lot.
|
|
8238
8358
|
|
|
8239
|
-
```abide Contacts.abide
|
|
8359
|
+
```abide lib/Contacts.abide
|
|
8240
8360
|
<script>
|
|
8241
8361
|
import { reactive } from '@abide/abide'
|
|
8242
8362
|
|
|
@@ -8282,7 +8402,7 @@ Read on: [Patching a list](/values/patch-a-list-from-a-live-feed.md)
|
|
|
8282
8402
|
|
|
8283
8403
|
**`reactive(contacts, { by: 'id' })` — every list over it moves nodes** — The contacts and the call queue, one list shown twice. Swap the top two.
|
|
8284
8404
|
|
|
8285
|
-
```abide ContactsAndQueue.abide
|
|
8405
|
+
```abide lib/ContactsAndQueue.abide
|
|
8286
8406
|
<script>
|
|
8287
8407
|
import { reactive } from '@abide/abide'
|
|
8288
8408
|
|
|
@@ -8393,7 +8513,7 @@ A component is a `.abide` file, or a block in one. Invoking it is a capitalized
|
|
|
8393
8513
|
|
|
8394
8514
|
An inline component is TitleCase like any other, and it exists for the piece of markup that is worth naming but not worth a file — a row in one table, a cell shape used twice on one page. The parentheses hold the pattern a `props()` call would destructure, and an annotation on it is that call's type argument:
|
|
8395
8515
|
|
|
8396
|
-
```abide src/contacts/[id]/
|
|
8516
|
+
```abide src/contacts/[id]/index.abide — excerpt
|
|
8397
8517
|
{#component Field({ label, value }: { label: string; value: string })}
|
|
8398
8518
|
<li><span>{label}</span><strong>{value}</strong></li>
|
|
8399
8519
|
{/component}
|
|
@@ -8408,11 +8528,11 @@ It can be written anywhere markup can: at the top of the file, inside an element
|
|
|
8408
8528
|
|
|
8409
8529
|
**It closes over where it is written.** A value the file's `<script>` declares is read by name inside it, and live, the same as anywhere else in the template. Declared in a row, it reads that row's item without being handed it:
|
|
8410
8530
|
|
|
8411
|
-
```abide src/contacts/
|
|
8531
|
+
```abide src/contacts/index.abide — excerpt
|
|
8412
8532
|
<script>
|
|
8413
8533
|
import { reactive } from '@abide/abide'
|
|
8414
|
-
import
|
|
8415
|
-
import type { Contact } from '#src/Contact.ts'
|
|
8534
|
+
import listContacts from '#src/contacts/listContacts.ts'
|
|
8535
|
+
import type { Contact } from '#src/lib/Contact.ts'
|
|
8416
8536
|
|
|
8417
8537
|
const contacts = listContacts()
|
|
8418
8538
|
let masked = reactive(true)
|
|
@@ -8434,10 +8554,10 @@ Setting `masked` rewrites each `<strong>` in place. Its own `<script>` is set up
|
|
|
8434
8554
|
|
|
8435
8555
|
An inline component written directly between a component's tags is **passed to it as a prop** of the same name. The component declares that prop as a function of the props it hands over, and invokes it by name:
|
|
8436
8556
|
|
|
8437
|
-
```abide src/
|
|
8557
|
+
```abide src/lib/ContactTable.abide
|
|
8438
8558
|
<script>
|
|
8439
8559
|
import { props } from '@abide/abide'
|
|
8440
|
-
import type { Contact } from '#src/Contact.ts'
|
|
8560
|
+
import type { Contact } from '#src/lib/Contact.ts'
|
|
8441
8561
|
|
|
8442
8562
|
const { contacts, Row } = props<{
|
|
8443
8563
|
contacts: Contact[]
|
|
@@ -8455,7 +8575,7 @@ const { contacts, Row } = props<{
|
|
|
8455
8575
|
<slot/>
|
|
8456
8576
|
```
|
|
8457
8577
|
|
|
8458
|
-
```abide src/contacts/
|
|
8578
|
+
```abide src/contacts/index.abide — excerpt
|
|
8459
8579
|
<ContactTable contacts={contacts}>
|
|
8460
8580
|
{#component Row({ contact }: { contact: Contact })}
|
|
8461
8581
|
<td>{contact.name}</td><td>{masked ? '••• ••••' : contact.phone}</td>
|
|
@@ -8470,7 +8590,7 @@ The table decides where each row goes and the page decides what is in it, readin
|
|
|
8470
8590
|
|
|
8471
8591
|
**`props()` — a prop is a live read, not a snapshot** — An invoice's line count and the total a child component renders. Press + and watch the child.
|
|
8472
8592
|
|
|
8473
|
-
```abide Invoice.abide
|
|
8593
|
+
```abide lib/Invoice.abide
|
|
8474
8594
|
<script>
|
|
8475
8595
|
import { reactive } from '@abide/abide'
|
|
8476
8596
|
import Total from './Total.abide'
|
|
@@ -8489,7 +8609,7 @@ let lines = reactive(2)
|
|
|
8489
8609
|
</div>
|
|
8490
8610
|
```
|
|
8491
8611
|
|
|
8492
|
-
```abide Total.abide
|
|
8612
|
+
```abide lib/Total.abide
|
|
8493
8613
|
<script>
|
|
8494
8614
|
import { props } from '@abide/abide'
|
|
8495
8615
|
|
|
@@ -8516,7 +8636,7 @@ Read on: [Values by name](/templates/read-and-write-a-value-by-name.md) · [Form
|
|
|
8516
8636
|
|
|
8517
8637
|
**`crm-ui/Initials.abide` — a design system's component, imported by name** — The CRM's contacts carry the avatar its design system ships. Nothing in the app compiled it, and it renders with its own styles.
|
|
8518
8638
|
|
|
8519
|
-
```abide ContactCard.abide
|
|
8639
|
+
```abide lib/ContactCard.abide
|
|
8520
8640
|
<script>
|
|
8521
8641
|
import Initials from 'crm-ui/Initials.abide'
|
|
8522
8642
|
</script>
|
|
@@ -8558,7 +8678,7 @@ const initials = name
|
|
|
8558
8678
|
|
|
8559
8679
|
A package ships its components as `.abide` source, and the app imports one by the specifier its `exports` names, the same as a module. It is compiled with the app that imports it, wherever it lies: in `node_modules`, or in a package a workspace links from outside the app's root. Its props are typed by the app's project, its styles are scoped like the app's own, and it runs on the app's abide rather than on any copy the package installed beside it.
|
|
8560
8680
|
|
|
8561
|
-
What it is not is an address. A
|
|
8681
|
+
What it is not is an address. A page or a handler file inside a package answers nowhere until the app mounts the package's directory with a `type: 'routes'` import, which places them below the file that imports it.
|
|
8562
8682
|
|
|
8563
8683
|
Read on: [Styles](/templates/scope-styles-to-a-component.md) · [Reading data](/data/read-data-without-writing-an-api.md)
|
|
8564
8684
|
|
|
@@ -8572,7 +8692,7 @@ There is no implicit bag. A component that never calls `props()` takes no props
|
|
|
8572
8692
|
|
|
8573
8693
|
Whatever a caller puts between the tags arrives as `children`, and `<slot/>` is where it renders. `<slot>fallback</slot>` renders the fallback where a caller passed none, which is how a panel has a default body without the caller having to know what it is:
|
|
8574
8694
|
|
|
8575
|
-
```abide src/
|
|
8695
|
+
```abide src/lib/Panel.abide
|
|
8576
8696
|
<script>
|
|
8577
8697
|
import { props } from '@abide/abide'
|
|
8578
8698
|
|
|
@@ -8582,7 +8702,7 @@ const { title } = props<{ title: string }>()
|
|
|
8582
8702
|
<li><span>{title}</span><strong><slot>none yet</slot></strong></li>
|
|
8583
8703
|
```
|
|
8584
8704
|
|
|
8585
|
-
```abide src/contacts/[id]/
|
|
8705
|
+
```abide src/contacts/[id]/index.abide — excerpt
|
|
8586
8706
|
<ul class="fields">
|
|
8587
8707
|
<Panel title="phone">+1 415 555 0132</Panel>
|
|
8588
8708
|
<Panel title="notes"/>
|
|
@@ -8601,7 +8721,7 @@ Read on: [Expressions](/templates/put-a-value-in-the-markup.md)
|
|
|
8601
8721
|
|
|
8602
8722
|
A component that wraps a `<button>` usually declares the few props it reads itself and spreads the rest onto the element. `Attributes<'button'>` types that rest as what a `<button>` takes, so a caller is held to the element's own attributes and its `onclick` is handed a `MouseEvent`:
|
|
8603
8723
|
|
|
8604
|
-
```abide src/
|
|
8724
|
+
```abide src/lib/Button.abide
|
|
8605
8725
|
<script>
|
|
8606
8726
|
import { props, type Attributes } from '@abide/abide'
|
|
8607
8727
|
|
|
@@ -8648,7 +8768,7 @@ A component's styles are the component's. Writing `.badge` inside one and having
|
|
|
8648
8768
|
|
|
8649
8769
|
**`<style>` — the same class name in two components** — Two badge components that each style `.badge`, rendered side by side. Neither one reaches the other.
|
|
8650
8770
|
|
|
8651
|
-
```abide ScopedBadges.abide
|
|
8771
|
+
```abide lib/ScopedBadges.abide
|
|
8652
8772
|
<script>
|
|
8653
8773
|
import Overdue from './Overdue.abide'
|
|
8654
8774
|
import Paid from './Paid.abide'
|
|
@@ -8660,7 +8780,7 @@ import Paid from './Paid.abide'
|
|
|
8660
8780
|
</div>
|
|
8661
8781
|
```
|
|
8662
8782
|
|
|
8663
|
-
```abide Overdue.abide
|
|
8783
|
+
```abide lib/Overdue.abide
|
|
8664
8784
|
<span class="badge">Overdue</span>
|
|
8665
8785
|
|
|
8666
8786
|
<style>
|
|
@@ -8672,7 +8792,7 @@ import Paid from './Paid.abide'
|
|
|
8672
8792
|
</style>
|
|
8673
8793
|
```
|
|
8674
8794
|
|
|
8675
|
-
```abide Paid.abide
|
|
8795
|
+
```abide lib/Paid.abide
|
|
8676
8796
|
<span class="badge">Paid</span>
|
|
8677
8797
|
|
|
8678
8798
|
<style>
|
|
@@ -8696,7 +8816,7 @@ Read on: [Components](/templates/reuse-a-piece-of-markup.md)
|
|
|
8696
8816
|
|
|
8697
8817
|
**`:global(…)` — the per-selector escape from scoping** — A badge component whose stylesheet also colors a paragraph the page rendered, not the component.
|
|
8698
8818
|
|
|
8699
|
-
```abide GlobalLegend.abide
|
|
8819
|
+
```abide lib/GlobalLegend.abide
|
|
8700
8820
|
<script>
|
|
8701
8821
|
import Legend from './Legend.abide'
|
|
8702
8822
|
</script>
|
|
@@ -8707,7 +8827,7 @@ import Legend from './Legend.abide'
|
|
|
8707
8827
|
</div>
|
|
8708
8828
|
```
|
|
8709
8829
|
|
|
8710
|
-
```abide Legend.abide
|
|
8830
|
+
```abide lib/Legend.abide
|
|
8711
8831
|
<span class="badge">Overdue</span>
|
|
8712
8832
|
|
|
8713
8833
|
<style>
|
|
@@ -8762,24 +8882,29 @@ plugins = ["bun-plugin-tailwind"]
|
|
|
8762
8882
|
|
|
8763
8883
|
A `.abide` file's `<script>` block is the component's setup. It runs when an instance is created, in order, top to bottom — there is no mount hook to register and nothing to call it from, because the block **is** the hook.
|
|
8764
8884
|
|
|
8765
|
-
##
|
|
8885
|
+
## Three script blocks, and when each runs
|
|
8766
8886
|
|
|
8767
8887
|
| You write | Runs |
|
|
8768
8888
|
| --- | --- |
|
|
8769
8889
|
| `<script>` | once per instance of the component |
|
|
8770
8890
|
| `<script module>` | once for the module, on a server as in a browser |
|
|
8891
|
+
| `<script server>` | once for the module, on the server alone |
|
|
8771
8892
|
|
|
8772
|
-
The difference is what a variable declared in each belongs to. One row's `calls` is that row's; a lookup table every row shares belongs to the module and is built once however many rows there are.
|
|
8893
|
+
The difference between the first two is what a variable declared in each belongs to. One row's `calls` is that row's; a lookup table every row shares belongs to the module and is built once however many rows there are.
|
|
8773
8894
|
|
|
8774
|
-
|
|
8895
|
+
The third never reaches a browser. It holds the file's own handlers and its hooks, and the setup calls a handler it declares by name.
|
|
8896
|
+
|
|
8897
|
+
None of the three exports anything — the component is the file's one export — so a helper another file imports goes in a module under `lib`.
|
|
8775
8898
|
|
|
8776
8899
|
On a server, module scope is shared by every request. A `reactive` there is still one per request, as a `reactive` is wherever it is declared. A plain `let` is the process's, so state that changes while a page renders belongs in a `reactive`, as `opened` is below.
|
|
8777
8900
|
|
|
8901
|
+
Read on: [Reading data](/data/read-data-without-writing-an-api.md)
|
|
8902
|
+
|
|
8778
8903
|
## Per instance, and once
|
|
8779
8904
|
|
|
8780
8905
|
**`<script>` and `<script module>` — per instance, and once** — Two contact rows from one component. Log a call on the first and read both rows.
|
|
8781
8906
|
|
|
8782
|
-
```abide Contacts.abide
|
|
8907
|
+
```abide lib/Contacts.abide
|
|
8783
8908
|
<script>
|
|
8784
8909
|
import Row from './Row.abide'
|
|
8785
8910
|
</script>
|
|
@@ -8790,7 +8915,7 @@ import Row from './Row.abide'
|
|
|
8790
8915
|
</ul>
|
|
8791
8916
|
```
|
|
8792
8917
|
|
|
8793
|
-
```abide Row.abide
|
|
8918
|
+
```abide lib/Row.abide
|
|
8794
8919
|
<script module>
|
|
8795
8920
|
import { reactive } from '@abide/abide'
|
|
8796
8921
|
|
|
@@ -8903,13 +9028,13 @@ An environment variable is a string or it is missing. What an app wants is a num
|
|
|
8903
9028
|
|
|
8904
9029
|
**`onConfig(fn)` and `config()` — a default the deployment overrides** — A CRM's retry count: 3 beside the handler, 5 in the deployment's .env. The page adds one for the first try and gets 6, not 51.
|
|
8905
9030
|
|
|
8906
|
-
```abide Retries.abide
|
|
9031
|
+
```abide lib/Retries.abide
|
|
8907
9032
|
<script server>
|
|
8908
9033
|
import { config, GET, onConfig } from '@abide/abide/server'
|
|
8909
9034
|
|
|
8910
9035
|
onConfig(() => ({ RETRIES: 3 }))
|
|
8911
9036
|
|
|
8912
|
-
|
|
9037
|
+
const retryPolicy = GET(() => ({ retries: config().RETRIES }), {
|
|
8913
9038
|
description: 'How many times a call is retried.',
|
|
8914
9039
|
})
|
|
8915
9040
|
</script>
|
|
@@ -9018,7 +9143,7 @@ A `<script server>` runs once, on the server, before the first request. The root
|
|
|
9018
9143
|
|
|
9019
9144
|
**`onStart(fn)` — work the first request cannot race** — A CRM's database pool, connected before the app answers anything. Reload the page and both times stay where the boot put them.
|
|
9020
9145
|
|
|
9021
|
-
```abide Pool.abide
|
|
9146
|
+
```abide lib/Pool.abide
|
|
9022
9147
|
<script server>
|
|
9023
9148
|
import { GET, onStart, onStop } from '@abide/abide/server'
|
|
9024
9149
|
|
|
@@ -9049,7 +9174,7 @@ onStop(async (stop) => {
|
|
|
9049
9174
|
})
|
|
9050
9175
|
|
|
9051
9176
|
/** When the pool connected, and when the app came up around it. */
|
|
9052
|
-
|
|
9177
|
+
const poolState = GET(
|
|
9053
9178
|
() => ({ connectedAt: pool.connectedAt, upAt: pool.upAt }),
|
|
9054
9179
|
{ description: 'When the pool connected, and when the app came up.' },
|
|
9055
9180
|
)
|
|
@@ -9080,7 +9205,7 @@ Failing hard is the only useful answer to a failed migration. An app that starts
|
|
|
9080
9205
|
|
|
9081
9206
|
## `onStop` closes what `onStart` opened, after the last request
|
|
9082
9207
|
|
|
9083
|
-
```abide Pool.abide — excerpt
|
|
9208
|
+
```abide lib/Pool.abide — excerpt
|
|
9084
9209
|
onStop(async (stop) => {
|
|
9085
9210
|
await stop()
|
|
9086
9211
|
await pool.close()
|
|
@@ -9104,27 +9229,10 @@ Read on: [Config](/app/configure-the-app.md) · [Authorization](/data/decide-who
|
|
|
9104
9229
|
|
|
9105
9230
|
## A root rung answers what no route does
|
|
9106
9231
|
|
|
9107
|
-
`middleware` filed at the root route runs before routing, so a rung there can answer an address no page or handler claims, and hand everything else on with `next()`. These docs serve every page as markdown that way:
|
|
9108
|
-
|
|
9109
|
-
```
|
|
9110
|
-
|
|
9111
|
-
const { pathname, origin } = new URL(request.url)
|
|
9112
|
-
if (!pathname.endsWith('.md') && !pathname.endsWith('/llms.txt'))
|
|
9113
|
-
return next()
|
|
9114
|
-
if (pathname === '/llms.txt')
|
|
9115
|
-
return new Response('# abide\n\nEvery page, as markdown: abide.md\n')
|
|
9116
|
-
if (pathname === '/abide.md') {
|
|
9117
|
-
bundled ??= bundle(origin)
|
|
9118
|
-
return new Response(await bundled, { headers: MARKDOWN })
|
|
9119
|
-
}
|
|
9120
|
-
for (const group of SITE)
|
|
9121
|
-
for (const page of group.pages)
|
|
9122
|
-
if (pathname === markdownAddressOf(page.route))
|
|
9123
|
-
return new Response(await pageMarkdown(page.route, origin), {
|
|
9124
|
-
headers: MARKDOWN,
|
|
9125
|
-
})
|
|
9126
|
-
return next()
|
|
9127
|
-
})
|
|
9232
|
+
`middleware` filed at the root route runs before routing, so a rung there can answer an address no page or handler claims, and hand everything else on with `next()`. These docs serve every page as markdown that way — the rung is a module's export, and the root layout files it with `middleware(serveDownloads)`, since a hook is only called in a page, a layout or a handler file (37.13):
|
|
9233
|
+
|
|
9234
|
+
```ts src/lib/serveDownloads.ts — excerpt
|
|
9235
|
+
export const serveDownloads: Middleware<
|
|
9128
9236
|
```
|
|
9129
9237
|
|
|
9130
9238
|
It uses the same response helpers everything else does, there being no second set.
|
|
@@ -9151,7 +9259,7 @@ abide does not know how you authenticate somebody. It knows what to do **after**
|
|
|
9151
9259
|
|
|
9152
9260
|
**`principal.set` — one seal, read everywhere** — A sign-in on a CRM, and the header above it. Sign in, then sign out, and watch a component the form never names.
|
|
9153
9261
|
|
|
9154
|
-
```abide SignIn.abide
|
|
9262
|
+
```abide lib/SignIn.abide
|
|
9155
9263
|
<script server>
|
|
9156
9264
|
import { principal, refuse } from '@abide/abide'
|
|
9157
9265
|
import { POST } from '@abide/abide/server'
|
|
@@ -9167,7 +9275,7 @@ async function userByEmail(email: string) {
|
|
|
9167
9275
|
return USERS[email] ?? null
|
|
9168
9276
|
}
|
|
9169
9277
|
|
|
9170
|
-
|
|
9278
|
+
const signIn = POST(
|
|
9171
9279
|
async ({ email }: { email: string }) => {
|
|
9172
9280
|
const user = await userByEmail(email)
|
|
9173
9281
|
if (user === null) return refuse(401, 'No such account.')
|
|
@@ -9176,7 +9284,7 @@ export const signIn = POST(
|
|
|
9176
9284
|
{ description: 'Signs in by email.' },
|
|
9177
9285
|
)
|
|
9178
9286
|
|
|
9179
|
-
|
|
9287
|
+
const signOut = POST(() => principal.clear(), {
|
|
9180
9288
|
description: 'Signs out.',
|
|
9181
9289
|
})
|
|
9182
9290
|
</script>
|
|
@@ -9200,7 +9308,7 @@ let email = reactive('ada@example.com')
|
|
|
9200
9308
|
</div>
|
|
9201
9309
|
```
|
|
9202
9310
|
|
|
9203
|
-
```abide Header.abide
|
|
9311
|
+
```abide lib/Header.abide
|
|
9204
9312
|
<script>
|
|
9205
9313
|
// The seal is ambient, so this reads it with nothing handed down.
|
|
9206
9314
|
import { principal } from '@abide/abide'
|
|
@@ -9240,7 +9348,7 @@ Each is a `Reactive`, so reading one in a template subscribes and a sign-out mov
|
|
|
9240
9348
|
|
|
9241
9349
|
**`onPrincipal(fn)` — an id in the cookie, a user on the page** — A CRM that seals nothing but an account id. Sign in as either account and the name and role arrive anyway, looked up on the server.
|
|
9242
9350
|
|
|
9243
|
-
```abide Badge.abide
|
|
9351
|
+
```abide lib/Badge.abide
|
|
9244
9352
|
<script server>
|
|
9245
9353
|
import { onPrincipal, POST, principal } from '@abide/abide/server'
|
|
9246
9354
|
|
|
@@ -9270,7 +9378,7 @@ onPrincipal(async (claims) => {
|
|
|
9270
9378
|
})
|
|
9271
9379
|
|
|
9272
9380
|
/** Seals the id alone, and answers with what `onPrincipal` made of it. */
|
|
9273
|
-
|
|
9381
|
+
const signInAs = POST(
|
|
9274
9382
|
async ({ id }: { id: string }) => {
|
|
9275
9383
|
await principal.set({ id })
|
|
9276
9384
|
const { name = '', role = '' } = principal.resolved()
|
|
@@ -9322,7 +9430,7 @@ type Principal = { authenticated, expiresAt?, error?, …claims }
|
|
|
9322
9430
|
|
|
9323
9431
|
**`cookies()` — one jar, read on both sides** — The contact list's density, kept in a cookie. Pick one and reload the page — the server paints the one you picked.
|
|
9324
9432
|
|
|
9325
|
-
```abide Density.abide
|
|
9433
|
+
```abide lib/Density.abide
|
|
9326
9434
|
<script>
|
|
9327
9435
|
import { cookies, reactive } from '@abide/abide'
|
|
9328
9436
|
|
|
@@ -9369,13 +9477,11 @@ Three ambient values, all server-side, all **request-local**: the request being
|
|
|
9369
9477
|
|
|
9370
9478
|
## `request()` is the real `Request`
|
|
9371
9479
|
|
|
9372
|
-
```
|
|
9373
|
-
|
|
9374
|
-
export const greeting = GET(() => {
|
|
9480
|
+
```ts src/invoices/greeting.ts — excerpt
|
|
9481
|
+
export default GET(() => {
|
|
9375
9482
|
const locale = request().headers.get('accept-language') ?? 'en'
|
|
9376
9483
|
return locale.startsWith('fr') ? 'Bonjour' : 'Hello'
|
|
9377
9484
|
})
|
|
9378
|
-
</script>
|
|
9379
9485
|
```
|
|
9380
9486
|
|
|
9381
9487
|
It is the platform's own `Request`, not a wrapper — so everything you already know about one applies, and anything that takes a `Request` takes this.
|
|
@@ -9390,7 +9496,7 @@ Read on: [Authorization](/data/decide-who-may-call-what.md) · [Schemas](/data/c
|
|
|
9390
9496
|
|
|
9391
9497
|
**`response()` — a status the handler decides** — An integration adds contacts through the API, and expects 201 and where the new record lives.
|
|
9392
9498
|
|
|
9393
|
-
```abide AddContact.abide
|
|
9499
|
+
```abide lib/AddContact.abide
|
|
9394
9500
|
<script server>
|
|
9395
9501
|
import { POST, response } from '@abide/abide/server'
|
|
9396
9502
|
|
|
@@ -9404,7 +9510,7 @@ async function insertContact(name: string) {
|
|
|
9404
9510
|
return { id, name }
|
|
9405
9511
|
}
|
|
9406
9512
|
|
|
9407
|
-
|
|
9513
|
+
const addContact = POST(
|
|
9408
9514
|
async ({ name }: { name: string }) => {
|
|
9409
9515
|
const contact = await insertContact(name)
|
|
9410
9516
|
response().status = 201
|
|
@@ -9496,7 +9602,7 @@ log.error('mailer refused', { error })
|
|
|
9496
9602
|
|
|
9497
9603
|
**`log.named` and `DEBUG` — one namespace on, one off** — A CRM's own log. Send an invoice, then press Poll — only one of them has a namespace that is on.
|
|
9498
9604
|
|
|
9499
|
-
```abide Console.abide
|
|
9605
|
+
```abide lib/Console.abide
|
|
9500
9606
|
<script>
|
|
9501
9607
|
import { log } from '@abide/abide'
|
|
9502
9608
|
|
|
@@ -9569,7 +9675,7 @@ type LogRecord = {
|
|
|
9569
9675
|
|
|
9570
9676
|
In a template it is a member access rather than a name, so the read is spelled out:
|
|
9571
9677
|
|
|
9572
|
-
```abide Console.abide — excerpt
|
|
9678
|
+
```abide lib/Console.abide — excerpt
|
|
9573
9679
|
{#for record of log.records().slice(-20)}
|
|
9574
9680
|
```
|
|
9575
9681
|
|
|
@@ -9605,12 +9711,12 @@ Every log line an abide app writes carries a `trace`. It is the id of the operat
|
|
|
9605
9711
|
|
|
9606
9712
|
**`trace()` — one id for everything one request did** — A contact synced to billing across two modules. Sync it, and both lines it logged carry that request's trace; sync again for a new one.
|
|
9607
9713
|
|
|
9608
|
-
```abide Sync.abide
|
|
9714
|
+
```abide lib/Sync.abide
|
|
9609
9715
|
<script server>
|
|
9610
9716
|
import { log, POST, trace } from '@abide/abide/server'
|
|
9611
9717
|
import { billing } from './billing'
|
|
9612
9718
|
|
|
9613
|
-
|
|
9719
|
+
const syncContact = POST(
|
|
9614
9720
|
async ({ id }: { id: string }) => {
|
|
9615
9721
|
log(`contact ${id} loaded`)
|
|
9616
9722
|
await billing.notify(id)
|
|
@@ -9650,7 +9756,7 @@ async function sync() {
|
|
|
9650
9756
|
</ul>
|
|
9651
9757
|
```
|
|
9652
9758
|
|
|
9653
|
-
```ts billing.ts
|
|
9759
|
+
```ts lib/billing.ts
|
|
9654
9760
|
import { log } from '@abide/abide/server'
|
|
9655
9761
|
|
|
9656
9762
|
/** Another module the sync reaches, handed the contact and nothing else. */
|
|
@@ -9771,7 +9877,7 @@ Only what finished **before the head went out** can be in it. A page sends its h
|
|
|
9771
9877
|
|
|
9772
9878
|
**`onHealth(fn)` — your fields beside the baseline** — The account a load balancer scrapes, read on a CRM's status panel through health(). The database line is the app's own; the rest is abide's.
|
|
9773
9879
|
|
|
9774
|
-
```abide Account.abide
|
|
9880
|
+
```abide lib/Account.abide
|
|
9775
9881
|
<script server>
|
|
9776
9882
|
import { onHealth, onStart, onStop } from '@abide/abide/server'
|
|
9777
9883
|
|
|
@@ -9823,7 +9929,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [Logging](/app/recor
|
|
|
9823
9929
|
`health` is an `Rpc`, imported from `@abide/abide` and called the same way on both sides: `health()` in a page, `await
|
|
9824
9930
|
health()` in a handler. The call is what asks, so a page that never makes it loads no account.
|
|
9825
9931
|
|
|
9826
|
-
```abide Account.abide — excerpt
|
|
9932
|
+
```abide lib/Account.abide — excerpt
|
|
9827
9933
|
const account = health()
|
|
9828
9934
|
```
|
|
9829
9935
|
|
|
@@ -9856,7 +9962,7 @@ A content security policy is worth having and painful to write, because the hard
|
|
|
9856
9962
|
|
|
9857
9963
|
**`csp()` — your own scripts run, a stranger's do not** — A support widget somebody pasted into the CRM from another origin. Add it, and the policy this page was served with refuses it.
|
|
9858
9964
|
|
|
9859
|
-
```abide Widget.abide
|
|
9965
|
+
```abide lib/Widget.abide
|
|
9860
9966
|
<script>
|
|
9861
9967
|
import { reactive } from '@abide/abide'
|
|
9862
9968
|
|
|
@@ -9883,21 +9989,24 @@ let refused = reactive('—')
|
|
|
9883
9989
|
<script server>
|
|
9884
9990
|
// The docs app's own hooks; each example files its own in its own file.
|
|
9885
9991
|
import { csp, middleware, onConfig } from '@abide/abide/server'
|
|
9992
|
+
import { serveDownloads } from '#src/lib/serveDownloads.ts'
|
|
9886
9993
|
|
|
9887
9994
|
// The sign-in examples need a secret: one per process, or the deployment's.
|
|
9888
9995
|
const PRINCIPAL_SECRET = crypto.randomUUID()
|
|
9889
9996
|
|
|
9890
9997
|
onConfig(() => ({ ABIDE_PRINCIPAL_SECRET: PRINCIPAL_SECRET }))
|
|
9891
9998
|
|
|
9999
|
+
middleware(serveDownloads)
|
|
10000
|
+
|
|
9892
10001
|
// Every docs page's policy; 'self' so the docs can frame invoice previews.
|
|
9893
10002
|
middleware(csp({ 'frame-ancestors': ["'self'"] }))
|
|
9894
10003
|
</script>
|
|
9895
10004
|
|
|
9896
10005
|
<script>
|
|
9897
|
-
import DocsNav from '#src/
|
|
9898
|
-
import SiteBar from '#src/
|
|
10006
|
+
import DocsNav from '#src/lib/DocsNav.abide'
|
|
10007
|
+
import SiteBar from '#src/lib/SiteBar.abide'
|
|
9899
10008
|
import { url } from '@abide/abide'
|
|
9900
|
-
import '#src/app.css'
|
|
10009
|
+
import '#src/lib/app.css'
|
|
9901
10010
|
</script>
|
|
9902
10011
|
|
|
9903
10012
|
<html lang="en">
|
|
@@ -10016,7 +10125,7 @@ There is one form and no options, `online` being ambient rather than declared: i
|
|
|
10016
10125
|
|
|
10017
10126
|
**`online` — the connection, read by name** — An order form with unsaved changes. Take the browser offline — DevTools' Network panel, or the Wi-Fi — and watch the line.
|
|
10018
10127
|
|
|
10019
|
-
```abide OfflineBanner.abide
|
|
10128
|
+
```abide lib/OfflineBanner.abide
|
|
10020
10129
|
<script>
|
|
10021
10130
|
import { online } from '@abide/abide'
|
|
10022
10131
|
</script>
|
|
@@ -10070,7 +10179,7 @@ See [Reloading](/values/decide-when-a-value-reloads.md) for the two in full.
|
|
|
10070
10179
|
|
|
10071
10180
|
That shapes what `online` is good for. It is right for telling a reader what to expect and wrong as the gate on a write: the load that failed is the honest signal, and `online` explains it. Read the failure, then read `online` to say why.
|
|
10072
10181
|
|
|
10073
|
-
```abide src/orders/
|
|
10182
|
+
```abide src/orders/index.abide — excerpt
|
|
10074
10183
|
{#if orders.error()}
|
|
10075
10184
|
<p>{online ? 'Could not load orders.' : 'You are offline.'}</p>
|
|
10076
10185
|
{/if}
|
|
@@ -10107,7 +10216,7 @@ Nothing here asks you to annotate a handler for machines specifically. The piece
|
|
|
10107
10216
|
|
|
10108
10217
|
| Piece | Where it comes from |
|
|
10109
10218
|
| --- | --- |
|
|
10110
|
-
| address | the file path
|
|
10219
|
+
| address | the handler file's path |
|
|
10111
10220
|
| method | which handler gave it an address |
|
|
10112
10221
|
| argument schema | the annotations you already wrote, or the schema you named |
|
|
10113
10222
|
| result schema | the handler's return type |
|
|
@@ -10183,11 +10292,11 @@ Read on: [Withholding a surface](/machines/keep-a-handler-off-a-surface.md)
|
|
|
10183
10292
|
|
|
10184
10293
|
An MCP server is a list of tools with typed arguments and descriptions. A declared handler is a typed argument shape with a description. `/__abide/mcp` is the second read as the first, over streamable http.
|
|
10185
10294
|
|
|
10186
|
-
##
|
|
10295
|
+
## Every handler is a tool
|
|
10187
10296
|
|
|
10188
|
-
|
|
10297
|
+
Every handler the route table publishes is a **tool** — a read that takes no arguments as much as a mutation. None is published as an MCP resource, because a client that only calls tools, Claude Code under `abide-bridge` among them, would never reach a handler listed as one.
|
|
10189
10298
|
|
|
10190
|
-
That is the whole of the mapping
|
|
10299
|
+
That is the whole of the mapping: the model sees every handler your app publishes, and whether one changes something is the handler's own method, not a second listing.
|
|
10191
10300
|
|
|
10192
10301
|
## The description is the handler's
|
|
10193
10302
|
|
|
@@ -10258,8 +10367,7 @@ A `Provider` is one method, `run`, handed the transcript and a `Toolbox` of your
|
|
|
10258
10367
|
|
|
10259
10368
|
`assistant` has no address of its own. You yield it from a streaming handler, so the address, the middleware and the surfaces are yours to set, as they are for any other handler.
|
|
10260
10369
|
|
|
10261
|
-
```
|
|
10262
|
-
<script server>
|
|
10370
|
+
```ts src/support/ask.ts — excerpt
|
|
10263
10371
|
import {
|
|
10264
10372
|
assistant,
|
|
10265
10373
|
type AssistantMessage,
|
|
@@ -10269,7 +10377,7 @@ import {
|
|
|
10269
10377
|
} from '@abide/abide/server'
|
|
10270
10378
|
import { onlyStaff } from '#src/server/onlyStaff'
|
|
10271
10379
|
|
|
10272
|
-
export
|
|
10380
|
+
export default POST(
|
|
10273
10381
|
async function* ({ messages }: { messages: AssistantMessage[] }) {
|
|
10274
10382
|
yield* assistant(
|
|
10275
10383
|
messages,
|
|
@@ -10284,7 +10392,6 @@ export const ask = POST(
|
|
|
10284
10392
|
},
|
|
10285
10393
|
{ middleware: [onlyStaff], clients: { mcp: false } },
|
|
10286
10394
|
)
|
|
10287
|
-
</script>
|
|
10288
10395
|
```
|
|
10289
10396
|
|
|
10290
10397
|
`clients: { mcp: false }` keeps the assistant out of its own tool list. The page reads `ask` like any stream: each event arrives as a production of the value it hands back.
|
|
@@ -10306,23 +10413,16 @@ Changing the provider changes nothing downstream of the handler. A page written
|
|
|
10306
10413
|
|
|
10307
10414
|
**`assistant(messages, provider, { mcp: true })` — your handlers as its tools** — The support assistant, asked about one invoice. The model here is a recording and its lines are fixed; the tool call it makes runs this site's own handler, as you.
|
|
10308
10415
|
|
|
10309
|
-
```abide Chat.abide
|
|
10416
|
+
```abide lib/Chat.abide
|
|
10310
10417
|
<script server>
|
|
10311
10418
|
import {
|
|
10312
10419
|
type AssistantMessage,
|
|
10313
10420
|
assistant,
|
|
10314
|
-
GET,
|
|
10315
10421
|
POST,
|
|
10316
10422
|
type Provider,
|
|
10317
10423
|
} from '@abide/abide/server'
|
|
10318
10424
|
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
export const invoiceStatus = GET(
|
|
10322
|
-
({ id }: { id: string }) => ({ id, paid: PAID.has(id) }),
|
|
10323
|
-
{ description: 'Whether an invoice has been paid, by its number.' },
|
|
10324
|
-
)
|
|
10325
|
-
|
|
10425
|
+
// The MCP tool `invoiceStatus.ts` beside the page is listed as.
|
|
10326
10426
|
const TOOL = 'get_machines_put-a-claude-assistant-on-your-site_invoiceStatus'
|
|
10327
10427
|
|
|
10328
10428
|
// The model is a recording; the tool call it asks for is made for real.
|
|
@@ -10337,7 +10437,7 @@ const recorded: Provider = {
|
|
|
10337
10437
|
},
|
|
10338
10438
|
}
|
|
10339
10439
|
|
|
10340
|
-
|
|
10440
|
+
const askWithTools = POST(
|
|
10341
10441
|
async function* ({ messages }: { messages: AssistantMessage[] }) {
|
|
10342
10442
|
yield* assistant(messages, recorded, { mcp: true })
|
|
10343
10443
|
},
|
|
@@ -10381,6 +10481,16 @@ const events = reactive(() =>
|
|
|
10381
10481
|
</div>
|
|
10382
10482
|
```
|
|
10383
10483
|
|
|
10484
|
+
```ts invoiceStatus.ts
|
|
10485
|
+
import { GET } from '@abide/abide/server'
|
|
10486
|
+
|
|
10487
|
+
const PAID = new Set(['INV-4021'])
|
|
10488
|
+
|
|
10489
|
+
export default GET(({ id }: { id: string }) => ({ id, paid: PAID.has(id) }), {
|
|
10490
|
+
description: 'Whether an invoice has been paid, by its number.',
|
|
10491
|
+
})
|
|
10492
|
+
```
|
|
10493
|
+
|
|
10384
10494
|
*1 option, 0 tool definitions* — against the arm's tool table, which spells each tool's name, description and input schema a second time and dispatches the call by name.
|
|
10385
10495
|
|
|
10386
10496
|
`mcp: true` offers the model the tools `/__abide/mcp` lists, so which handlers it may call is decided where it already is — `clients: { mcp: false }` withholds one from the assistant as it does from every other model. Asking for tools in an app that lists no handler for MCP throws and says so.
|
|
@@ -10399,7 +10509,7 @@ The cost is the resend: every call carries the whole transcript, and the API bil
|
|
|
10399
10509
|
|
|
10400
10510
|
**`claudeApi(config)` — the request body you wrote, sent as written** — The support assistant on claudeApi, its baseURL pointed at a stand-in on this site that answers with the body it was sent. Ask, and read the request.
|
|
10401
10511
|
|
|
10402
|
-
```abide ApiRequest.abide
|
|
10512
|
+
```abide lib/ApiRequest.abide
|
|
10403
10513
|
<script server>
|
|
10404
10514
|
import {
|
|
10405
10515
|
type AssistantMessage,
|
|
@@ -10408,9 +10518,9 @@ import {
|
|
|
10408
10518
|
POST,
|
|
10409
10519
|
request,
|
|
10410
10520
|
} from '@abide/abide/server'
|
|
10411
|
-
import
|
|
10521
|
+
import standIn from '../v1/messages.ts'
|
|
10412
10522
|
|
|
10413
|
-
|
|
10523
|
+
const askTheApi = POST(
|
|
10414
10524
|
async function* ({ messages }: { messages: AssistantMessage[] }) {
|
|
10415
10525
|
// This site's stand-in for https://api.anthropic.com.
|
|
10416
10526
|
const standInAt = new URL(standIn.url({}), request().url)
|
|
@@ -10456,12 +10566,11 @@ const events = reactive(() =>
|
|
|
10456
10566
|
{/for}
|
|
10457
10567
|
```
|
|
10458
10568
|
|
|
10459
|
-
```
|
|
10460
|
-
<script server>
|
|
10569
|
+
```ts v1/messages.ts
|
|
10461
10570
|
import { POST, sse } from '@abide/abide/server'
|
|
10462
10571
|
|
|
10463
10572
|
// A stand-in for the Messages API, streaming back the body it was sent.
|
|
10464
|
-
export
|
|
10573
|
+
export default POST(
|
|
10465
10574
|
(asked: Record<string, unknown>) =>
|
|
10466
10575
|
sse([
|
|
10467
10576
|
{ type: 'message_start', message: { usage: { input_tokens: 0 } } },
|
|
@@ -10487,7 +10596,6 @@ export const messages = POST(
|
|
|
10487
10596
|
]),
|
|
10488
10597
|
{ clients: { ui: false, mcp: false, cli: false, openapi: false } },
|
|
10489
10598
|
)
|
|
10490
|
-
</script>
|
|
10491
10599
|
```
|
|
10492
10600
|
|
|
10493
10601
|
`claudeApi` takes a Messages API request body as Anthropic documents it, snake_case, with the client's `apiKey` and `baseURL` beside it. abide writes `messages` and `stream`; every other field goes on the body as you wrote it, and the `tools` you list — a server tool like web search — are sent beside your own.
|
|
@@ -10502,7 +10610,7 @@ Read on: [Config](/app/configure-the-app.md)
|
|
|
10502
10610
|
|
|
10503
10611
|
**`claudeCode(config)` — a turn on the reader's own Claude Code** — The support assistant on whatever Claude Code is paired with this browser. Ask first and it is refused; connect, run the command in a terminal, and ask again.
|
|
10504
10612
|
|
|
10505
|
-
```abide Bridge.abide
|
|
10613
|
+
```abide lib/Bridge.abide
|
|
10506
10614
|
<script server>
|
|
10507
10615
|
import {
|
|
10508
10616
|
type AssistantMessage,
|
|
@@ -10512,12 +10620,12 @@ import {
|
|
|
10512
10620
|
POST,
|
|
10513
10621
|
} from '@abide/abide/server'
|
|
10514
10622
|
|
|
10515
|
-
|
|
10623
|
+
const bridge = GET(() => claudeCode.bridge(), {
|
|
10516
10624
|
timeout: Infinity,
|
|
10517
10625
|
clients: { mcp: false },
|
|
10518
10626
|
})
|
|
10519
10627
|
|
|
10520
|
-
|
|
10628
|
+
const askClaudeCode = POST(
|
|
10521
10629
|
async function* ({ messages }: { messages: AssistantMessage[] }) {
|
|
10522
10630
|
yield* assistant(
|
|
10523
10631
|
messages,
|
|
@@ -10586,8 +10694,8 @@ The relay at `/__abide/bridge` opens the first time `claudeCode` hands out a com
|
|
|
10586
10694
|
|
|
10587
10695
|
`claudeCode.bridge()` streams this browser's pairing: unpaired with the command to run, then paired the moment the visitor's bridge connects.
|
|
10588
10696
|
|
|
10589
|
-
```abide Bridge.abide — excerpt
|
|
10590
|
-
|
|
10697
|
+
```abide lib/Bridge.abide — excerpt
|
|
10698
|
+
const bridge = GET(() => claudeCode.bridge(), {
|
|
10591
10699
|
timeout: Infinity,
|
|
10592
10700
|
clients: { mcp: false },
|
|
10593
10701
|
})
|
|
@@ -10620,20 +10728,26 @@ A handler declared once is reached four ways: imported by a page, listed in an O
|
|
|
10620
10728
|
type Clients = { ui?: boolean; mcp?: boolean; cli?: boolean; openapi?: boolean }
|
|
10621
10729
|
```
|
|
10622
10730
|
|
|
10623
|
-
```
|
|
10624
|
-
|
|
10625
|
-
export const rebuildSearchIndex = POST(rebuild, {
|
|
10731
|
+
```ts src/admin/rebuildSearchIndex.ts — excerpt
|
|
10732
|
+
export default POST(rebuild, {
|
|
10626
10733
|
clients: { ui: false, mcp: false },
|
|
10627
10734
|
})
|
|
10628
|
-
</script>
|
|
10629
10735
|
```
|
|
10630
10736
|
|
|
10631
10737
|
## Every key defaults to true
|
|
10632
10738
|
|
|
10633
|
-
A handler
|
|
10739
|
+
A handler file is on every surface unless its `clients` says otherwise (20.2). That is the right default because the alternative — opting each handler into each surface — is four decisions per handler and a fifth surface later that nothing is on.
|
|
10634
10740
|
|
|
10635
10741
|
Every surface is **derived from the route table** rather than authored beside it, so a handler added is a handler listed, and there is no second file to keep in step.
|
|
10636
10742
|
|
|
10743
|
+
## A private handler is on no surface
|
|
10744
|
+
|
|
10745
|
+
A handler a `<script server>` declares is not listed on OpenAPI, on MCP or by the CLI, whatever its `clients` says (16.71). Its file exports nothing (34.17), so no other page can import it either: the one caller is the file that declared it. So `clients` is a handler file's option, and the first decision is which of the two to write: a handler only one file calls stays in that file, and one meant for anything else is a handler file.
|
|
10746
|
+
|
|
10747
|
+
Off every surface is still not off the network. A private handler answers http at its address below `/__abide/handlers`, under the rungs filed over its file (16.72).
|
|
10748
|
+
|
|
10749
|
+
Read on: [Reading data](/data/read-data-without-writing-an-api.md)
|
|
10750
|
+
|
|
10637
10751
|
## `clients` decides what is generated, never who may call
|
|
10638
10752
|
|
|
10639
10753
|
**`clients` is not access control.** A handler with `mcp:
|
|
@@ -10738,11 +10852,11 @@ abide dev
|
|
|
10738
10852
|
|
|
10739
10853
|
| | |
|
|
10740
10854
|
| --- | --- |
|
|
10741
|
-
| `src/
|
|
10855
|
+
| `src/index.abide` | the page at `/` |
|
|
10742
10856
|
| `src/layout.abide` | the root layout: the document its pages are served in, and the `<script server>` holding the lifecycle hooks |
|
|
10743
|
-
| `package.json`, `tsconfig.json`, `biome.json` | the toolchain, configured |
|
|
10857
|
+
| `package.json`, `tsconfig.json`, `biome.json`, `bunfig.toml` | the toolchain, configured — tests included |
|
|
10744
10858
|
|
|
10745
|
-
`#src/*` is the one import alias, mapping onto `src/`. Grow the app by subject — a `src/billing/` with its own pages, components and `server` — and its pages answer below `/billing`, with no file moved to make room.
|
|
10859
|
+
`#src/*` is the one import alias, mapping onto `src/`. Grow the app by subject — a `src/billing/` with its own pages and handlers, a `lib` for its components and a `server` — and its pages answer below `/billing`, with no file moved to make room.
|
|
10746
10860
|
|
|
10747
10861
|
Nothing generated is a framework file you are asked not to edit. The layout is a real component writing a real HTML document, and the page is a component too.
|
|
10748
10862
|
|
|
@@ -10797,7 +10911,7 @@ Read on: [Config](/app/configure-the-app.md) · [Build & start](/ship/build-and-
|
|
|
10797
10911
|
abide run scripts/backfill.ts --since 2026-01-01
|
|
10798
10912
|
```
|
|
10799
10913
|
|
|
10800
|
-
Runs a script under the abide runtime, which is what lets a one-off import `#src/server/database` and `#src/failures` without a build and without a second tsconfig.
|
|
10914
|
+
Runs a script under the abide runtime, which is what lets a one-off import `#src/server/database` and `#src/lib/failures` without a build and without a second tsconfig.
|
|
10801
10915
|
|
|
10802
10916
|
That is the shape a backfill, a migration and a fixture loader want. They are not endpoints and should not become endpoints just to reach the app's own modules — which is what happens in a stack where the only thing that can import them is the server.
|
|
10803
10917
|
|
|
@@ -10927,9 +11041,12 @@ Every one of these is caught before the code runs, which is why the ergonomic sp
|
|
|
10927
11041
|
| a `Failed` built and discarded | the refusal did not happen |
|
|
10928
11042
|
| a `reactive` handed to two handlers | names both |
|
|
10929
11043
|
| a nested object argument on a `GET` or `DELETE` | names the method, and says a `POST` is the fix |
|
|
10930
|
-
| two files
|
|
11044
|
+
| two files answering one address | names both files |
|
|
11045
|
+
| a stylesheet or a helper directly beside the pages | names the file, and the `lib` to move it into |
|
|
11046
|
+
| a handler file exporting anything but its handler | names the export, and where it belongs instead |
|
|
11047
|
+
| an `export` in a `.abide` | the component is the file's one export, and a handler another file calls is a handler file |
|
|
10931
11048
|
|
|
10932
|
-
The last
|
|
11049
|
+
The last six are build errors rather than type errors, and they are here for the same reason: each is a mistake that otherwise fails at the first request, or worse, quietly succeeds.
|
|
10933
11050
|
|
|
10934
11051
|
Read on: [`.abide` files](/reference/abide-files.md) · [Failures](/data/refuse-a-request-and-say-why.md)
|
|
10935
11052
|
|
|
@@ -10952,13 +11069,20 @@ A hover, a jump to a definition and the members offered after a `.` are answered
|
|
|
10952
11069
|
|
|
10953
11070
|
Read on: [CLI](/reference/cli.md)
|
|
10954
11071
|
|
|
10955
|
-
##
|
|
11072
|
+
## A handler another file calls is a `.ts`
|
|
11073
|
+
|
|
11074
|
+
A `.abide` exports its component and nothing else, so a handler a second file calls is a handler file — a `.ts` — and `tsc` reads it as it reads any other. A `.ts` importing it is typed with nothing generated in between and nothing to keep current.
|
|
11075
|
+
|
|
11076
|
+
## `abide routes` is the table the build will serve
|
|
10956
11077
|
|
|
10957
11078
|
```
|
|
10958
|
-
abide
|
|
11079
|
+
abide routes
|
|
11080
|
+
abide routes --json
|
|
10959
11081
|
```
|
|
10960
11082
|
|
|
10961
|
-
|
|
11083
|
+
Every address the app answers, the method and the file behind each, and the file filing each hook. It is the build's own reading of the tree, so an address you meant to add and did not is missing here before it is missing in production.
|
|
11084
|
+
|
|
11085
|
+
Read on: [Routes](/pages/add-a-page.md) · [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
|
|
10962
11086
|
|
|
10963
11087
|
## In CI
|
|
10964
11088
|
|
|
@@ -10966,7 +11090,7 @@ abide sync
|
|
|
10966
11090
|
abide check && abide build && bun test
|
|
10967
11091
|
```
|
|
10968
11092
|
|
|
10969
|
-
`check` first, because a type error is the cheapest failure to report and the fastest to get. `build` next, because
|
|
11093
|
+
`check` first, because a type error is the cheapest failure to report and the fastest to get. `build` next, because a bundle that will not build is the next cheapest, and the tests last, since they are the slowest to tell you anything.
|
|
10970
11094
|
|
|
10971
11095
|
Read on: [Build & start](/ship/build-and-serve-the-app.md)
|
|
10972
11096
|
|
|
@@ -11074,21 +11198,21 @@ The launch remembers the choice in `APP_DATA_DIR`. If you started the app with t
|
|
|
11074
11198
|
|
|
11075
11199
|
**Disconnect** in the File menu stops the app served here and goes back to the forms, and the next launch opens there too. **Connect…** opens the forms without stopping anything until you choose.
|
|
11076
11200
|
|
|
11077
|
-
## `
|
|
11201
|
+
## `onLauncher` sets the window and the forms
|
|
11078
11202
|
|
|
11079
11203
|
```abide abide
|
|
11080
|
-
<script
|
|
11081
|
-
import
|
|
11204
|
+
<script server>
|
|
11205
|
+
import { onLauncher } from '@abide/abide/server'
|
|
11082
11206
|
|
|
11083
|
-
|
|
11207
|
+
onLauncher(() => ({
|
|
11084
11208
|
width: 960,
|
|
11085
11209
|
height: 640,
|
|
11086
11210
|
connect: false,
|
|
11087
|
-
}
|
|
11211
|
+
}))
|
|
11088
11212
|
</script>
|
|
11089
11213
|
```
|
|
11090
11214
|
|
|
11091
|
-
`src/launcher.abide` sits beside your root layout, and the `
|
|
11215
|
+
`src/launcher.abide` sits beside your root layout, and what the `onLauncher` its `<script server>` calls answers is read when the app launches. It is the one file that may call it: an `onLauncher` anywhere else fails the build, naming the file. A file of that block alone keeps the built-in page. `width` and `height` are the first window's size, and `minWidth` and `minHeight` its floor. After the first launch, the window opens where it was last left.
|
|
11092
11216
|
|
|
11093
11217
|
`start` and `connect` turn a button off. An app that only ever talks to its server turns `start` off. An app that only runs locally turns `connect` off, and Connect… leaves the File menu with it. Turning both off is a type error, because the launcher would have nothing to offer, and an app that slips one past the checker refuses to launch.
|
|
11094
11218
|
|
|
@@ -11117,42 +11241,48 @@ const { name, start, fields, failure } = props<Launch>()
|
|
|
11117
11241
|
{/if}
|
|
11118
11242
|
```
|
|
11119
11243
|
|
|
11120
|
-
Markup in `src/launcher.abide` replaces the built-in page, and receives the same `Launch` the built-in page is drawn from. `start` and `connect` are the addresses the two forms post to, and each is `undefined` where `
|
|
11244
|
+
Markup in `src/launcher.abide` replaces the built-in page, and receives the same `Launch` the built-in page is drawn from. `start` and `connect` are the addresses the two forms post to, and each is `undefined` where `onLauncher` turned that form off. `fields` is one `ConfigField` per input the start form asks for, prefilled. `failure` is what the last attempt was refused with, and `icon` and `accent` are the icon and the color the built-in page uses.
|
|
11121
11245
|
|
|
11122
11246
|
It is rendered by `render` before the app it belongs to is running, so it is a document rather than a live page: its forms post and its `<style>` applies, but an `onclick` does nothing. Written inside `<html>`, it writes its own document. A launch page that fails to render reaches `onError`, and the built-in page shows in its place.
|
|
11123
11247
|
|
|
11124
11248
|
Read on: [Manual rendering](/pages/render-a-document-yourself.md)
|
|
11125
11249
|
|
|
11126
|
-
## A page adds to the menu bar
|
|
11250
|
+
## A page adds to the menu bar with `onMenu`
|
|
11127
11251
|
|
|
11128
11252
|
```abide abide
|
|
11129
|
-
<script
|
|
11130
|
-
import { menuItem } from '@abide/abide'
|
|
11253
|
+
<script>
|
|
11254
|
+
import { menuItem, onMenu, reactive, watch } from '@abide/abide'
|
|
11255
|
+
|
|
11256
|
+
let selected = reactive<string[]>([])
|
|
11257
|
+
|
|
11258
|
+
const exportInvoices = menuItem({ label: 'Export…', key: 'cmd+e' })
|
|
11259
|
+
const markPaid = menuItem({ label: 'Mark Paid', key: 'cmd+p' })
|
|
11131
11260
|
|
|
11132
|
-
|
|
11133
|
-
export const menu = [
|
|
11261
|
+
onMenu(() => [
|
|
11134
11262
|
{ label: 'File', items: ['separator', exportInvoices] },
|
|
11135
11263
|
{
|
|
11136
11264
|
label: 'Invoices',
|
|
11137
|
-
items: [
|
|
11265
|
+
items: [
|
|
11266
|
+
{ label: 'Overdue', href: '/invoices/overdue' },
|
|
11267
|
+
...(selected.length > 0 ? [markPaid] : []),
|
|
11268
|
+
],
|
|
11138
11269
|
},
|
|
11139
|
-
]
|
|
11140
|
-
</script>
|
|
11141
|
-
|
|
11142
|
-
<script>
|
|
11143
|
-
import { watch } from '@abide/abide'
|
|
11270
|
+
])
|
|
11144
11271
|
|
|
11145
11272
|
watch(() => exportInvoices.on(() => download()))
|
|
11273
|
+
watch(() => markPaid.on(() => payAll(selected)))
|
|
11146
11274
|
</script>
|
|
11147
11275
|
```
|
|
11148
11276
|
|
|
11149
|
-
The menu bar is the route's. Every desktop app has the app's own menu, File, Edit, View and Window, and each layout around the page and then the page itself adds what its
|
|
11277
|
+
The menu bar is the route's. Every desktop app has the app's own menu, File, Edit, View and Window, and each layout around the page and then the page itself adds what its `onMenu` answers. A layout's menus are there on every page under it, and a page's are there only while it is showing. Navigate away and they go.
|
|
11278
|
+
|
|
11279
|
+
The function is read the way a template is, so the menus follow the page's state. Mark Paid is in the Invoices menu while something is selected, and leaves it when the selection empties — the menu bar is told again each time a value the function read moves.
|
|
11150
11280
|
|
|
11151
11281
|
A menu with the same `label` as one already there adds its items to the end of it, so Export… lands in File. Any other menu stands before Window, where macOS puts an app's own.
|
|
11152
11282
|
|
|
11153
11283
|
| An item | When picked |
|
|
11154
11284
|
| --- | --- |
|
|
11155
|
-
| made by `menuItem` | runs every function its `on` filed, in the page that
|
|
11285
|
+
| made by `menuItem` | runs every function its `on` filed, in the page that filed it |
|
|
11156
11286
|
| with an `href` | navigates there, as a link in the page would |
|
|
11157
11287
|
| with a `role` | does what the platform does: `copy` copies, `quit` stops the app and quits |
|
|
11158
11288
|
| with `items` | is a menu of its own |
|
|
@@ -11244,7 +11374,7 @@ abide
|
|
|
11244
11374
|
> watch chat/room id=onboarding
|
|
11245
11375
|
```
|
|
11246
11376
|
|
|
11247
|
-
The address is the handler's route without its leading `/` —
|
|
11377
|
+
The address is the handler's route without its leading `/` — its file's path under `src` — and the args are `name=value` words or one JSON object, as `call` takes them. Against a deployed app it is a command of its own: `abide watch chat/room id=onboarding --url https://app.example`.
|
|
11248
11378
|
|
|
11249
11379
|
## The channel is the same channel
|
|
11250
11380
|
|
|
@@ -11313,9 +11443,9 @@ Read on: [Build & start](/ship/build-and-serve-the-app.md) · [Lifecycle](/app/r
|
|
|
11313
11443
|
|
|
11314
11444
|
**`createApp()` — the app a test holds, with no port** — The test beside the card asks for the pool through app.fetch, then stops the app. Fetch the same handler over the wire and read the same answer.
|
|
11315
11445
|
|
|
11316
|
-
```abide Answer.abide
|
|
11446
|
+
```abide lib/Answer.abide
|
|
11317
11447
|
<script server>
|
|
11318
|
-
import {
|
|
11448
|
+
import { onStart, onStop } from '@abide/abide/server'
|
|
11319
11449
|
import { pool } from './pool'
|
|
11320
11450
|
|
|
11321
11451
|
onStart(async (start) => {
|
|
@@ -11327,14 +11457,11 @@ onStop(async (stop) => {
|
|
|
11327
11457
|
await stop()
|
|
11328
11458
|
await pool.close()
|
|
11329
11459
|
})
|
|
11330
|
-
|
|
11331
|
-
export const poolState = GET(() => ({ connected: pool.connected }), {
|
|
11332
|
-
description: 'Whether the pool is connected.',
|
|
11333
|
-
})
|
|
11334
11460
|
</script>
|
|
11335
11461
|
|
|
11336
11462
|
<script>
|
|
11337
11463
|
import { reactive } from '@abide/abide'
|
|
11464
|
+
import poolState from '../poolState.ts'
|
|
11338
11465
|
|
|
11339
11466
|
let status = reactive('—')
|
|
11340
11467
|
let traced = reactive('—')
|
|
@@ -11354,7 +11481,16 @@ let traced = reactive('—')
|
|
|
11354
11481
|
</ul>
|
|
11355
11482
|
```
|
|
11356
11483
|
|
|
11357
|
-
```ts
|
|
11484
|
+
```ts poolState.ts
|
|
11485
|
+
import { GET } from '@abide/abide/server'
|
|
11486
|
+
import { pool } from './lib/pool.ts'
|
|
11487
|
+
|
|
11488
|
+
export default GET(() => ({ connected: pool.connected }), {
|
|
11489
|
+
description: 'Whether the pool is connected.',
|
|
11490
|
+
})
|
|
11491
|
+
```
|
|
11492
|
+
|
|
11493
|
+
```ts lib/pool.ts
|
|
11358
11494
|
/** The pool `onStart` opens and `onStop` closes. */
|
|
11359
11495
|
export const pool = {
|
|
11360
11496
|
connected: false,
|
|
@@ -11367,10 +11503,10 @@ export const pool = {
|
|
|
11367
11503
|
}
|
|
11368
11504
|
```
|
|
11369
11505
|
|
|
11370
|
-
```ts app.test.ts
|
|
11506
|
+
```ts tests/app.test.ts
|
|
11371
11507
|
import { expect, test } from 'bun:test'
|
|
11372
11508
|
import { createApp } from '@abide/abide/server'
|
|
11373
|
-
import { pool } from '
|
|
11509
|
+
import { pool } from '../lib/pool'
|
|
11374
11510
|
|
|
11375
11511
|
// Run from the app's own directory: `createApp` finds the app there (38.25).
|
|
11376
11512
|
test('the pool handler answers in process as it does on the wire', async () => {
|
|
@@ -11396,8 +11532,72 @@ A `Request` in and a `Response` out, through everything a served request goes th
|
|
|
11396
11532
|
|
|
11397
11533
|
Those are the parts a handler called directly does not have, which is the reason to reach for a request at all. Assert on the response and you are asserting on what a browser would have been sent.
|
|
11398
11534
|
|
|
11535
|
+
`poolState.ts` is a handler file, so the address the test asks for is its path. The test is `tests/app.test.ts`, beside the page it tests: nothing under a `tests` directory has an address, so a test sits next to the page or the handler it reaches and is never served.
|
|
11536
|
+
|
|
11399
11537
|
Read on: [Call a handler](/ship/call-a-handler-from-the-terminal.md)
|
|
11400
11538
|
|
|
11539
|
+
## A test renders a `.abide` it imports
|
|
11540
|
+
|
|
11541
|
+
**`render(component)` — the `.abide` a test imports** — The pipeline sums the deals still open. Its test renders the same component and reads the total off the markup.
|
|
11542
|
+
|
|
11543
|
+
```abide lib/Pipeline.abide
|
|
11544
|
+
<script>
|
|
11545
|
+
import { reactive } from '@abide/abide'
|
|
11546
|
+
|
|
11547
|
+
let deals = reactive([
|
|
11548
|
+
{ name: 'Analytical Engines', amount: 8000, won: false },
|
|
11549
|
+
{ name: 'Hopper & Co', amount: 4400, won: false },
|
|
11550
|
+
])
|
|
11551
|
+
const open = reactive(() => deals.filter((deal) => !deal.won))
|
|
11552
|
+
const total = reactive(() => open.reduce((sum, deal) => sum + deal.amount, 0))
|
|
11553
|
+
|
|
11554
|
+
const usd = (amount: number) => `$${amount.toLocaleString('en-US')}`
|
|
11555
|
+
|
|
11556
|
+
function win(name: string) {
|
|
11557
|
+
deals = deals.map((deal) =>
|
|
11558
|
+
deal.name === name ? { ...deal, won: true } : deal,
|
|
11559
|
+
)
|
|
11560
|
+
}
|
|
11561
|
+
</script>
|
|
11562
|
+
|
|
11563
|
+
<ul class="fields">
|
|
11564
|
+
{#for deal of open by deal.name}
|
|
11565
|
+
<li><span>{deal.name}</span><strong>{usd(deal.amount)}</strong>
|
|
11566
|
+
<button onclick={() => win(deal.name)}>Won</button></li>
|
|
11567
|
+
{/for}
|
|
11568
|
+
</ul>
|
|
11569
|
+
|
|
11570
|
+
<ul class="tiles">
|
|
11571
|
+
<li><span>open pipeline</span><strong>{usd(total)}</strong></li>
|
|
11572
|
+
</ul>
|
|
11573
|
+
```
|
|
11574
|
+
|
|
11575
|
+
```ts tests/Pipeline.test.ts
|
|
11576
|
+
import { expect, test } from 'bun:test'
|
|
11577
|
+
import { render } from '@abide/abide/server'
|
|
11578
|
+
import Pipeline from '../lib/Pipeline.abide'
|
|
11579
|
+
|
|
11580
|
+
// bunfig.toml preloads `@abide/abide/test`, which makes the import a component.
|
|
11581
|
+
test('the pipeline totals the deals still open', async () => {
|
|
11582
|
+
const html = new TextDecoder().decode(await render(Pipeline({})))
|
|
11583
|
+
expect(html).toContain('Hopper & Co')
|
|
11584
|
+
expect(html).toContain('$12,400')
|
|
11585
|
+
})
|
|
11586
|
+
```
|
|
11587
|
+
|
|
11588
|
+
The import is the component the build compiles, with no `createApp` run first, and `render` turns it into a document's bytes as a page's render does. The test asserts on markup, so what it checks is what a reader would see.
|
|
11589
|
+
|
|
11590
|
+
One line in the app's `bunfig.toml` makes that so, and the scaffold writes it:
|
|
11591
|
+
|
|
11592
|
+
```toml bunfig.toml
|
|
11593
|
+
[test]
|
|
11594
|
+
preload = ["@abide/abide/test"]
|
|
11595
|
+
```
|
|
11596
|
+
|
|
11597
|
+
Without it, `bun test` resolves a `.abide` import to the file's path, and the test fails on `Pipeline is not a function`.
|
|
11598
|
+
|
|
11599
|
+
Read on: [Manual rendering](/pages/render-a-document-yourself.md)
|
|
11600
|
+
|
|
11401
11601
|
## `app.run` is a scope and nothing else
|
|
11402
11602
|
|
|
11403
11603
|
Calling a handler by name already runs its body, its rungs and its schema. What it needs from you is a scope — the thing a cache entry is filed under, and the lifetime a request-local value belongs to.
|
|
@@ -11437,7 +11637,7 @@ Read on: [Request](/app/read-the-incoming-request.md)
|
|
|
11437
11637
|
|
|
11438
11638
|
Hold it with `await using` and it stops where the block ends, a failed assertion included. The test's second case stops the app by hand and finds the pool `onStart` opened closed again:
|
|
11439
11639
|
|
|
11440
|
-
```ts app.test.ts — excerpt
|
|
11640
|
+
```ts tests/app.test.ts — excerpt
|
|
11441
11641
|
test('stopping the app closes the pool onStart opened', async () => {
|
|
11442
11642
|
const app = await createApp()
|
|
11443
11643
|
expect(pool.connected).toBe(true)
|
|
@@ -11595,8 +11795,8 @@ Every `Reactive` takes these, whichever overload made it.
|
|
|
11595
11795
|
| `s.then` | `<Result>(onSettled?: (value: Stored) => Result \| PromiseLike<Result>, onFailed?: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Result>` | The settling, as a promise. Resolves when `busy()` goes false, and rejects with what a read would throw. |
|
|
11596
11796
|
| `s.catch` | `<Result>(onFailed: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Stored \| Result>` | The settling's refusal path, derived from `then`. Fires for a producer that failed with nothing landed, and for nothing else. |
|
|
11597
11797
|
| `s.finally` | `(onSettled: () => void) => Promise<Stored>` | The settling's end, whichever way it went. What ends is the settling, not the value — a subscriber's copy of a channel settles at its first production and goes on. |
|
|
11598
|
-
| `for await (… of s)` | `AsyncIterable<Produced>` |
|
|
11599
|
-
| `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The
|
|
11798
|
+
| `for await (… of s)` | `AsyncIterable<Produced>` | What one run produced. On a stream, one turn per chunk, ending at the close and throwing what failed the run. On any other value, the settled value once, as `await` gives it. To follow changes, `watch`. |
|
|
11799
|
+
| `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The run being served, from its start to its end. A chunk is a piece of one value, so a stream's cursor sees every chunk; any other value's cursor sees the value it settles on. |
|
|
11600
11800
|
|
|
11601
11801
|
## Writes
|
|
11602
11802
|
|
|
@@ -11850,7 +12050,7 @@ export const notMember = refuse.named('NotMember', 403, undefined, {
|
|
|
11850
12050
|
### Returning a refusal
|
|
11851
12051
|
|
|
11852
12052
|
```ts server
|
|
11853
|
-
export
|
|
12053
|
+
export default POST(async ({ group }: { group: string }) => {
|
|
11854
12054
|
if (!(await isMember(group))) return notMember({ group })
|
|
11855
12055
|
return send(group)
|
|
11856
12056
|
})
|
|
@@ -12015,13 +12215,11 @@ There is no `schemas` option on `GET` or `POST`. A shape is a fact about the val
|
|
|
12015
12215
|
|
|
12016
12216
|
### A read any surface may call
|
|
12017
12217
|
|
|
12018
|
-
```
|
|
12019
|
-
|
|
12020
|
-
export const getInvoice = GET(
|
|
12218
|
+
```ts src/invoices/getInvoice.ts — excerpt
|
|
12219
|
+
export default GET(
|
|
12021
12220
|
({ id }: { id: string }) => database.invoice.find(id),
|
|
12022
12221
|
{ description: 'One invoice, by id.' },
|
|
12023
12222
|
)
|
|
12024
|
-
</script>
|
|
12025
12223
|
```
|
|
12026
12224
|
|
|
12027
12225
|
### A value every caller follows
|
|
@@ -12032,9 +12230,9 @@ const rooms = reactive((_room: { id: string }): Turn[] => [], {
|
|
|
12032
12230
|
by: 'id',
|
|
12033
12231
|
})
|
|
12034
12232
|
|
|
12035
|
-
|
|
12233
|
+
const room = GET(rooms)
|
|
12036
12234
|
|
|
12037
|
-
|
|
12235
|
+
const say = POST(({ id, text }: { id: string; text: string }) => {
|
|
12038
12236
|
rooms({ id }).patch((turns) => {
|
|
12039
12237
|
turns.push({ id: crypto.randomUUID(), text })
|
|
12040
12238
|
})
|
|
@@ -12169,9 +12367,9 @@ An ambient that is a `Reactive` is read the way any value is read, so reading it
|
|
|
12169
12367
|
|
|
12170
12368
|
### The caller, in a rung
|
|
12171
12369
|
|
|
12172
|
-
```abide src/invoices/
|
|
12370
|
+
```abide src/invoices/index.abide — excerpt
|
|
12173
12371
|
<script server>
|
|
12174
|
-
|
|
12372
|
+
const voidInvoice = POST(
|
|
12175
12373
|
({ id }: { id: string }) => database.invoice.void(id),
|
|
12176
12374
|
{
|
|
12177
12375
|
middleware: [
|
|
@@ -12361,7 +12559,7 @@ A **`.abide`** file is a component: a template, with `<script>` and `<style>` bl
|
|
|
12361
12559
|
</script>
|
|
12362
12560
|
|
|
12363
12561
|
<script server>
|
|
12364
|
-
// module scope, on the server alone;
|
|
12562
|
+
// module scope, on the server alone; private handlers, hooks
|
|
12365
12563
|
</script>
|
|
12366
12564
|
|
|
12367
12565
|
<script>
|
|
@@ -12376,7 +12574,7 @@ A **`.abide`** file is a component: a template, with `<script>` and `<style>` bl
|
|
|
12376
12574
|
```
|
|
12377
12575
|
|
|
12378
12576
|
Every block is optional and order is free. A file with no `<script>` is markup, which is what a presentational component usually is. A `<script
|
|
12379
|
-
server>` is written outside a `server` or `public` directory, and the browser's copy of the file carries a client per handler it
|
|
12577
|
+
server>` is written outside a `server` or `public` directory, and the browser's copy of the file carries a client per handler it declares in its place. No block exports: the component is the file's one export (34.17).
|
|
12380
12578
|
|
|
12381
12579
|
## Reading and writing by name
|
|
12382
12580
|
|
|
@@ -12403,25 +12601,22 @@ The rule is **position**. An operand, a text hole, an attribute and a value-type
|
|
|
12403
12601
|
| Spelling | Meaning |
|
|
12404
12602
|
| --- | --- |
|
|
12405
12603
|
| `{expr}` | Reactive text, escaped. |
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
|
12410
|
-
|
|
|
12411
|
-
| `
|
|
12412
|
-
| `
|
|
12413
|
-
| `
|
|
12414
|
-
| `
|
|
12415
|
-
| `bind:
|
|
12416
|
-
| `bind:
|
|
12417
|
-
| `bind:
|
|
12418
|
-
| `
|
|
12419
|
-
| `
|
|
12420
|
-
| `
|
|
12421
|
-
|
|
|
12422
|
-
| `class:name={cond}` | Toggles a class. | 32.10 |
|
|
12423
|
-
| `style:prop={value}` | Sets one style property. | 32.10, 32.19 |
|
|
12424
|
-
| `{...expr}` | Spreads props or attributes. | 32.11, 33.12, 33.13 |
|
|
12604
|
+
| formatting whitespace | What a run of whitespace renders as. |
|
|
12605
|
+
| `{raw(...)}` | Raw HTML. |
|
|
12606
|
+
| `name={expr}` | A reactive attribute or property. |
|
|
12607
|
+
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. |
|
|
12608
|
+
| `name="…{expr}…"` | An interpolated quoted value. |
|
|
12609
|
+
| `bind:value` | Two-way bind on an element. |
|
|
12610
|
+
| `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. |
|
|
12611
|
+
| `bind:checked` | A boolean bind on an input. |
|
|
12612
|
+
| `bind:open` | The same, on a `<details>`. |
|
|
12613
|
+
| `bind:group` | Radio or checkbox membership. |
|
|
12614
|
+
| `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. |
|
|
12615
|
+
| `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. |
|
|
12616
|
+
| `class:name={cond}` | Toggles a class. |
|
|
12617
|
+
| `style:prop={value}` | Sets one style property. |
|
|
12618
|
+
| `{...expr}` | Spreads props or attributes. |
|
|
12619
|
+
| static template text | What an author writes outside a slot, in body text and in an attribute value. |
|
|
12425
12620
|
|
|
12426
12621
|
Static text is markup. An author-written `&` or `©` renders the same in body text and in an attribute value, and only an interpolated value is escaped — the one exception being a URL-typed attribute, where the encoding is what stops an entity forming a scheme.
|
|
12427
12622
|
|
|
@@ -12466,28 +12661,27 @@ Static text is markup. An author-written `&` or `©` renders the same in
|
|
|
12466
12661
|
| --- | --- |
|
|
12467
12662
|
| `<script>` | Per-instance component setup. |
|
|
12468
12663
|
| `<script module>` | Module scope. |
|
|
12469
|
-
| `<script server>` | Module scope on a server alone,
|
|
12664
|
+
| `<script server>` | Module scope on a server alone, declaring its file's private handlers and filing its hooks. |
|
|
12470
12665
|
| `<style>` | Component-scoped styles. |
|
|
12471
12666
|
| `import './app.css'` | A stylesheet the component depends on. |
|
|
12472
12667
|
| `:global(…)` | The per-selector escape from scoping. |
|
|
12473
12668
|
|
|
12474
|
-
|
|
12669
|
+
An `export` in any block is a compile error, so what two files share is a module under a `lib`, and a handler two files call is a handler file of its own — a `.ts` that TypeScript reads as it reads any other.
|
|
12475
12670
|
|
|
12476
12671
|
## Tracking
|
|
12477
12672
|
|
|
12478
|
-
|
|
|
12479
|
-
| --- | --- | --- |
|
|
12480
|
-
|
|
|
12481
|
-
|
|
|
12482
|
-
|
|
12483
|
-
|
|
|
12484
|
-
|
|
|
12485
|
-
| `watch` effect, bare | **yes** | Pushes its own subscriber. |
|
|
12486
|
-
| `watch` effect, over sources | **no** | The sources decide the reruns. |
|
|
12487
|
-
|
|
12488
|
-
| event handler | no | Not the flow. |
|
|
12489
|
-
|
|
|
12490
|
-
| `Transformer`, `Disposer`, middleware, lifecycle hooks | **no** | Not the flow. | 14.10 |
|
|
12673
|
+
| Context | Tracks | Meaning |
|
|
12674
|
+
| --- | --- | --- |
|
|
12675
|
+
| template expression | **yes** | The flow itself. |
|
|
12676
|
+
| branch-local `<script>` | **no** | Setup, once per item. |
|
|
12677
|
+
| `reactive` body, unkeyed | **yes** | Pushes its own subscriber. |
|
|
12678
|
+
| `reactive` body, keyed | **no** | Untracked by handler. |
|
|
12679
|
+
| block body binding | **yes** | A live read, like a prop. |
|
|
12680
|
+
| `watch` effect, bare | **yes** | Pushes its own subscriber. |
|
|
12681
|
+
| `watch` effect, over sources | **no** | The sources decide the reruns. |
|
|
12682
|
+
| component `<script>` setup | **no** | Setup, once per instance. |
|
|
12683
|
+
| event handler | **no** | Not the flow. |
|
|
12684
|
+
| `Transformer`, `Disposer`, middleware, lifecycle hooks | **no** | Not the flow. |
|
|
12491
12685
|
|
|
12492
12686
|
A context that tracks reruns when what it read changes. A context that does not is one where a read is a read — an event handler runs because something happened, and a setup body runs once — so nothing is arranged by accident and the arrangement you want is `reactive` or `watch`.
|
|
12493
12687
|
|
|
@@ -12497,8 +12691,14 @@ A context that tracks reruns when what it read changes. A context that does not
|
|
|
12497
12691
|
|
|
12498
12692
|
A run of whitespace carrying newlines but no space or tab is **dropped wherever it stands**, and a run that renders nothing but carries a newline and indentation is dropped at both ends of every block body and of the top-level template. So indentation in a template is layout for the reader rather than text in the document.
|
|
12499
12693
|
|
|
12694
|
+
### Where a `.abide` lives
|
|
12695
|
+
|
|
12696
|
+
A `.abide` directly in a directory under no fence is a page at its path, save a `layout.abide`, an `error.abide` and the root's `launcher.abide` (23.21). A component is a `.abide` under a `lib`, imported by path, and a test under a `tests` directory imports one the same way once `@abide/abide/test` is preloaded (38.109). The [Routing](/reference/routing.md) reference has the whole map.
|
|
12697
|
+
|
|
12500
12698
|
### What the compiler refuses
|
|
12501
12699
|
|
|
12700
|
+
- An `export` in any block, however the language spells it.
|
|
12701
|
+
- A `<script server>` in a file under a `server` or a `public` directory.
|
|
12502
12702
|
- A read spelled `foo()` with an argument.
|
|
12503
12703
|
- `props()` from a `<script module>` block.
|
|
12504
12704
|
- A prop passed to a component that never calls `props()`.
|
|
@@ -12510,7 +12710,7 @@ Each is caught before the code runs, which is why the ergonomic form does not ha
|
|
|
12510
12710
|
|
|
12511
12711
|
### A component that owns a value
|
|
12512
12712
|
|
|
12513
|
-
```abide src/
|
|
12713
|
+
```abide src/lib/Handle.abide
|
|
12514
12714
|
<script>
|
|
12515
12715
|
import { reactive } from '@abide/abide'
|
|
12516
12716
|
|
|
@@ -12523,7 +12723,7 @@ let handle = reactive('ada')
|
|
|
12523
12723
|
|
|
12524
12724
|
### A component with props and children
|
|
12525
12725
|
|
|
12526
|
-
```abide src/
|
|
12726
|
+
```abide src/lib/Panel.abide
|
|
12527
12727
|
<script>
|
|
12528
12728
|
import { props } from '@abide/abide'
|
|
12529
12729
|
|
|
@@ -12547,7 +12747,7 @@ const { title } = props<{ title: string }>()
|
|
|
12547
12747
|
|
|
12548
12748
|
*File patterns, precedence, head merging and render.*
|
|
12549
12749
|
|
|
12550
|
-
Every address an abide app answers on comes from a **file path**. A page is a `
|
|
12750
|
+
Every address an abide app answers on comes from a **file path**. A page is a `.abide` and a handler is a `.ts`, each directly in a directory under `src` and answering at its own path. So there is no route table, and nothing to keep in step with the filesystem.
|
|
12551
12751
|
|
|
12552
12752
|
## Page patterns
|
|
12553
12753
|
|
|
@@ -12564,19 +12764,18 @@ A file or directory named `[id]` matches exactly one segment and is a required k
|
|
|
12564
12764
|
|
|
12565
12765
|
## Mount paths
|
|
12566
12766
|
|
|
12567
|
-
| File |
|
|
12767
|
+
| File | Declares | Served at |
|
|
12568
12768
|
| --- | --- | --- |
|
|
12569
|
-
| `users/
|
|
12570
|
-
| `
|
|
12571
|
-
| `
|
|
12572
|
-
| `media/[id]/
|
|
12573
|
-
| `media/[id]/comments.abide` | `addComment` | `/media/[id]/addComment` |
|
|
12769
|
+
| `users/getUser.ts` | `export default GET(…)` | `/users/getUser` |
|
|
12770
|
+
| `health.ts` | `export default GET(…)` | `/health` |
|
|
12771
|
+
| `media/[id]/save.ts` | `export default POST(…)` | `/media/[id]/save`, `id` reaching its `Args` |
|
|
12772
|
+
| `media/[id]/lib/Comments.abide` | `const addComment = POST(…)` in its `<script server>` | `/__abide/handlers/media/id/lib/Comments/addComment` |
|
|
12574
12773
|
|
|
12575
|
-
A handler answers at its
|
|
12774
|
+
A handler file answers at its PATH, and declares one handler as its default export, exporting nothing else. A handler a `<script server>` declares is its file's alone: it answers below `/__abide/handlers`, takes nothing from the path, is listed on no other surface, and runs under the rungs filed where its file's hooks are.
|
|
12576
12775
|
|
|
12577
12776
|
A parameter in the route reaches the handler's args from the path, whatever the method, so the handler's `Args` has to declare it — one it does not is a build error naming the parameter. A request that names it in the query or the body as well is refused `400`.
|
|
12578
12777
|
|
|
12579
|
-
A route answers each method **once**. A page answers `GET` at its own route, so a `GET` handler
|
|
12778
|
+
A route answers each method **once**. A page answers `GET` at its own route, so a `GET` handler file at that route is a build error naming both files, and a `POST` there is the form the page posts to. Two routes that differ only in a parameter's name are one route.
|
|
12580
12779
|
|
|
12581
12780
|
## `render`
|
|
12582
12781
|
|
|
@@ -12608,12 +12807,34 @@ The batching unit of a fill is the **flush**, not the production: a value that p
|
|
|
12608
12807
|
|
|
12609
12808
|
## Description
|
|
12610
12809
|
|
|
12810
|
+
### From file to route
|
|
12811
|
+
|
|
12812
|
+
```
|
|
12813
|
+
src/
|
|
12814
|
+
index.abide GET /
|
|
12815
|
+
layout.abide around every page below /
|
|
12816
|
+
crm/
|
|
12817
|
+
index.abide GET /crm
|
|
12818
|
+
[id].abide GET /crm/[id]
|
|
12819
|
+
getInvoice.ts GET /crm/getInvoice
|
|
12820
|
+
layout.abide around every page and handler below /crm
|
|
12821
|
+
lib/ no route: components and modules
|
|
12822
|
+
Notes.abide its handlers below /__abide/handlers/crm/lib/Notes
|
|
12823
|
+
tests/ no route: tests
|
|
12824
|
+
server/ no route: modules the browser never loads
|
|
12825
|
+
public/
|
|
12826
|
+
robots.txt the file, at /robots.txt
|
|
12827
|
+
```
|
|
12828
|
+
|
|
12829
|
+
A route is its file's path without the extension, and an `index.abide`, a `layout.abide` and an `error.abide` take their directory's (23.14). Every other `.abide` directly in a directory is a page (23.21), so a component lives under a `lib`. A `.ts` there is a handler file.
|
|
12830
|
+
|
|
12831
|
+
`lib`, `tests`, `server` and `public` are the fences, and nothing under one has a route (23.15). One inside another is a build error, save a `server` inside a `lib` or a `tests`, which keeps code only the server loads beside the components that call it — and so is an `index.abide`, a `layout.abide` or an `error.abide` under one (23.16, 23.18). A directory's top level holds pages, layouts, error pages, handler files, directories and fences alone — any other file there is a build error naming the `lib` to move it into (23.22). So listing a directory lists its routes, and `abide routes` lists the app's.
|
|
12832
|
+
|
|
12611
12833
|
### Resolution order
|
|
12612
12834
|
|
|
12613
12835
|
A request goes to the most specific route **answering its method**, a literal segment beating a dynamic one at the same depth — so `POST
|
|
12614
|
-
/media/new` reaches the `new
|
|
12615
|
-
|
|
12616
|
-
/media/new` falls through to the page at `media/[id]/page.abide`. `layout.abide` and `error.abide` are both resolved **nearest-ancestor by route**, which is what lets a section answer for itself and the app answer for the rest.
|
|
12836
|
+
/media/new` reaches the handler file `media/new.ts`, while `GET
|
|
12837
|
+
/media/new` falls through to the page at `media/[id]/index.abide`. `layout.abide` and `error.abide` are both resolved **nearest-ancestor by route**, which is what lets a section answer for itself and the app answer for the rest.
|
|
12617
12838
|
|
|
12618
12839
|
### The head
|
|
12619
12840
|
|
|
@@ -12623,7 +12844,7 @@ A request goes to the most specific route **answering its method**, a literal se
|
|
|
12623
12844
|
|
|
12624
12845
|
### A route with a required and an optional segment
|
|
12625
12846
|
|
|
12626
|
-
```abide src/crm/invoices/[id]/[[tab]]/
|
|
12847
|
+
```abide src/crm/invoices/[id]/[[tab]]/index.abide — excerpt
|
|
12627
12848
|
<script>
|
|
12628
12849
|
import { route } from '@abide/abide'
|
|
12629
12850
|
|
|
@@ -12635,8 +12856,8 @@ const { id, tab = 'summary' } = route.params
|
|
|
12635
12856
|
|
|
12636
12857
|
### A document rendered outside the page pipeline
|
|
12637
12858
|
|
|
12638
|
-
```abide src/pages/render-a-document-yourself/InvoicePreview.abide — excerpt
|
|
12639
|
-
|
|
12859
|
+
```abide src/pages/render-a-document-yourself/lib/InvoicePreview.abide — excerpt
|
|
12860
|
+
const previewInvoice = GET(
|
|
12640
12861
|
async ({ id }: { id: string }) => {
|
|
12641
12862
|
const invoice = await findInvoice(id)
|
|
12642
12863
|
return page(await render(Document({ invoice })))
|
|
@@ -12648,6 +12869,7 @@ export const previewInvoice = GET(
|
|
|
12648
12869
|
## See also
|
|
12649
12870
|
|
|
12650
12871
|
- [Routes](/pages/add-a-page.md) — the guide to page files
|
|
12872
|
+
- [Handler URLs](/data/find-the-url-a-handler-answers-on.md) — every address a handler answers on, and `abide routes`
|
|
12651
12873
|
- [Manual rendering](/pages/render-a-document-yourself.md) — `render` in use
|
|
12652
12874
|
- [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) — what a sink is for
|
|
12653
12875
|
|
|
@@ -12659,7 +12881,7 @@ export const previewInvoice = GET(
|
|
|
12659
12881
|
|
|
12660
12882
|
*Environment variables, files and lifecycle hooks.*
|
|
12661
12883
|
|
|
12662
|
-
An abide app is configured by three things: the **environment** it starts in, the **files** it finds under `src/`, and the **hooks** its
|
|
12884
|
+
An abide app is configured by three things: the **environment** it starts in, the **files** it finds under `src/`, and the **hooks** its server blocks and handler files call. There is no config file format, because a typed `config()` read is what an app actually wants and an environment variable is what a deployment actually has.
|
|
12663
12885
|
|
|
12664
12886
|
## Environment variables
|
|
12665
12887
|
|
|
@@ -12694,23 +12916,28 @@ The `ABIDE_`-prefixed ceilings default to no ceiling. That is the honest default
|
|
|
12694
12916
|
| --- | --- |
|
|
12695
12917
|
| `src/**` | The app's source. |
|
|
12696
12918
|
| `**/server/**` | Source the browser never loads. |
|
|
12919
|
+
| `**/lib/**` | The app's code that is no address: components and modules, on both sides. |
|
|
12920
|
+
| `**/tests/**` | The app's tests. |
|
|
12697
12921
|
| `**/public/**` | The files answered as authored, at their path below that directory. |
|
|
12698
12922
|
| `src/public/favicon.ico` | The file answered at `/favicon.ico`. |
|
|
12699
12923
|
| `src/public/robots.txt` | The file answered at `/robots.txt`. |
|
|
12700
12924
|
| `src/public/icon.png` · `src/public/icon.icns` | The files answered at `/icon.png` and `/icon.icns`, and the desktop app's icon. |
|
|
12701
|
-
| `src/**/` |
|
|
12702
|
-
| `**/
|
|
12925
|
+
| `src/**/` | A directory of the app's, a segment of the route of every file directly in it. |
|
|
12926
|
+
| `**/index.abide` | The page at its directory's route. |
|
|
12927
|
+
| `**/<name>.abide` | The page at its directory's route and then `<name>`. |
|
|
12928
|
+
| `**/<name>.ts` | A handler file: the handler its default export declares, at its directory's route and then `<name>`. |
|
|
12703
12929
|
| `**/layout.abide` | A layout, rendering every page below its route through a slot, and the hooks over that route. |
|
|
12704
|
-
| `src/launcher.abide` | The desktop launcher, beside the root layout: the `
|
|
12930
|
+
| `src/launcher.abide` | The desktop launcher, beside the root layout: the `onLauncher` it calls, and its page in place of the launcher's own where it has markup. |
|
|
12705
12931
|
| `src/layout.abide` | The root layout, whose `<script server>` is the app's own, and whose `<html>` is the document its pages are served in. |
|
|
12706
12932
|
| `**/error.abide` | The page a refusal renders in. |
|
|
12707
12933
|
| `**/node_modules/**/*.abide` | A component a dependency ships as source, compiled with the app that imports it. |
|
|
12708
|
-
| `import '…' with { type: 'routes' }` | A package's directory mounted at the importing file's directory, its pages, layouts, error pages and
|
|
12934
|
+
| `import '…' with { type: 'routes' }` | A package's directory mounted at the importing file's directory, its pages, layouts, error pages and handler files answering below it. |
|
|
12709
12935
|
| `bunfig.toml` | Under `[serve.static] plugins`, the bundler plugins the client is built with. |
|
|
12936
|
+
| `preload = ["@abide/abide/test"]` | The `[test]` preload a test imports the app's `.abide` files through. |
|
|
12710
12937
|
|
|
12711
|
-
Every directory under `src` is a segment of an address, so `src/billing/
|
|
12938
|
+
Every directory under `src` is a segment of an address, so `src/billing/index.abide` is `/billing`. A `server`, `public`, `lib` or `tests` directory can sit anywhere under it and is never a segment: nothing under one has an address, an `index.abide`, a `layout.abide` or an `error.abide` there is a build error, and one inside another is a build error too. Any other directory holds pages, layouts, error pages, handler files and directories alone, and a file of any other kind directly in it is a build error naming the `lib` to move it into. A `public` directory's files answer below the path of the directories above it. Everything outside a `server` directory is both sides. `#src/*` is the one import alias, mapping onto `src/`.
|
|
12712
12939
|
|
|
12713
|
-
`
|
|
12940
|
+
A page and a handler file are **addresses rather than names**, and so are `index.abide`, `layout.abide` and `error.abide` — the framework's own conventions. A file under a `lib` is named after what it exports.
|
|
12714
12941
|
|
|
12715
12942
|
## Lifecycle hooks
|
|
12716
12943
|
|
|
@@ -12723,8 +12950,11 @@ Every directory under `src` is a segment of an address, so `src/billing/page.abi
|
|
|
12723
12950
|
| `onConfig` | `(fn: ConfigDefaults \| null, options?: ConfigOptions) => () => void` | The app's config defaults. |
|
|
12724
12951
|
| `onHealth` | `(report: () => unknown \| Promise<unknown>) => () => void` | The app's health reporter. |
|
|
12725
12952
|
| `onPrincipal` | `(resolve: (claims: unknown) => unknown \| Promise<unknown>) => () => void` | Turns claims into the app's half of a principal. |
|
|
12953
|
+
| `onLauncher` | `(options: () => LauncherOptions) => () => void` | The window the desktop app opens and the forms its launcher offers. |
|
|
12726
12954
|
|
|
12727
|
-
Each is a hook you **call**
|
|
12955
|
+
Each is a hook you **call** rather than an export the framework looks for, a `.abide` exporting nothing. It is filed at the route of the `.abide` or the handler file that called it (37.1), and any other module calling one is a build error (37.13) — export the function and call the hook where it serves. A `.abide` under a `lib` or a `tests` directory files its hooks at the route of the directory holding that one. `onLauncher` is called in `src/launcher.abide` alone.
|
|
12956
|
+
|
|
12957
|
+
`middleware` runs over that route and the ones below it — before routing at the root, after it anywhere else — and `onError` hears an error there, nearest route first. The rest are the process's wherever they are written, and every one of a kind composes: `onConfig` and `onPrincipal` merge what each answers, the deeper route's over the root's. `abide routes` lists every call with the route it is filed at.
|
|
12728
12958
|
|
|
12729
12959
|
Every one hands back the way off again, so a module that is torn down can remove its own.
|
|
12730
12960
|
|
|
@@ -12738,6 +12968,10 @@ Every one hands back the way off again, so a module that is torn down can remove
|
|
|
12738
12968
|
|
|
12739
12969
|
`config()` is ambient, and the same name on both sides. On a server it reads the process environment merged over the app's declared defaults; in a browser it reads what the document was served with, which is the subset an app marked as public. A secret is not in that subset, and that is a property of the declaration rather than of a naming convention.
|
|
12740
12970
|
|
|
12971
|
+
### `Config` is typed from the `onConfig` calls
|
|
12972
|
+
|
|
12973
|
+
`abide check` and `abide lsp` type `Config` from the defaults every `onConfig` returns, read where it is called as a statement at the top of a `<script server>` or of a handler file (27.13). A default that returns a union types the keys every member of it has. So a config read is typed without a declaration of its own, wherever in the app the default is written.
|
|
12974
|
+
|
|
12741
12975
|
### `NODE_ENV` is verbatim
|
|
12742
12976
|
|
|
12743
12977
|
It is the environment name as given, not a boolean and not a three-way enum. An app comparing it against `'production'` is comparing against the string it was started with.
|
|
@@ -12746,17 +12980,17 @@ It is the environment name as given, not a boolean and not a three-way enum. An
|
|
|
12746
12980
|
|
|
12747
12981
|
### Declaring the app's own config
|
|
12748
12982
|
|
|
12749
|
-
```abide src/app/configure-the-app/Retries.abide — excerpt
|
|
12983
|
+
```abide src/app/configure-the-app/lib/Retries.abide — excerpt
|
|
12750
12984
|
onConfig(() => ({ RETRIES: 3 }))
|
|
12751
12985
|
…
|
|
12752
|
-
|
|
12986
|
+
const retryPolicy = GET(() => ({ retries: config().RETRIES }), {
|
|
12753
12987
|
description: 'How many times a call is retried.',
|
|
12754
12988
|
})
|
|
12755
12989
|
```
|
|
12756
12990
|
|
|
12757
12991
|
### Running work around the boot
|
|
12758
12992
|
|
|
12759
|
-
```abide src/app/run-code-at-start-and-stop/Pool.abide — excerpt
|
|
12993
|
+
```abide src/app/run-code-at-start-and-stop/lib/Pool.abide — excerpt
|
|
12760
12994
|
onStart(async (start) => {
|
|
12761
12995
|
await pool.connect()
|
|
12762
12996
|
await start()
|
|
@@ -12816,7 +13050,7 @@ abide --help
|
|
|
12816
13050
|
| `abide scaffold <name>` | Writes a starter project. |
|
|
12817
13051
|
| `abide run <file> [args…]` | Runs a script under the abide runtime. |
|
|
12818
13052
|
| `abide check [dir…]` | Type-checks `.abide`, on the `.abide` line. |
|
|
12819
|
-
| `abide
|
|
13053
|
+
| `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. |
|
|
12820
13054
|
| `abide dev [--port <n>]` | Watches the project and keeps the app up. |
|
|
12821
13055
|
| `abide build` | Builds the client into content-hashed chunks and a manifest. |
|
|
12822
13056
|
| `abide start [--port <n>]` | Boots the built app. |
|