@abide/abide 0.110.4 → 0.112.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/docs/REGISTRY.md +30 -29
- package/docs/RULEBOOK.md +98 -22
- package/docs/abide.md +46 -32
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +5 -0
- package/src/compiler/classifyTemplate.ts +129 -34
- package/src/compiler/compileApp.ts +2 -1
- package/src/compiler/emitClient.ts +128 -14
- package/src/compiler/renderMarkup.ts +14 -8
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/HEARTBEAT_MILLISECONDS.ts +2 -0
- package/src/server/IDLE_TIMEOUT_SECONDS.ts +6 -0
- package/src/server/answerRequest.ts +44 -3
- package/src/server/answerRpc.ts +14 -41
- package/src/server/answerSocket.ts +2 -2
- package/src/server/answerValue.ts +15 -3
- package/src/server/answerWorker.ts +195 -0
- package/src/server/appManifest.ts +48 -0
- package/src/server/assistant.ts +1 -1
- package/src/server/bridgeRelay.ts +4 -50
- package/src/server/claudeApi.ts +1 -1
- package/src/server/claudeCode.ts +3 -3
- package/src/server/clientBundle.ts +7 -0
- package/src/server/config.ts +14 -0
- package/src/server/createApp.ts +28 -4
- package/src/server/devWorker.ts +4 -1
- package/src/server/render.ts +2 -2
- package/src/server/requestScope.ts +31 -11
- package/src/server/responseRelay.ts +7 -7
- package/src/server/serveBuild.ts +2 -0
- package/src/server/serveDev.ts +4 -1
- package/src/server/streamEvent.ts +60 -1
- package/src/shared/ADDRESSES.ts +11 -0
- package/src/shared/ASCII_WHITESPACE.ts +6 -0
- package/src/shared/LONGEST_DELAY.ts +2 -0
- package/src/shared/OFFLINE_CACHES.ts +12 -0
- package/src/shared/OFFLINE_META.ts +7 -0
- package/src/shared/answerOf.ts +29 -4
- package/src/shared/attributeText.ts +1 -1
- package/src/shared/reactive/Expiry.ts +13 -1
- package/src/shared/reactive/Reactive.ts +13 -3
- package/src/shared/safeUrl.ts +8 -1
- package/src/ui/Level.ts +43 -95
- package/src/ui/Router.ts +37 -13
- package/src/ui/bindGroup.ts +41 -0
- package/src/ui/bindProperty.ts +1 -21
- package/src/ui/index.ts +5 -3
- package/src/ui/reconcileHead.ts +215 -0
- package/src/ui/registerWorker.ts +34 -0
- package/src/ui/routing.ts +1 -0
- package/src/ui/setClassGroup.ts +79 -0
- package/src/ui/setStyleGroup.ts +130 -0
- package/src/ui/socketWire.ts +33 -2
- package/src/ui/applyHead.ts +0 -214
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
classifyAttributes,
|
|
22
22
|
isBound,
|
|
23
23
|
isSingleRooted,
|
|
24
|
+
type MergeGroup,
|
|
24
25
|
isSlot,
|
|
25
26
|
} from './classifyTemplate.ts'
|
|
26
27
|
import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
|
|
@@ -220,9 +221,7 @@ export function emitClient(
|
|
|
220
221
|
const registered = heads
|
|
221
222
|
? `if (${ROUTED}) ${ROUTED}.setHead(() => ${headMarkup})\n`
|
|
222
223
|
: ''
|
|
223
|
-
const applied = heads
|
|
224
|
-
? `if (${ROUTED}) ${lowering.use('applyHead')}(${ROUTED}.headMarkup()); `
|
|
225
|
-
: ''
|
|
224
|
+
const applied = heads ? `if (${ROUTED}) ${ROUTED}.reconcile(); ` : ''
|
|
226
225
|
// Applied with the fill either way, so the watch's first run only subscribes: what it would
|
|
227
226
|
// write is already there, from the server or from the line above.
|
|
228
227
|
if (liveHeads) lines.push(`if (${ROUTED}) ${ROUTED}.followHead(${SCOPE})`)
|
|
@@ -767,6 +766,17 @@ function elementUnits(
|
|
|
767
766
|
)
|
|
768
767
|
// 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
|
|
769
768
|
// region's scope writes the property, and a listener writes it back.
|
|
769
|
+
// 32.8 — a group compares against the input's own value, read where the value is.
|
|
770
|
+
else if (one.kind === 'bound' && one.bind === 'group')
|
|
771
|
+
binds.push(
|
|
772
|
+
`${lowering.use('bindGroup')}(${element}, ${lowering.reference(one.attribute)}, () => (${
|
|
773
|
+
one.own === undefined
|
|
774
|
+
? "'on'"
|
|
775
|
+
: one.own.expression
|
|
776
|
+
? lowering.binding(one.own)
|
|
777
|
+
: JSON.stringify(one.own.value)
|
|
778
|
+
}), ${SCOPE})`,
|
|
779
|
+
)
|
|
770
780
|
else if (one.kind === 'bound')
|
|
771
781
|
binds.push(
|
|
772
782
|
`${lowering.use('bindProperty')}(${element}, ${JSON.stringify(one.bind)}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
|
|
@@ -798,16 +808,32 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
|
798
808
|
write: 'attribute' as const,
|
|
799
809
|
name: one.attribute.name,
|
|
800
810
|
}
|
|
811
|
+
// 32.71 — A SHARED GROUP IS ONE WATCH over its whole value, which wakes for any member:
|
|
812
|
+
// a directive cannot keep what the plain attribute holds from a watch of its own.
|
|
813
|
+
const group =
|
|
814
|
+
one.kind === 'merged' && writesGroup(write)
|
|
815
|
+
? one.group
|
|
816
|
+
: undefined
|
|
801
817
|
// 32.67 — one reading a row's item is one of the row's item watches, on a name of its
|
|
802
818
|
// own because it is installed apart from the element's other attributes.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
819
|
+
const asItem =
|
|
820
|
+
group === undefined
|
|
821
|
+
? lowering.readsOwnItem(one.attribute)
|
|
822
|
+
: groupReadsOwnItem(group, lowering)
|
|
823
|
+
const call = writeCall(
|
|
824
|
+
write,
|
|
825
|
+
asItem ? `__i${slot}` : `__a${slot}`,
|
|
826
|
+
name,
|
|
827
|
+
group === undefined
|
|
828
|
+
? lowering.binding(one.attribute)
|
|
829
|
+
: groupArguments(group, (attribute) =>
|
|
830
|
+
lowering.binding(attribute),
|
|
831
|
+
),
|
|
832
|
+
lowering,
|
|
833
|
+
)
|
|
834
|
+
;(asItem ? itemWrites : writes).push(
|
|
835
|
+
`${SCOPE}.${asItem ? 'itemWatch' : 'watch'}(() => ${call})`,
|
|
836
|
+
)
|
|
811
837
|
}
|
|
812
838
|
}
|
|
813
839
|
if (itemWrites.length > 0)
|
|
@@ -825,14 +851,27 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
|
|
|
825
851
|
const SCOPE = '__scope'
|
|
826
852
|
|
|
827
853
|
// Which runtime write each kind of attribute binding is handed to. A record rather than a chain
|
|
828
|
-
// of ternaries, so a
|
|
829
|
-
//
|
|
854
|
+
// of ternaries, so a sixth kind is a row here and the call site stays one line — all take the
|
|
855
|
+
// same three arguments, which is what makes them one site at all.
|
|
830
856
|
const WRITES = {
|
|
831
857
|
attribute: 'setAttribute',
|
|
832
858
|
class: 'setClass',
|
|
833
859
|
style: 'setStyleProperty',
|
|
860
|
+
classGroup: 'setClassGroup',
|
|
861
|
+
styleGroup: 'setStyleGroup',
|
|
834
862
|
} as const
|
|
835
863
|
|
|
864
|
+
/** One attribute binding's write: the element, the one name it owns, and the value. */
|
|
865
|
+
function writeCall(
|
|
866
|
+
write: keyof typeof WRITES,
|
|
867
|
+
element: string,
|
|
868
|
+
name: string,
|
|
869
|
+
value: string,
|
|
870
|
+
lowering: Lowering,
|
|
871
|
+
): string {
|
|
872
|
+
return `${lowering.use(WRITES[write])}(${element}, ${JSON.stringify(name)}, ${value})`
|
|
873
|
+
}
|
|
874
|
+
|
|
836
875
|
// The share scope an instance captures for its deferred mounts (10.5), named apart from the one an
|
|
837
876
|
// instance OPENS (`shareScope`): a page that shares nothing still carries this for a child that does.
|
|
838
877
|
const SHARE_AT = '__shareAt'
|
|
@@ -1059,6 +1098,41 @@ function buildRender(lowering: Lowering): ValueRender {
|
|
|
1059
1098
|
}
|
|
1060
1099
|
}
|
|
1061
1100
|
|
|
1101
|
+
/** 32.71 — whether a write is a shared group's whole value. */
|
|
1102
|
+
function writesGroup(write: keyof typeof WRITES): boolean {
|
|
1103
|
+
return write === 'classGroup' || write === 'styleGroup'
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** 32.71 — whether a shared group is one of a row's item watches: any member is. */
|
|
1107
|
+
function groupReadsOwnItem(group: MergeGroup, lowering: Lowering): boolean {
|
|
1108
|
+
for (const member of group.members)
|
|
1109
|
+
if (member.expression && lowering.readsOwnItem(member)) return true
|
|
1110
|
+
return false
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* 32.71 — a shared group's write arguments: the plain attribute's value, then every directive's
|
|
1115
|
+
* condition or value in the order the group names them, each read by `read`. A directive with no
|
|
1116
|
+
* expression is a class that is always on, or a property's own bytes.
|
|
1117
|
+
*/
|
|
1118
|
+
function groupArguments(
|
|
1119
|
+
group: MergeGroup,
|
|
1120
|
+
read: (attribute: Attribute) => string,
|
|
1121
|
+
): string {
|
|
1122
|
+
const values: string[] = []
|
|
1123
|
+
for (const directive of group.directives)
|
|
1124
|
+
values.push(
|
|
1125
|
+
directive.expression
|
|
1126
|
+
? read(directive)
|
|
1127
|
+
: group.name === 'class'
|
|
1128
|
+
? 'true'
|
|
1129
|
+
: JSON.stringify(
|
|
1130
|
+
decodeCharacterReferences(directive.value, 'attribute'),
|
|
1131
|
+
),
|
|
1132
|
+
)
|
|
1133
|
+
return `${read(group.plain as Attribute)}, [${values.join(', ')}]`
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1062
1136
|
/**
|
|
1063
1137
|
* Emits a region's `_build` and `_mount` pair into `helpers`, for a region nobody painted.
|
|
1064
1138
|
*
|
|
@@ -1565,6 +1639,40 @@ function rowUpdate(
|
|
|
1565
1639
|
position.holder as ElementNode,
|
|
1566
1640
|
lowering,
|
|
1567
1641
|
)) {
|
|
1642
|
+
// 32.71 — a shared group is written whole where a member moves with the item — and
|
|
1643
|
+
// where none is live, as for any attribute: a live group's watch reads the item too.
|
|
1644
|
+
if (
|
|
1645
|
+
one.kind === 'merged' &&
|
|
1646
|
+
one.group !== undefined &&
|
|
1647
|
+
one.toggles !== undefined &&
|
|
1648
|
+
writesGroup(one.toggles.write)
|
|
1649
|
+
) {
|
|
1650
|
+
if (one.live) continue
|
|
1651
|
+
for (const member of one.group.members)
|
|
1652
|
+
if (
|
|
1653
|
+
member.expression &&
|
|
1654
|
+
!lowering.isLive(member) &&
|
|
1655
|
+
lowering.readsItem(member, block)
|
|
1656
|
+
) {
|
|
1657
|
+
own.push(
|
|
1658
|
+
writeCall(
|
|
1659
|
+
one.toggles.write,
|
|
1660
|
+
`__a${slot}`,
|
|
1661
|
+
one.toggles.name,
|
|
1662
|
+
groupArguments(one.group, (attribute) =>
|
|
1663
|
+
unlandable(
|
|
1664
|
+
lowering,
|
|
1665
|
+
attribute,
|
|
1666
|
+
lowering.expression(attribute),
|
|
1667
|
+
),
|
|
1668
|
+
),
|
|
1669
|
+
lowering,
|
|
1670
|
+
),
|
|
1671
|
+
)
|
|
1672
|
+
break
|
|
1673
|
+
}
|
|
1674
|
+
continue
|
|
1675
|
+
}
|
|
1568
1676
|
if (!one.attribute.expression || one.live) continue
|
|
1569
1677
|
if (!lowering.readsItem(one.attribute, block)) continue
|
|
1570
1678
|
const toggles =
|
|
@@ -1578,7 +1686,13 @@ function rowUpdate(
|
|
|
1578
1686
|
: undefined
|
|
1579
1687
|
if (toggles === undefined) continue
|
|
1580
1688
|
own.push(
|
|
1581
|
-
|
|
1689
|
+
writeCall(
|
|
1690
|
+
toggles.write,
|
|
1691
|
+
`__a${slot}`,
|
|
1692
|
+
toggles.name,
|
|
1693
|
+
lowering.expression(one.attribute),
|
|
1694
|
+
lowering,
|
|
1695
|
+
),
|
|
1582
1696
|
)
|
|
1583
1697
|
}
|
|
1584
1698
|
if (own.length === 0) continue
|
|
@@ -354,13 +354,9 @@ function renderPosition(
|
|
|
354
354
|
// `<br>`, and a close tag written for one is markup no browser accepts.
|
|
355
355
|
if (node.children.length === 0 && VOID_ELEMENTS.has(node.lowerTag))
|
|
356
356
|
return parts.join(' + ')
|
|
357
|
-
// 32.61 — THE VALUE
|
|
358
|
-
//
|
|
359
|
-
if (
|
|
360
|
-
within !== undefined &&
|
|
361
|
-
node.lowerTag === 'textarea' &&
|
|
362
|
-
node.children.length === 0
|
|
363
|
-
)
|
|
357
|
+
// 32.61 — THE VALUE AS THE TEXT, led by the newline the parser drops after `<textarea>` so a
|
|
358
|
+
// value opening on one keeps it. The author wrote none: 32.72 refuses it beside the bind.
|
|
359
|
+
if (within !== undefined && node.lowerTag === 'textarea')
|
|
364
360
|
return [
|
|
365
361
|
...parts,
|
|
366
362
|
`'\\n' + ${lowering.use('escapeAttribute')}(${within})`,
|
|
@@ -614,7 +610,17 @@ function boundMarkup(
|
|
|
614
610
|
return `' value="' + ${lowering.use('escapeAttribute')}(${read}) + '"'`
|
|
615
611
|
const on =
|
|
616
612
|
written.bind === 'group'
|
|
617
|
-
? `${lowering.use('groupHolds')}(${read}, ${
|
|
613
|
+
? `${lowering.use('groupHolds')}(${read}, ${
|
|
614
|
+
written.own === undefined
|
|
615
|
+
? "'on'"
|
|
616
|
+
: written.own.expression
|
|
617
|
+
? unlandable(
|
|
618
|
+
lowering,
|
|
619
|
+
written.own,
|
|
620
|
+
lowering.expression(written.own),
|
|
621
|
+
)
|
|
622
|
+
: JSON.stringify(written.own.value)
|
|
623
|
+
})`
|
|
618
624
|
: read
|
|
619
625
|
return `(${on} ? ' ${written.reflects}' : '')`
|
|
620
626
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
|
|
2
2
|
// server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
|
|
3
|
-
export const BRIDGE_VERSION = '0.3.
|
|
3
|
+
export const BRIDGE_VERSION = '0.3.3'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a connection `App.listen` or `abide dev` binds may go without a byte while nothing over it
|
|
3
|
+
* is being answered (16.56): a body still arriving, or a kept-alive connection between requests.
|
|
4
|
+
* `Bun.serve`'s own default, named so the bound a request lifts can be put back.
|
|
5
|
+
*/
|
|
6
|
+
export const IDLE_TIMEOUT_SECONDS = 10
|
|
@@ -8,15 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
ABIDE_ADDRESS,
|
|
11
|
+
APP_MANIFEST_ADDRESS,
|
|
11
12
|
BRIDGE_ADDRESS,
|
|
12
13
|
CLIENT_ADDRESS,
|
|
13
14
|
HEALTH_ADDRESS,
|
|
14
15
|
MCP_ADDRESS,
|
|
15
16
|
OPENAPI_ADDRESS,
|
|
16
17
|
PRIVATE_HANDLERS_ADDRESS,
|
|
18
|
+
WORKER_ADDRESS,
|
|
17
19
|
} from '#shared/ADDRESSES.ts'
|
|
18
20
|
import { APP_META } from '#shared/APP_META.ts'
|
|
19
21
|
import { DEBUG_META } from '#shared/DEBUG_META.ts'
|
|
22
|
+
import { ICON_ADDRESSES } from '#shared/LAUNCHER_FILES.ts'
|
|
23
|
+
import { OFFLINE_META } from '#shared/OFFLINE_META.ts'
|
|
20
24
|
import { decodedOrKept } from '#shared/decodedOrKept.ts'
|
|
21
25
|
import { ERROR_META } from '#shared/ERROR_META.ts'
|
|
22
26
|
import { escapeAttribute } from '#shared/escapeText.ts'
|
|
@@ -49,7 +53,10 @@ import {
|
|
|
49
53
|
import { answerBlobTagged, answerValue, contentEtag } from './answerValue.ts'
|
|
50
54
|
import { answerBridge } from './bridgeRelay.ts'
|
|
51
55
|
import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
|
|
56
|
+
import { appManifest } from './appManifest.ts'
|
|
57
|
+
import { answerWorker } from './answerWorker.ts'
|
|
52
58
|
import { callerOrMint } from './callerOf.ts'
|
|
59
|
+
import { settingOn } from './config.ts'
|
|
53
60
|
import { compressAnswer } from './compressAnswer.ts'
|
|
54
61
|
import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
|
|
55
62
|
import { noteCallInRender } from './deferredBuilds.ts'
|
|
@@ -72,6 +79,7 @@ import {
|
|
|
72
79
|
adoptRequest,
|
|
73
80
|
beginRender,
|
|
74
81
|
currentRequestScope,
|
|
82
|
+
type PageRender,
|
|
75
83
|
type RequestScope,
|
|
76
84
|
within,
|
|
77
85
|
writtenStatus,
|
|
@@ -162,6 +170,9 @@ export type AppSources = {
|
|
|
162
170
|
explained: ((thrown: unknown) => Response) | undefined
|
|
163
171
|
}
|
|
164
172
|
|
|
173
|
+
// 41.29 — the seed sink of a render no page hydrates: every call is left to run when it is made.
|
|
174
|
+
const SEEDS_NOTHING: PageRender['seed'] = () => false
|
|
175
|
+
|
|
165
176
|
/** The `Response` to one request: a handler, a surface of abide's own, a file, or a page. */
|
|
166
177
|
export async function answerRequest(
|
|
167
178
|
app: AppSources,
|
|
@@ -338,7 +349,10 @@ async function answerMatched(
|
|
|
338
349
|
arrive(at.href, page.pattern, params)
|
|
339
350
|
// 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
|
|
340
351
|
// and 41.13, every call it makes is left for the browser's own call to be answered from.
|
|
341
|
-
|
|
352
|
+
// 41.29 — BUT NOT A PAGE A SCRIPT FETCHED, which nothing hydrates: its entries would answer the
|
|
353
|
+
// next call this caller makes with what the render saw, a sign-in since included.
|
|
354
|
+
const hydrated = request.headers.get('sec-fetch-dest') !== 'empty'
|
|
355
|
+
beginRender({ seed: hydrated ? seedFor(caller) : SEEDS_NOTHING, calls: [] })
|
|
342
356
|
// 41.1, 35.2 — THE DOCUMENT IS THE BUILT PAGE, read whole before there is a `Response` to commit a
|
|
343
357
|
// status to: a refusal the page read, or a throw, is answered by the nearest error page at its own
|
|
344
358
|
// status rather than as a 200 the stream then abandons.
|
|
@@ -457,6 +471,11 @@ type ClientLane = {
|
|
|
457
471
|
manifest: Manifest
|
|
458
472
|
framework: AppSources['framework']
|
|
459
473
|
head: string
|
|
474
|
+
/**
|
|
475
|
+
* 45.1, 45.9 — whether the worker saves `manifest`'s bundle and the pages register it: `abide
|
|
476
|
+
* build`'s, unless `ABIDE_OFFLINE` is off. Where not, the worker address answers the removing one.
|
|
477
|
+
*/
|
|
478
|
+
offline: boolean
|
|
460
479
|
}
|
|
461
480
|
|
|
462
481
|
/**
|
|
@@ -476,10 +495,19 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
476
495
|
])
|
|
477
496
|
// 35.6 — every page is served in abide's own document; what it carries beyond that, the root
|
|
478
497
|
// `layout.abide` writes (35.15).
|
|
498
|
+
// 45.15 — installable where the pages are kept offline and the app has an icon to install by.
|
|
499
|
+
const icon = published.get(ICON_ADDRESSES.png)
|
|
500
|
+
const install =
|
|
501
|
+
icon !== undefined && settingOn('ABIDE_OFFLINE', true)
|
|
502
|
+
? await appManifest(icon.path)
|
|
503
|
+
: undefined
|
|
479
504
|
const documentOf = (modules: string[]): string =>
|
|
480
505
|
intoHead(
|
|
481
506
|
DEFAULT_DOCUMENT,
|
|
482
|
-
clientTags(client.manifest, modules) +
|
|
507
|
+
clientTags(client.manifest, modules) +
|
|
508
|
+
(client.offline ? `<meta name="${OFFLINE_META}">` : '') +
|
|
509
|
+
(install?.tags ?? '') +
|
|
510
|
+
client.head,
|
|
483
511
|
)
|
|
484
512
|
const pages: Omit<Ranked, 'segments'>[] = []
|
|
485
513
|
for (const route of parts.table.routes)
|
|
@@ -517,7 +545,20 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
|
|
|
517
545
|
errors,
|
|
518
546
|
// 35.18 — marked, so the browser hydrates no page over what an error page painted.
|
|
519
547
|
errorDocument: intoHead(documentOf([]), `<meta name="${ERROR_META}">`),
|
|
520
|
-
framework:
|
|
548
|
+
framework: (pathname, request) => {
|
|
549
|
+
if (pathname === WORKER_ADDRESS)
|
|
550
|
+
return answerWorker(
|
|
551
|
+
client.offline ? client.manifest : undefined,
|
|
552
|
+
)
|
|
553
|
+
if (pathname === APP_MANIFEST_ADDRESS && install !== undefined)
|
|
554
|
+
return new Response(install.body, {
|
|
555
|
+
headers: {
|
|
556
|
+
'content-type': 'application/manifest+json',
|
|
557
|
+
'cache-control': 'no-cache',
|
|
558
|
+
},
|
|
559
|
+
})
|
|
560
|
+
return client.framework(pathname, request)
|
|
561
|
+
},
|
|
521
562
|
published,
|
|
522
563
|
explained: parts.explained,
|
|
523
564
|
...mounted,
|
package/src/server/answerRpc.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// args off the request, and the answer onto a response.
|
|
4
4
|
//
|
|
5
5
|
// WIRE-ONLY, per 16.13: the `Origin` gate on a mutation, `maxBodySize`, the `crossOrigin` preflight,
|
|
6
|
-
//
|
|
6
|
+
// and 19.7's coercion of the text a URL carried.
|
|
7
7
|
|
|
8
8
|
import type { CopyOptions } from '#shared/CopyOptions.ts'
|
|
9
9
|
import { type Failed, isFailed } from '#shared/Failed.ts'
|
|
@@ -47,9 +47,9 @@ import { type AppRung, failureOf, rungsOver } from './hooks.ts'
|
|
|
47
47
|
import { prefers } from './prefers.ts'
|
|
48
48
|
import {
|
|
49
49
|
currentRequestScope,
|
|
50
|
-
LISTENING,
|
|
51
50
|
rungsTook,
|
|
52
51
|
timed,
|
|
52
|
+
unreap,
|
|
53
53
|
writtenStatus,
|
|
54
54
|
} from './requestScope.ts'
|
|
55
55
|
import { type Outcome, outcomeOf, takeSeed } from './seedBuffer.ts'
|
|
@@ -265,9 +265,6 @@ export async function answerRpc(
|
|
|
265
265
|
if (isFailed(refused)) return answered(refused)
|
|
266
266
|
throw refused
|
|
267
267
|
}
|
|
268
|
-
// 16.56 — where neither names one, nothing is raised and the server's own idle timeout stands.
|
|
269
|
-
const timeout = handler.timeout ?? RPC_SERVER.timeout()
|
|
270
|
-
if (timeout !== undefined) raiseTimeout(timeout)
|
|
271
268
|
// 41.7 — the call a hydrating page makes for what its render already reached is answered from
|
|
272
269
|
// that, and the handler is not run again (41.13).
|
|
273
270
|
// 16.51 — a `HEAD` leaves the seed for the `GET` it stands in front of.
|
|
@@ -499,23 +496,25 @@ function withParameters(
|
|
|
499
496
|
}
|
|
500
497
|
|
|
501
498
|
/**
|
|
502
|
-
* The request's body under the ceiling `maxBodySize` sets, or
|
|
503
|
-
* is unset, or a 413 refusal past it
|
|
499
|
+
* The request's body, received whole, under the ceiling `maxBodySize` sets, or
|
|
500
|
+
* `ABIDE_MAX_REQUEST_BODY_SIZE` where it is unset, or a 413 refusal past it.
|
|
504
501
|
*
|
|
505
502
|
* 16.54 — NO CEILING BY DEFAULT. 16.55 — read off the declared length before a byte of the body is.
|
|
506
503
|
* 16.78 — EVERY BODY ABIDE READS, a handler's and `/__abide/mcp`'s and `/__abide/bridge`'s alike.
|
|
504
|
+
* 16.23 — and the request received whole once it is read, which is where its idle timeout goes off.
|
|
507
505
|
*/
|
|
508
506
|
export async function bodyWithin(
|
|
509
507
|
request: Request,
|
|
510
508
|
maxBodySize: number | undefined,
|
|
511
|
-
): Promise<
|
|
509
|
+
): Promise<Response | Failed<'HttpError', undefined>> {
|
|
512
510
|
const ceiling =
|
|
513
511
|
maxBodySize ??
|
|
514
512
|
positiveSetting('ABIDE_MAX_REQUEST_BODY_SIZE', Number.POSITIVE_INFINITY)
|
|
515
|
-
if (!Number.isFinite(ceiling)) return request
|
|
516
513
|
if (Number(request.headers.get('content-length') ?? 0) > ceiling)
|
|
517
514
|
return overCeiling(ceiling)
|
|
518
|
-
|
|
515
|
+
const body = await bodyUnder(request, ceiling)
|
|
516
|
+
if (!isFailed(body)) unreap(currentRequestScope())
|
|
517
|
+
return body
|
|
519
518
|
}
|
|
520
519
|
|
|
521
520
|
/**
|
|
@@ -530,6 +529,11 @@ async function bodyUnder(
|
|
|
530
529
|
request: Request,
|
|
531
530
|
ceiling: number,
|
|
532
531
|
): Promise<Response | Failed<'HttpError', undefined>> {
|
|
532
|
+
if (!Number.isFinite(ceiling))
|
|
533
|
+
// A blob, which a `Response` shares where it copies an `ArrayBuffer`.
|
|
534
|
+
return new Response(await request.blob(), {
|
|
535
|
+
headers: request.headers,
|
|
536
|
+
})
|
|
533
537
|
const chunks: Uint8Array[] = []
|
|
534
538
|
let size = 0
|
|
535
539
|
if (request.body !== null)
|
|
@@ -560,37 +564,6 @@ function preflight(found: Mounted, origin: string, request: Request): Response {
|
|
|
560
564
|
return new Response(null, { status: 204, headers })
|
|
561
565
|
}
|
|
562
566
|
|
|
563
|
-
/**
|
|
564
|
-
* Raises the request's idle timeout to cover `milliseconds`, never lowering it.
|
|
565
|
-
*
|
|
566
|
-
* 16.22, 16.23 — the floor is the longest any handler in the request asked for. Where no socket is
|
|
567
|
-
* bound there is no idle timeout to raise.
|
|
568
|
-
*
|
|
569
|
-
* 16.57 — `Infinity` is never reaped, which bun spells 0; any finite bound is capped at the 255
|
|
570
|
-
* seconds bun's idle timeout can hold.
|
|
571
|
-
*
|
|
572
|
-
* FOUR SECONDS PAST THE TIMEOUT, NOT AT IT: a load that goes its timeout is refused at that moment,
|
|
573
|
-
* and a stream's refusal is a frame written then (16.68). bun's sockets check an idle timeout on a
|
|
574
|
-
* tick four seconds wide, so an idle timeout of the same second — or one past it — fired anywhere in
|
|
575
|
-
* the tick before it and reaped the connection ahead of the frame, one stream in two under `abide
|
|
576
|
-
* dev`. Four is the tick, so the refusal always goes out first.
|
|
577
|
-
*
|
|
578
|
-
* ON THE REQUEST THE LISTENER RECEIVED, never the one being answered: under a mount, or past a
|
|
579
|
-
* rung that handed `next` its own, that is a copy, and bun's `timeout` does nothing for a request
|
|
580
|
-
* it was not handed itself.
|
|
581
|
-
*/
|
|
582
|
-
export function raiseTimeout(milliseconds: number): void {
|
|
583
|
-
const scope = currentRequestScope()
|
|
584
|
-
const received = scope?.received
|
|
585
|
-
if (scope === undefined || received === undefined) return
|
|
586
|
-
const seconds = Number.isFinite(milliseconds)
|
|
587
|
-
? Math.min(255, Math.ceil(milliseconds / 1000) + 4)
|
|
588
|
-
: Number.POSITIVE_INFINITY
|
|
589
|
-
if (seconds <= scope.timeout) return
|
|
590
|
-
scope.timeout = seconds
|
|
591
|
-
LISTENING.timeout(received, Number.isFinite(seconds) ? seconds : 0)
|
|
592
|
-
}
|
|
593
|
-
|
|
594
567
|
function isForm(request: Request): boolean {
|
|
595
568
|
const type = request.headers.get('content-type') ?? ''
|
|
596
569
|
return (
|
|
@@ -315,7 +315,7 @@ export class MuxConnection {
|
|
|
315
315
|
upgrade.headers.get('origin'),
|
|
316
316
|
upgrade.url,
|
|
317
317
|
)
|
|
318
|
-
// NO `received`: an upgraded connection has no http idle timeout for 16.
|
|
318
|
+
// NO `received`: an upgraded connection has no http idle timeout for 16.23 to set.
|
|
319
319
|
this.scope = scopeFor(
|
|
320
320
|
new Request(upgrade, { signal: this.hangUp.signal }),
|
|
321
321
|
undefined,
|
|
@@ -562,7 +562,7 @@ export function followAsEvents(
|
|
|
562
562
|
let ended = false
|
|
563
563
|
return new ReadableStream({
|
|
564
564
|
start(controller) {
|
|
565
|
-
controller.enqueue(
|
|
565
|
+
controller.enqueue(EVENT_STREAM_OPENER)
|
|
566
566
|
// Ended by the app's stop as any body it holds open is, or that stop waits on it.
|
|
567
567
|
unhold = heldByApp(() => {
|
|
568
568
|
if (ended) return
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './requestScope.ts'
|
|
15
15
|
import {
|
|
16
16
|
EVENT_STREAM_OPENER,
|
|
17
|
+
heldBody,
|
|
17
18
|
refusalFrame,
|
|
18
19
|
streamEvent,
|
|
19
20
|
} from './streamEvent.ts'
|
|
@@ -38,7 +39,7 @@ export async function answerValue(
|
|
|
38
39
|
headers: Headers,
|
|
39
40
|
status: number,
|
|
40
41
|
): Promise<Response> {
|
|
41
|
-
if (value instanceof Response) return value
|
|
42
|
+
if (value instanceof Response) return heldUntilSent(value)
|
|
42
43
|
const framing = currentRequestScope()?.framing
|
|
43
44
|
// 17.9, 17.10 — what `json` fixed: the standing value, whatever it is, and `null` for nothing.
|
|
44
45
|
if (framing === 'json') {
|
|
@@ -223,8 +224,7 @@ export function bodyOf(
|
|
|
223
224
|
const unhold = heldByApp(() => stopped())
|
|
224
225
|
return new ReadableStream<Uint8Array>({
|
|
225
226
|
start(controller) {
|
|
226
|
-
if (framing === 'sse')
|
|
227
|
-
controller.enqueue(ENCODER.encode(EVENT_STREAM_OPENER))
|
|
227
|
+
if (framing === 'sse') controller.enqueue(EVENT_STREAM_OPENER)
|
|
228
228
|
stopped = () => {
|
|
229
229
|
if (ended) return
|
|
230
230
|
release()
|
|
@@ -290,6 +290,18 @@ export function bodyOf(
|
|
|
290
290
|
})
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/**
|
|
294
|
+
* `answer`, holding its request's scope until its body has been read to the end.
|
|
295
|
+
*
|
|
296
|
+
* 12.5, 16.23 — A `Response` A HANDLER BUILT MAY BE A STREAM, and nothing tells a stream from a
|
|
297
|
+
* string or a blob once it is one: `body` is a `ReadableStream` either way. So every body is held,
|
|
298
|
+
* at the price of a buffered one going out chunked rather than under a `Content-Length`.
|
|
299
|
+
*/
|
|
300
|
+
function heldUntilSent(answer: Response): Response {
|
|
301
|
+
if (answer.body === null) return answer
|
|
302
|
+
return new Response(heldBody(answer.body, holdScope(), false), answer)
|
|
303
|
+
}
|
|
304
|
+
|
|
293
305
|
/**
|
|
294
306
|
* A blob answered at its own type under the given etag, range-aware.
|
|
295
307
|
*
|