@abide/abide 0.103.0 → 0.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +27 -27
  4. package/docs/RULEBOOK.md +68 -24
  5. package/docs/abide.md +31 -46
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +46 -89
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -25
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +12 -10
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +183 -478
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/projectShadow.ts +1 -1
  25. package/src/compiler/readSources.ts +27 -42
  26. package/src/compiler/readsIn.ts +5 -16
  27. package/src/compiler/resolverIn.ts +77 -0
  28. package/src/server/abideModules.ts +152 -146
  29. package/src/server/answerRequest.ts +18 -4
  30. package/src/server/answerSocket.ts +136 -76
  31. package/src/server/answerValue.ts +28 -15
  32. package/src/server/appTree.ts +59 -73
  33. package/src/server/claudeApi.ts +2 -5
  34. package/src/server/clientBundle.ts +198 -0
  35. package/src/server/coerceArgs.ts +1 -1
  36. package/src/server/commands.ts +93 -57
  37. package/src/server/createApp.ts +90 -16
  38. package/src/server/deferredBuilds.ts +175 -0
  39. package/src/server/devWorker.ts +107 -319
  40. package/src/server/faceOf.ts +120 -0
  41. package/src/server/fenceServer.ts +10 -18
  42. package/src/server/generatedSurfaces.ts +12 -53
  43. package/src/server/index.ts +3 -0
  44. package/src/server/placement.ts +167 -0
  45. package/src/server/remote.ts +14 -42
  46. package/src/server/render.ts +25 -17
  47. package/src/server/renderComponent.ts +9 -5
  48. package/src/server/requestScope.ts +10 -4
  49. package/src/server/responseRelay.ts +389 -0
  50. package/src/server/runCompiled.ts +9 -12
  51. package/src/server/runDesktop.ts +6 -9
  52. package/src/server/serveBuild.ts +26 -73
  53. package/src/server/serveDev.ts +19 -176
  54. package/src/server/streamEvent.ts +29 -0
  55. package/src/server/versionOf.ts +1 -2
  56. package/src/server/withAbideSources.ts +2 -1
  57. package/src/shared/HandlerRoute.ts +68 -0
  58. package/src/shared/REFUSAL_KEY.ts +5 -0
  59. package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
  60. package/src/shared/answerOf.ts +105 -78
  61. package/src/shared/filledPath.ts +1 -9
  62. package/src/shared/framing.ts +127 -128
  63. package/src/shared/queryText.ts +15 -7
  64. package/src/shared/reactive/Cursor.ts +69 -124
  65. package/src/shared/reactive/Reactive.ts +296 -162
  66. package/src/shared/reactive/callLoad.ts +4 -18
  67. package/src/shared/reactive/index.ts +11 -8
  68. package/src/shared/reactive/pairReactive.ts +1 -1
  69. package/src/shared/reactive/scoped.ts +4 -3
  70. package/src/shared/reactive/share.ts +11 -1
  71. package/src/shared/routePattern.ts +0 -110
  72. package/src/shared/rpc.ts +17 -24
  73. package/src/shared/rpcRequest.ts +18 -47
  74. package/src/shared/takeEarlyCall.ts +12 -37
  75. package/src/shared/url.ts +1 -1
  76. package/src/ui/Level.ts +276 -0
  77. package/src/ui/List.ts +17 -17
  78. package/src/ui/Router.ts +20 -87
  79. package/src/ui/applyHead.ts +92 -79
  80. package/src/ui/index.ts +4 -2
  81. package/src/ui/reconcile.ts +4 -110
  82. package/src/ui/rows.ts +123 -2
  83. package/src/ui/wire.ts +12 -27
  84. package/src/compiler/hooksCalled.ts +0 -35
  85. package/src/server/clientBuildOptions.ts +0 -54
  86. package/src/server/refuseAddressCollision.ts +0 -37
  87. package/src/shared/reactive/chunksOfStream.ts +0 -57
  88. package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/docs/RULEBOOK.md CHANGED
@@ -106,11 +106,11 @@ second.
106
106
 
107
107
  2.4 A failed revalidation MUST NOT cause a read to throw.
108
108
 
109
- 2.5 `s[Symbol.asyncIterator]` MUST yield the production in flight from its start, and MUST then
110
- continue live.
109
+ 2.5 On a value that is not a streaming producer, `s[Symbol.asyncIterator]` MUST settle as `s.then`
110
+ does, yielding once what it resolves with.
111
111
 
112
- 2.6 On a value that is not a streaming producer, `s[Symbol.asyncIterator]` MUST hand back the
113
- standing value once per turn, however many productions or patches landed since the last.
112
+ 2.6 On a streaming producer, `s[Symbol.asyncIterator]` MUST yield the chunks of the run being served
113
+ from its first, and MUST end when that run closes.
114
114
 
115
115
  2.7 `s.then` MUST resolve with the value where `s.busy` becomes false and 2.2 would not throw.
116
116
 
@@ -270,9 +270,7 @@ the production it serves.
270
270
 
271
271
  6.9 An expiry MUST NOT report `s.busy` true.
272
272
 
273
- 6.10 A cursor MUST NOT be handed an `undefined` production.
274
-
275
- 6.11 A streaming producer's `ttl` MUST run from its run's close, and MUST NOT reach the run in
273
+ 6.10 A streaming producer's `ttl` MUST run from its run's close, and MUST NOT reach the run in
276
274
  flight.
277
275
 
278
276
  # 7. Triggers
@@ -396,6 +394,11 @@ made by one (11.61) MUST end with it.
396
394
  9.14 A subscriber's copy of a wired `Reactive` MUST report `s.busy` true until the first production
397
395
  arrives.
398
396
 
397
+ 9.15 A cursor over a streaming producer MUST throw, once it has yielded every chunk, the failure its
398
+ run ended on.
399
+
400
+ 9.16 A cursor over a streaming producer MUST end where a new run replaces the one it was reading.
401
+
399
402
  # 10. Sharing
400
403
 
401
404
  10.1 `reactive.share` MUST get or create in one call.
@@ -579,6 +582,9 @@ start again when the load ends.
579
582
 
580
583
  11.70 `s.set` MUST take only a `Reactive` whose `Failures` are this value's.
581
584
 
585
+ 11.71 An unkeyed `reactive` with a `store`, built on a server outside every request, MUST NOT run
586
+ the `store`'s `get` before it is reached.
587
+
582
588
  # 12. Effects
583
589
 
584
590
  12.1 A `Disposer` MUST run before each rerun of its `Effect`, and once more at teardown.
@@ -896,7 +902,7 @@ such a call MUST NOT reach it.
896
902
  the reader asks for that type and `application/jsonl` otherwise.
897
903
 
898
904
  16.45 An sse answer MUST frame each value as `data: <json>` followed by a blank line, with
899
- `text/event-stream`, `no-cache` and `X-Accel-Buffering: no`.
905
+ `text/event-stream` and `cache-control: private, no-store`.
900
906
 
901
907
  16.46 A stream of value chunks MUST admit jsonl and then sse, and a stream of binary chunks MUST
902
908
  admit `application/octet-stream` alone.
@@ -981,6 +987,15 @@ naming it.
981
987
  16.74 A module of the client graph importing a handler file MUST receive the wrapper 16.28 states as
982
988
  that file's default.
983
989
 
990
+ 16.75 A streamed answer — jsonl, sse, bytes, or a document `page` hands a stream — MUST carry
991
+ `X-Accel-Buffering: no`.
992
+
993
+ 16.76 An `Rpc` streaming under 16.32 MUST end its answer where the run it sends ends: at that run's
994
+ close, where a new run replaces it, and with the failure that run ended on.
995
+
996
+ 16.77 A `timeout` MUST NOT end a `throttle` or `debounce` window holding a production back, and the
997
+ call MUST wait on that window as 5.15 has it.
998
+
984
999
  # 17. Response helpers
985
1000
 
986
1001
  17.1 Every body helper MUST take `Values<T>`.
@@ -1083,6 +1098,10 @@ value again rather than apply what a patch wrote, as 18.12 has it for a version
1083
1098
  18.26 A subscription to a `GET` MUST pass the `middleware` 37.2 runs at the route of the file that
1084
1099
  declared it, as it opens, and MUST NOT be fed where a rung answered in the handler's place.
1085
1100
 
1101
+ 18.27 A reader of a `text/event-stream` answer — an `Rpc`'s copy and `abide watch` alike — MUST read
1102
+ it by the format's fields: a comment skipped, the space after a field's colon optional, `data` lines
1103
+ joined, and an event ended by a blank line.
1104
+
1086
1105
  # 19. Mount paths and schemas
1087
1106
 
1088
1107
  19.1 A route MUST be answered for each method by one file at most, a page answering `GET` and `HEAD`
@@ -1164,8 +1183,8 @@ lists a handler for it. What a closed one answers is 36.7.
1164
1183
 
1165
1184
  20.8 Every `/__abide/**` address MUST run inside the app's own middleware.
1166
1185
 
1167
- 20.9 `/__abide/mcp` MUST publish a handler that reads as a tool and a resource according to which
1168
- arm `GET` took, and MUST NOT require a flag to tell them apart.
1186
+ 20.9 `/__abide/mcp` MUST publish every handler it lists as a tool, a `GET` taking no args included,
1187
+ and MUST NOT publish one as a resource.
1169
1188
 
1170
1189
  20.10 A handler whose own `Middleware` rung would refuse a caller MUST still be listed, and MUST
1171
1190
  still be refused on the call.
@@ -1294,7 +1313,8 @@ segment a page below it adds.
1294
1313
 
1295
1314
  23.15 A file under a fence MUST NOT have a route.
1296
1315
 
1297
- 23.16 A fence under a fence MUST be a build error naming both.
1316
+ 23.16 A fence under a fence MUST be a build error naming both, save a `server` directory under a
1317
+ `lib` or a `tests` directory.
1298
1318
 
1299
1319
  23.17 A route whose first segment is `__abide` MUST be a build error.
1300
1320
 
@@ -1432,8 +1452,7 @@ module ran is not the one served after it.
1432
1452
  27.12 `Config` MUST admit a field an app declares on it, of any type.
1433
1453
 
1434
1454
  27.13 `abide check` and `abide lsp` MUST type `Config` with what the defaults of every `onConfig`
1435
- called as a statement at the top of a `<script server>`, a handler file or a module under a `server`
1436
- directory return, and a union return with the keys
1455
+ called as a statement at the top of a `<script server>` or a handler file return, and a union return with the keys
1437
1456
  every member of it has.
1438
1457
 
1439
1458
  # 28. Logging
@@ -1563,12 +1582,16 @@ the page shown, and MUST NOT where only its query or fragment does.
1563
1582
  30.25 A `navigate` that does not remount the page MUST NOT move the scroll, except to the element a
1564
1583
  fragment it carries names.
1565
1584
 
1566
- 30.26 `abide check` MUST refuse a `url` literal that no page of the app is filed under and no public
1567
- file of the app is served at.
1585
+ 30.26 `abide check` MUST refuse a `url` literal that no page route of the app (23.3) is filed under
1586
+ and no public file of the app is served at.
1568
1587
 
1569
1588
  30.27 A `navigate` whose request fails, or whose route's chunks fail to load, MUST leave the address
1570
1589
  to the document.
1571
1590
 
1591
+ 30.28 A `Query` record and the args an `Rpc` carries in its URL MUST be written in one spelling: a
1592
+ member left `undefined` omitted, a list its member repeated, an empty list its name with no `=`, and
1593
+ anything else its text.
1594
+
1572
1595
  # 31. Reading and writing by name
1573
1596
 
1574
1597
  31.1 Inside a `.abide` file a name bound to a `Reactive` MUST be read wherever 31.3 does not hold it.
@@ -2042,7 +2065,8 @@ answered per 41.12.
2042
2065
 
2043
2066
  37.1 A lifecycle hook MUST be filed at the route of the `.abide` or the handler file that called it,
2044
2067
  one under a `lib` or a `tests` directory at the route of the directory holding that one, and at the
2045
- root route where neither did.
2068
+ root route where neither did; a call MUST be read as a hook only where it names a hook the module
2069
+ imports from abide, under any name, or a member of a namespace it imports from abide.
2046
2070
 
2047
2071
  37.2 `middleware` filed below the root route MUST run after routing, over a request its route or a
2048
2072
  route below it answers, outermost route first.
@@ -2079,6 +2103,9 @@ through `App.stop`, and the process MUST then exit with 0.
2079
2103
 
2080
2104
  37.12 A second `SIGTERM` or `SIGINT` while the app is stopping MUST end the process at once.
2081
2105
 
2106
+ 37.13 A lifecycle hook called in a module of the app's own that is neither a `.abide` nor a handler
2107
+ file MUST be a build error naming the module and the hook.
2108
+
2082
2109
  # 38. Commands
2083
2110
 
2084
2111
  38.1 `abide scaffold` MUST write a starter project, and MUST skip a step already done.
@@ -2139,7 +2166,8 @@ link none where the build bundled none.
2139
2166
  by the replacement.
2140
2167
 
2141
2168
  38.25 `--root` MUST default to `src` where the working directory holds one, and to the working
2142
- directory otherwise.
2169
+ directory otherwise; and the app it finds MUST be the one the whole command means — what it builds,
2170
+ boots, serves and announces.
2143
2171
 
2144
2172
  38.26 A command that finds no app at its `--root` MUST fail, naming where it looked.
2145
2173
 
@@ -2163,7 +2191,7 @@ warnings alone.
2163
2191
  every file that refusal does not stop.
2164
2192
 
2165
2193
  38.34 `abide lsp` MUST report what 38.3 reports, over the text the editor holds rather than the
2166
- text on disk.
2194
+ text on disk, and every refusal the app's tree makes of the file, as the tree was last saved.
2167
2195
 
2168
2196
  38.35 `abide dev` MUST send a response body as the app produces it.
2169
2197
 
@@ -2385,6 +2413,11 @@ with `--json` it MUST print the same as one JSON document.
2385
2413
  38.109 Under `bun test` with `@abide/abide/test` preloaded, a test MUST import a `.abide` of the app as
2386
2414
  the module it compiles to, whether or not `createApp` has run.
2387
2415
 
2416
+ 38.110 A build `abide start`, `abide call`, `abide openapi` or `abide mcp` makes first (38.100) MUST
2417
+ be reported on stderr, leaving the command's stdout to its own output.
2418
+
2419
+ 38.111 `abide openapi` MUST print the OpenAPI document on stdout.
2420
+
2388
2421
  # 39. Head and view transitions
2389
2422
 
2390
2423
  39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
@@ -2396,7 +2429,8 @@ the module it compiles to, whether or not `createApp` has run.
2396
2429
  39.4 `<head>` contributions MUST be merged by key, and the deepest contributor MUST win. The shell's
2397
2430
  `<head>` MUST be the outermost contributor.
2398
2431
 
2399
- 39.5 A `<head>` contribution with no merge key MUST append in tree order.
2432
+ 39.5 A `<head>` contribution with no merge key MUST append — in tree order among the contributions
2433
+ its level mounts with, and when it mounts for a component mounted after its level.
2400
2434
 
2401
2435
  39.6 On a client navigation the incoming page's keyed `<head>` contributions MUST replace the
2402
2436
  outgoing page's by key, and a layout's MUST NOT move.
@@ -2418,12 +2452,19 @@ app MUST NOT be required to feature-detect.
2418
2452
 
2419
2453
  39.12 The last `<head>` contribution for a key within one file MUST win.
2420
2454
 
2421
- 39.13 In a browser, a `<head>` contribution that reads a value MUST follow it for as long as its page
2422
- or layout is mounted.
2455
+ 39.13 In a browser, a `<head>` contribution that reads a value MUST follow it for as long as the
2456
+ page, layout or component that made it is mounted.
2423
2457
 
2424
- 39.14 On a client navigation, a keyed `<head>` contribution that a leaving page or layout made and
2425
- no arriving one makes MUST return to the nearest kept layout's value for its key, and MUST be removed
2426
- where no kept layout has that key.
2458
+ 39.14 When a page, layout or component leaves — on a client navigation, or with its region — a keyed
2459
+ `<head>` contribution it made and no deeper contributor still makes MUST return to the nearest
2460
+ remaining outer contributor's value for its key, and MUST be removed where no remaining outer
2461
+ contributor has that key.
2462
+
2463
+ 39.15 A `<head>` contribution with no merge key that its page, layout or component writes again
2464
+ MUST replace, in place, the one that same contributor wrote before.
2465
+
2466
+ 39.16 A `<head>` contribution with no merge key MUST be removed when the page, layout or component
2467
+ that made it leaves.
2427
2468
 
2428
2469
  # 40. Documentation
2429
2470
 
@@ -2595,6 +2636,9 @@ has answered or failed.
2595
2636
  41.27 An `Rpc` call `render` built the page's markup from in flight MUST be in flight as hydration
2596
2637
  adopts the page, however early its answer arrived.
2597
2638
 
2639
+ 41.28 A `render` MUST let the turn's queued microtasks run between setting up a `<Name/>` whose setup
2640
+ made an `Rpc` call and building that component's markup.
2641
+
2598
2642
  # 42. The app object
2599
2643
 
2600
2644
  42.1 `createApp` MUST build the route table and compose every `Middleware` rung, per 16.14.
package/docs/abide.md CHANGED
@@ -573,7 +573,7 @@ Read on: [Head & metadata](/pages/set-the-title-and-social-preview.md) · [Layou
573
573
 
574
574
  Rendering a page runs its setup: the `<script>` blocks, the `reactive` calls, the bodies the markup reads. Each read that needs the server starts its work immediately, so the loads on a page are in flight together rather than in the order the markup happens to mention them.
575
575
 
576
- The total time is the slowest one, not the sum — and the first byte does not wait for any of them. The markup waits one turn, so a load that answers without waiting on anything is painted rather than left as a hole.
576
+ The total time is the slowest one, not the sum — and the first byte does not wait for any of them. The markup waits one turn, so a load that answers without waiting on anything is painted rather than left as a hole. A component whose setup starts a load gets the same turn before its own markup is built, however deep it sits.
577
577
 
578
578
  ## 4. The first bytes leave
579
579
 
@@ -1597,11 +1597,11 @@ Read on: [Throttle & debounce](/values/slow-down-a-value-that-changes-too-fast.m
1597
1597
 
1598
1598
  ## Following a channel sees turns, not every message
1599
1599
 
1600
- `for await (const turns of onboarding)` walks the channel's cursor. On a value with no streaming producer the cursor hands back the standing value once per turn, however many writes landed since the last, and a patch in place is one of them (2.6). A burst of three messages in one turn is one step, holding all three.
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.
1601
1601
 
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. The cursor never hands back an `undefined` value (6.10).
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.
1603
1603
 
1604
- A stream is the other case: its cursor yields from the start of the run in flight and then live, one step per chunk (2.5), and a capped window still delivers every chunk (5.10).
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).
1605
1605
 
1606
1606
  Read on: [Watching values](/values/do-something-when-a-value-changes.md) · [Streaming data](/data/send-data-as-it-arrives.md)
1607
1607
 
@@ -2506,7 +2506,7 @@ Two costs, and both are yours rather than abide's.
2506
2506
 
2507
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.
2508
2508
 
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 start and then goes live (2.5), and a reader leaving does not cancel the producer for the ones that remain (11.39).
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).
2510
2510
 
2511
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)
2512
2512
 
@@ -3652,7 +3652,7 @@ Read on: [Patching a list](/values/patch-a-list-from-a-live-feed.md) · [Lists](
3652
3652
 
3653
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.
3654
3654
 
3655
- A reader cannot build the window for you. `for await` over a value hands back the standing value once per turn, and a burst of writes in one turn is one step (2.6), 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.
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.
3656
3656
 
3657
3657
  A channel is this same shape with many writers — a `reactive` over an array that everyone connected reads, trimmed the same way.
3658
3658
 
@@ -4930,7 +4930,7 @@ Each takes `Values<T>`, and sse is reachable from jsonl without declaring it: se
4930
4930
 
4931
4931
  ## An async generator streams as JSON Lines
4932
4932
 
4933
- A handler returning an `AsyncGenerator` is framed as **jsonl** — one JSON value per line, `application/jsonl`, or `application/x-ndjson` for a reader that asks for NDJSON by name (16.44). The body is written per `pull`, so back-pressure reaches your database cursor rather than piling rows up in memory.
4933
+ A handler returning an `AsyncGenerator` is framed as **jsonl** — one JSON value per line, `application/jsonl`, or `application/x-ndjson` for a reader that asks for NDJSON by name (16.44). The body is written per `pull`, so back-pressure reaches your database cursor rather than piling rows up in memory, and it carries `X-Accel-Buffering: no`, as every streamed answer does, so a proxy in front passes each line on as it arrives (16.75).
4934
4934
 
4935
4935
  That is the default framing and not the only one.
4936
4936
 
@@ -4986,7 +4986,7 @@ const feed = reactive(async () => {
4986
4986
  *1 helper, 0 lines of framing* — the arm writes `data: `, the blank line after it and the two headers a proxy needs to leave the stream alone, by hand, per event.
4987
4987
 
4988
4988
  The card's request carries no `Accept`, which is what a wallboard script or a `curl` sends, and without `sse` that caller is answered jsonl. A caller that does ask — an `EventSource` sends `Accept:
4989
- text/event-stream` — gets the same events from a handler that never declared them: the jsonl transcript reframed as `data: <json>` per line with a blank line after it, `no-cache` and `X-Accel-Buffering: no`. That is one URL with two bodies, so it carries `vary: accept`. Declaring `sse` is for the caller who cannot say what it wants. The `:` the card's answer opens on is a comment, which sends the head before the first event exists.
4989
+ text/event-stream` — gets the same events from a handler that never declared them: the jsonl transcript reframed as `data: <json>` per line with a blank line after it, under `private, no-store` like any rpc answer. That is one URL with two bodies, so it carries `vary: accept`. Declaring `sse` is for the caller who cannot say what it wants. The `:` the card's answer opens on is a comment, which sends the head before the first event exists.
4990
4990
 
4991
4991
  Read on: [Response types](/data/answer-with-something-other-than-json.md)
4992
4992
 
@@ -5176,7 +5176,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Lists
5176
5176
 
5177
5177
  ## A reader arriving mid-run replays the run
5178
5178
 
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 cursor over the stream hands it the run in flight from its start before it goes live (2.5). So a reader arriving halfway through still paints every row.
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.
5180
5180
 
5181
5181
  ## A stream is not a channel
5182
5182
 
@@ -5386,7 +5386,7 @@ The call hands back the same `Reactive` the handler had, `Produced` included (16
5386
5386
 
5387
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`.
5388
5388
 
5389
- `room.invalidate({ id })` and `room.refresh({ id })` inside a handler reach every browser's copy of that room (7.10, 7.18). A `for await` over the room hands back the standing array once per turn, a burst collapsing to the latest (2.6), so code that has to act on every message acts in `say`.
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`.
5390
5390
 
5391
5391
  ## Each instance holds its own room
5392
5392
 
@@ -7220,7 +7220,7 @@ Read on: [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) · [Req
7220
7220
 
7221
7221
  The incoming page's keyed contributions replace the outgoing page's **by key**. A layout's contributions do not move at all, the layout not having been rebuilt — so the app's icon and its font preloads are declared once and never touched again.
7222
7222
 
7223
- A key only the outgoing page set goes back to the layout's value, and is removed where no layout has one. The tab ends on the title a fresh load of that address shows.
7223
+ A key only the outgoing page set goes back to the layout's value, and is removed where no layout has one, and what it appended with no key leaves with it. The tab ends on the title a fresh load of that address shows.
7224
7224
 
7225
7225
  Read on: [Links & navigation](/pages/link-to-another-page.md)
7226
7226
 
@@ -9229,27 +9229,10 @@ Read on: [Config](/app/configure-the-app.md) · [Authorization](/data/decide-who
9229
9229
 
9230
9230
  ## A root rung answers what no route does
9231
9231
 
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:
9233
-
9234
- ```ts src/lib/downloads.ts — excerpt
9235
- middleware(async (next, { request }) => {
9236
- const { pathname, origin } = new URL(request.url)
9237
- if (!pathname.endsWith('.md') && !pathname.endsWith('/llms.txt'))
9238
- return next()
9239
- if (pathname === '/llms.txt')
9240
- return new Response('# abide\n\nEvery page, as markdown: abide.md\n')
9241
- if (pathname === '/abide.md') {
9242
- bundled ??= bundle(origin)
9243
- return new Response(await bundled, { headers: MARKDOWN })
9244
- }
9245
- for (const group of SITE)
9246
- for (const page of group.pages)
9247
- if (pathname === markdownAddressOf(page.route))
9248
- return new Response(await pageMarkdown(page.route, origin), {
9249
- headers: MARKDOWN,
9250
- })
9251
- return next()
9252
- })
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<
9253
9236
  ```
9254
9237
 
9255
9238
  It uses the same response helpers everything else does, there being no second set.
@@ -10006,13 +9989,15 @@ let refused = reactive('—')
10006
9989
  <script server>
10007
9990
  // The docs app's own hooks; each example files its own in its own file.
10008
9991
  import { csp, middleware, onConfig } from '@abide/abide/server'
10009
- import '#src/lib/downloads.ts'
9992
+ import { serveDownloads } from '#src/lib/serveDownloads.ts'
10010
9993
 
10011
9994
  // The sign-in examples need a secret: one per process, or the deployment's.
10012
9995
  const PRINCIPAL_SECRET = crypto.randomUUID()
10013
9996
 
10014
9997
  onConfig(() => ({ ABIDE_PRINCIPAL_SECRET: PRINCIPAL_SECRET }))
10015
9998
 
9999
+ middleware(serveDownloads)
10000
+
10016
10001
  // Every docs page's policy; 'self' so the docs can frame invoice previews.
10017
10002
  middleware(csp({ 'frame-ancestors': ["'self'"] }))
10018
10003
  </script>
@@ -10276,10 +10261,10 @@ The address runs inside the app's own middleware like every `/__abide/**` addres
10276
10261
  ## Written rather than served
10277
10262
 
10278
10263
  ```
10279
- abide openapi --out dist/openapi.json
10264
+ abide openapi > dist/openapi.json
10280
10265
  ```
10281
10266
 
10282
- The same document, produced from the source rather than asked of a server. That is what a build step wants — a spec committed beside the code, or handed to a client generator — and it does not require the app to be running or the address to be on.
10267
+ The same document, printed from the source rather than asked of a server. That is what a build step wants — a spec committed beside the code, or handed to a client generator — and it does not require the app to be running or the address to be on.
10283
10268
 
10284
10269
  `--url` points it at a running app instead, which is what you want when the question is what *that deployment* is serving.
10285
10270
 
@@ -10307,11 +10292,11 @@ Read on: [Withholding a surface](/machines/keep-a-handler-off-a-surface.md)
10307
10292
 
10308
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.
10309
10294
 
10310
- ## The method decides tool or resource
10295
+ ## Every handler is a tool
10311
10296
 
10312
- A handler is published as a **tool** or as a **resource** according to which arm `GET` took, and no flag tells them apart. A read is a resource, a mutation is a tool — which is the distinction MCP already draws, arrived at from the declaration rather than from an annotation somebody has to keep true.
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.
10313
10298
 
10314
- That is the whole of the mapping, and it is worth being able to state in one line: the model sees your app's verbs because your app already declared which of its handlers change something.
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.
10315
10300
 
10316
10301
  ## The description is the handler's
10317
10302
 
@@ -11810,8 +11795,8 @@ Every `Reactive` takes these, whichever overload made it.
11810
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. |
11811
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. |
11812
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. |
11813
- | `for await (… of s)` | `AsyncIterable<Produced>` | The live cursor face of a read. On a stream, one turn per chunk. On any other value, the standing value once per turn — a burst collapses to the latest, and an in-place `patch` is a turn of its own. |
11814
- | `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The production in flight from its start, and then what follows it. A chunk is a piece of one value, so a stream's cursor sees every chunk; any other value's cursor sees where the value stands. |
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. |
11815
11800
 
11816
11801
  ## Writes
11817
11802
 
@@ -12843,7 +12828,7 @@ src/
12843
12828
 
12844
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.
12845
12830
 
12846
- `lib`, `tests`, `server` and `public` are the fences, and nothing under one has a route (23.15). One inside another is a build error, 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.
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.
12847
12832
 
12848
12833
  ### Resolution order
12849
12834
 
@@ -12967,7 +12952,7 @@ A page and a handler file are **addresses rather than names**, and so are `index
12967
12952
  | `onPrincipal` | `(resolve: (claims: unknown) => unknown \| Promise<unknown>) => () => void` | Turns claims into the app's half of a principal. |
12968
12953
  | `onLauncher` | `(options: () => LauncherOptions) => () => void` | The window the desktop app opens and the forms its launcher offers. |
12969
12954
 
12970
- 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, and at the root where a module called it (37.1). 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.
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.
12971
12956
 
12972
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.
12973
12958
 
@@ -12985,7 +12970,7 @@ Every one hands back the way off again, so a module that is torn down can remove
12985
12970
 
12986
12971
  ### `Config` is typed from the `onConfig` calls
12987
12972
 
12988
- `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>`, of a handler file or of a module under a `server` directory (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.
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.
12989
12974
 
12990
12975
  ### `NODE_ENV` is verbatim
12991
12976
 
@@ -13047,7 +13032,7 @@ abide --help
13047
13032
  - `--port <n>` — the listen port for `dev` and `start`. Defaults to `PORT`, then to 3000 — where `dev` moves on to the first free port through 3099.
13048
13033
  - `--root <dir>` — the app's source directory. Defaults to `src` where you are standing, and to where you are standing when there is none — so `cd ledger && abide dev` and `abide dev --root
13049
13034
  ./src` are the same command. A directory holding no `.abide` under a `package.json` is not an app, and the command says so rather than building nothing.
13050
- - `--out <dir>` — where `build` writes, beside the app rather than beside you: building `packages/ledger/src` from anywhere leaves `packages/ledger/.abide`, which is where `start` looks. It is also the file `openapi` and `compile` write, rather than standard output.
13035
+ - `--out <dir>` — where `build` writes, beside the app rather than beside you: building `packages/ledger/src` from anywhere leaves `packages/ledger/.abide`, which is where `start` looks. It is also the file `compile` and `bundle` write.
13051
13036
  - `--url <origin>` — the running app the shell, `openapi`, `mcp`, `call` and `logs` talk to. Defaults to `ABIDE_APP_URL`, then to the local app.
13052
13037
  - `--target`, `--platforms` — what `compile` builds for.
13053
13038
 
@@ -13071,7 +13056,7 @@ abide --help
13071
13056
  | `abide start [--port <n>]` | Boots the built app. |
13072
13057
  | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. |
13073
13058
  | `abide call <address> [args]` | One call to one handler. |
13074
- | `abide openapi [--out <file>] [--url <origin>]` | The OpenAPI document written rather than served. |
13059
+ | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. |
13075
13060
  | `abide mcp [--url <origin>]` | The app's MCP server over stdio. |
13076
13061
  | `abide watch <address> [args]` | A live subscription to one of the app's followed `GET`s. |
13077
13062
  | `abide compile [--target] [--out] [--platforms]` | One standalone executable. |
@@ -13115,7 +13100,7 @@ abide call invoices/getInvoice id=4310
13115
13100
  ### Write the OpenAPI document at build time
13116
13101
 
13117
13102
  ```
13118
- abide openapi --out dist/openapi.json
13103
+ abide openapi > dist/openapi.json
13119
13104
  ```
13120
13105
 
13121
13106
  ## See also
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.103.0",
3
+ "version": "0.105.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -33,6 +33,12 @@ import { nesting, type Token, tokenize } from './tokenize.ts'
33
33
  // no parentheses to survive one.
34
34
  const SHORT_CIRCUIT: ReadonlySet<string> = new Set(['||', '&&', '??'])
35
35
 
36
+ /** 23.9 — a page's or a layout's route, which its `route.params` promises. */
37
+ export type Routed = { route: string; kind: 'page' | 'layout' }
38
+
39
+ /** A run of emitted code a `Lowering` wrote, and where each of its runs stood in the projection. */
40
+ export type LoweredChunk = { text: string; origins: Origin[] }
41
+
36
42
  /**
37
43
  * One file's lowering: each piece the emitters ask for, with its reactive reads rewritten.
38
44
  *
@@ -42,9 +48,6 @@ const SHORT_CIRCUIT: ReadonlySet<string> = new Set(['||', '&&', '??'])
42
48
  * so the import goes with the call; left to the callers it is one `used.add` per call site and a
43
49
  * missed one emits a module calling a name it never imported.
44
50
  */
45
- /** A run of emitted code a `Lowering` wrote, and where each of its runs stood in the projection. */
46
- export type LoweredChunk = { text: string; origins: Origin[] }
47
-
48
51
  export class Lowering {
49
52
  readonly projection: Projection
50
53
  readonly used = new Set<string>()
@@ -85,18 +88,25 @@ export class Lowering {
85
88
  * the projection: what `sourceMapOf` finds in the module the emitter writes.
86
89
  */
87
90
  private readonly chunks: LoweredChunk[] | undefined
91
+ /**
92
+ * 23.9 — for a checked lowering of a page or a layout, its route and which of the two it is:
93
+ * what its `route.params` is typed by.
94
+ */
95
+ private readonly routed: Routed | undefined
88
96
 
89
97
  constructor(
90
98
  file: AbideFile,
91
99
  answer: CheckerAnswer,
92
100
  checking = false,
93
101
  chunks?: LoweredChunk[],
102
+ routed?: Routed,
94
103
  ) {
95
104
  this.projection = projectShadow(file)
96
105
  this.answer = answer
97
106
  this.holdsSlot = holdsSlot(file.template)
98
107
  this.checking = checking
99
108
  this.chunks = chunks
109
+ this.routed = routed
100
110
  }
101
111
 
102
112
  /**
@@ -441,8 +451,8 @@ export class Lowering {
441
451
  const spelled = this.projection.text.slice(read.start, read.end)
442
452
  // 23.9 — WHAT THIS PAGE'S OWN ROUTE PROMISES, for the checker alone: the running module
443
453
  // reads the one `route` every page shares.
444
- if (this.checking && read.routeParams !== undefined)
445
- return `(${spelled} as unknown as () => __RouteParams<${JSON.stringify(read.routeParams[0])}, ${JSON.stringify(read.routeParams[1])}>)`
454
+ if (this.routed !== undefined && read.routeParams)
455
+ return `(${spelled} as unknown as () => __RouteParams<${JSON.stringify(this.routed.route)}, ${JSON.stringify(this.routed.kind)}>)`
446
456
  return read.through ? `${spelled}()` : spelled
447
457
  }
448
458
 
@@ -0,0 +1,57 @@
1
+ import { resolve } from 'node:path' // bun exposes no path helpers of its own
2
+ import { formatDiagnosis } from './formatDiagnosis.ts'
3
+ import { diagnosisAt } from './lineOf.ts'
4
+ import type { Diagnosis } from './TypeSession.ts'
5
+
6
+ /**
7
+ * Where everything the compile refuses goes, each at the file it was refused in.
8
+ *
9
+ * TWO MODES AND ONE CALL: a build stops at a refusal, so a throwing sink throws it; `abide
10
+ * check` reports every one at its file and goes on (38.33), so a collecting sink keeps each as a
11
+ * finding at the file's first line. Every stage takes this and nothing else, so no stage decides for
12
+ * itself what a refusal does, and none spells a path its own way: a file is named relative to `root`
13
+ * or absolutely, and kept absolute.
14
+ */
15
+ export class Refusals {
16
+ /** Every refusal kept, in the order met; empty in a throwing sink. */
17
+ readonly found: Diagnosis[] = []
18
+ /** What a file below the root is named relative to. */
19
+ readonly root: string
20
+ /** Whether a refusal is kept and the compile goes on, rather than thrown. */
21
+ readonly collecting: boolean
22
+
23
+ constructor(root: string, collecting: boolean) {
24
+ this.root = root
25
+ this.collecting = collecting
26
+ }
27
+
28
+ /** Refuses at `file`, below the root or absolute: `thrown` is thrown, or kept. */
29
+ at(file: string, thrown: unknown): void {
30
+ if (!this.collecting) throw thrown
31
+ this.found.push(
32
+ diagnosisAt(
33
+ resolve(this.root, file),
34
+ undefined,
35
+ 0,
36
+ 'error',
37
+ thrown instanceof Error ? thrown.message : String(thrown),
38
+ ),
39
+ )
40
+ }
41
+
42
+ /**
43
+ * What another stage kept, as this one takes it: every one kept, or every one thrown as one
44
+ * error, each at its file — a build that stops names all it found rather than a first one and a
45
+ * second run (11.37).
46
+ */
47
+ adopt(found: readonly Diagnosis[]): void {
48
+ if (found.length === 0) return
49
+ if (this.collecting) {
50
+ this.found.push(...found)
51
+ return
52
+ }
53
+ const lines: string[] = []
54
+ for (const one of found) lines.push(formatDiagnosis(one, process.cwd()))
55
+ throw new Error(lines.join('\n'))
56
+ }
57
+ }