@abide/abide 0.104.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.
- package/CHANGELOG.md +16 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +49 -71
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -123
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -33
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
import { outsideScope, scoped } from './scoped.ts'
|
|
34
34
|
import { share } from './share.ts'
|
|
35
35
|
|
|
36
|
-
export { chunksOfStream } from './chunksOfStream.ts'
|
|
37
36
|
export { isReactive } from './isReactive.ts'
|
|
38
37
|
export { isKeyed, type Keyed } from './Keyed.ts'
|
|
39
38
|
export { NotLanded } from './NotLanded.ts'
|
|
@@ -42,6 +41,7 @@ export { NotLanded } from './NotLanded.ts'
|
|
|
42
41
|
// first that imports the core directly meets `MEMBER_KEYS` before it is initialized.
|
|
43
42
|
export {
|
|
44
43
|
abandonLoad,
|
|
44
|
+
chunksOfStream,
|
|
45
45
|
deadlineOf,
|
|
46
46
|
failedTransiently,
|
|
47
47
|
flushSync,
|
|
@@ -368,14 +368,17 @@ export function reactive(
|
|
|
368
368
|
): unknown {
|
|
369
369
|
const keyed = isKeyedBody(given[0])
|
|
370
370
|
// 11.32 — ON A SERVER, OUTSIDE A REQUEST, THE VALUE IS FILED: a forwarder to one instance per
|
|
371
|
-
// request, and to the one built here where there is no request at all
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
// request, and to the one built here where there is no request at all — or, for a value with a
|
|
372
|
+
// `store`, built where it is first reached (11.71). `global` is what keeps a value process-wide
|
|
373
|
+
// (11.34).
|
|
374
|
+
const options = given[1] as
|
|
375
|
+
| { global?: boolean; store?: unknown }
|
|
376
|
+
| undefined
|
|
377
|
+
if (outsideScope(options?.global === true))
|
|
378
|
+
return scoped(
|
|
379
|
+
() => build(given, keyed),
|
|
380
|
+
!keyed && options?.store !== undefined,
|
|
376
381
|
)
|
|
377
|
-
)
|
|
378
|
-
return scoped(() => build(given, keyed))
|
|
379
382
|
return build(given, keyed)
|
|
380
383
|
}
|
|
381
384
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the pair's, and nothing stands here — the value moves when what `get` reads does, as an element's
|
|
5
5
|
// `bind:value={{get, set}}` does.
|
|
6
6
|
//
|
|
7
|
-
// A PROTOTYPE, NOT A BRANCH in the core's `set`, for `brandedPrototype`'s reason: `
|
|
7
|
+
// A PROTOTYPE, NOT A BRANCH in the core's `set`, for `brandedPrototype`'s reason: `seat`
|
|
8
8
|
// leaves a seat it does not own alone, and the adoption path reaches this `set` through the
|
|
9
9
|
// callable it adopted.
|
|
10
10
|
|
|
@@ -24,9 +24,10 @@ const UNBUILT: unique symbol = Symbol('abide.unbuilt')
|
|
|
24
24
|
/**
|
|
25
25
|
* What a forwarder resolves through: the home, and how to build the same value again in a scope.
|
|
26
26
|
*
|
|
27
|
-
* 16.64 — A HANDLER'S HOME
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* 11.71 AND 16.64 — A HANDLER'S HOME, AND AN UNKEYED VALUE'S WITH A `store`, IS BUILT WHERE IT IS
|
|
28
|
+
* FIRST REACHED, and every other value's where it is declared. Building a value starts its body
|
|
29
|
+
* (1.7) and reads its store (8.14), and at import that ran a handler for a request nobody had made,
|
|
30
|
+
* and a store reading `cookies()` where there is no request.
|
|
30
31
|
*
|
|
31
32
|
* A `global` handler (11.34) is filed only to be built late: every request reaches the home, built
|
|
32
33
|
* outside each of them, so it is the one value its option says it is.
|
|
@@ -16,6 +16,12 @@ import type { Reactive } from './Reactive.ts'
|
|
|
16
16
|
export type ShareScope = {
|
|
17
17
|
parent: ShareScope | null
|
|
18
18
|
values: Map<string, unknown>
|
|
19
|
+
/**
|
|
20
|
+
* 39.1 — the routed level the instance stands in, opaque here (`Level`). Carried down the chain
|
|
21
|
+
* so a component mounted after its level — an arm opening, a row grown — finds the level its
|
|
22
|
+
* `<head>` joins through the scope it resumes, as it finds a shared key.
|
|
23
|
+
*/
|
|
24
|
+
level: unknown
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
// The instance being rendered or mounted, or `null` outside one. A dynamic scope like
|
|
@@ -62,7 +68,11 @@ export function share<Key extends string, Value>(
|
|
|
62
68
|
*/
|
|
63
69
|
export function enterShare(): ShareScope | null {
|
|
64
70
|
const previous = CURRENT
|
|
65
|
-
CURRENT = {
|
|
71
|
+
CURRENT = {
|
|
72
|
+
parent: previous,
|
|
73
|
+
values: new Map(),
|
|
74
|
+
level: previous === null ? undefined : previous.level,
|
|
75
|
+
}
|
|
66
76
|
return previous
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -10,13 +10,6 @@
|
|
|
10
10
|
// A LEAF: nothing imported but another leaf, because the compiler, the client router and a server
|
|
11
11
|
// router all read it and a constant crossing a seam lives where nothing pulls a graph in behind it.
|
|
12
12
|
|
|
13
|
-
import { LAUNCHER_PAGE } from './LAUNCHER_FILES.ts'
|
|
14
|
-
import {
|
|
15
|
-
HOLDS_SERVER,
|
|
16
|
-
RESERVED_DIRECTORIES,
|
|
17
|
-
SERVER_DIRECTORY,
|
|
18
|
-
} from './RESERVED_DIRECTORIES.ts'
|
|
19
|
-
|
|
20
13
|
type SegmentKind = 'static' | 'required' | 'optional' | 'rest'
|
|
21
14
|
|
|
22
15
|
/** One `/`-separated piece of a route literal, classified by kind. */
|
|
@@ -47,122 +40,6 @@ export function parseRoutePattern(pattern: string): Segment[] {
|
|
|
47
40
|
return out
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
/** Whether a source is a page, a layout, an error page or a component. */
|
|
51
|
-
export type FileKind = 'page' | 'layout' | 'error' | 'component'
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Whether the `.abide` at `path` is a page, a layout, an error page or a component.
|
|
55
|
-
*
|
|
56
|
-
* 23.21 — WHERE IT SITS, THEN ITS NAME. Every `.abide` directly in an address directory is a page
|
|
57
|
-
* but a `layout.abide`, an `error.abide` and the root's `launcher.abide`; one under a fence is a
|
|
58
|
-
* component whatever it is called.
|
|
59
|
-
* One test for every reader: the build's routes, the checker's `Routes` and the `route.params` a
|
|
60
|
-
* page is typed by.
|
|
61
|
-
*
|
|
62
|
-
* `path` is RELATIVE TO THE APP'S ROOT: its directories are its route, so an absolute path would
|
|
63
|
-
* read the machine's directories as the app's.
|
|
64
|
-
*/
|
|
65
|
-
export function fileKind(path: string): FileKind {
|
|
66
|
-
if (placeOf(path) !== 'address' || path === LAUNCHER_PAGE)
|
|
67
|
-
return 'component'
|
|
68
|
-
const name = path.slice(path.lastIndexOf('/') + 1)
|
|
69
|
-
if (name === 'layout.abide') return 'layout'
|
|
70
|
-
if (name === 'error.abide') return 'error'
|
|
71
|
-
return 'page'
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Where a file sits in an app, which is what it is to every reader (`placeOf`). */
|
|
75
|
-
export type Place =
|
|
76
|
-
| 'address'
|
|
77
|
-
| 'server'
|
|
78
|
-
| 'public'
|
|
79
|
-
| 'lib'
|
|
80
|
-
| 'tests'
|
|
81
|
-
| 'dependency'
|
|
82
|
-
| 'skipped'
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Where the file at `path`, relative to the app's root, sits — THE ONE ANSWER every walk, fence and
|
|
86
|
-
* emit reads, so no two of them disagree about a path.
|
|
87
|
-
*
|
|
88
|
-
* - `dependency` — 34.24: a package's, outside the root or under `node_modules`, whose directories
|
|
89
|
-
* are its package's layout and none of the app's;
|
|
90
|
-
* - `skipped` — under a dot directory, which no walk of the app enters;
|
|
91
|
-
* - `server`, `public`, `lib` or `tests` — 23.15: under that fence, the nearest to the root deciding,
|
|
92
|
-
* save a `server` under a `lib` or a `tests`, which is `server` — the fence the browser's bundle
|
|
93
|
-
* is checked against (16.29) is never hidden by one above it (23.16);
|
|
94
|
-
* - `address` — 23.14: everywhere else, its directories the segments of its route.
|
|
95
|
-
*/
|
|
96
|
-
export function placeOf(path: string): Place {
|
|
97
|
-
if (path.startsWith('../') || path.startsWith('/')) return 'dependency'
|
|
98
|
-
const parts = path.split('/')
|
|
99
|
-
// A `lib` or a `tests` met first, which a `server` below it still overrides.
|
|
100
|
-
let fence: Place | undefined
|
|
101
|
-
// The last part is the file itself, which is never a directory.
|
|
102
|
-
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
103
|
-
const part = parts[index] as string
|
|
104
|
-
if (skipped(part))
|
|
105
|
-
return (
|
|
106
|
-
fence ?? (part === 'node_modules' ? 'dependency' : 'skipped')
|
|
107
|
-
)
|
|
108
|
-
if (fence !== undefined) {
|
|
109
|
-
if (part === SERVER_DIRECTORY) return 'server'
|
|
110
|
-
} else if (RESERVED_DIRECTORIES.has(part)) {
|
|
111
|
-
if (!HOLDS_SERVER.has(part)) return part as Place
|
|
112
|
-
fence = part as Place
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return fence ?? 'address'
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Whether a walk of the app passes over the entry `name`: a dot file or directory, or `node_modules`. */
|
|
119
|
-
export function skipped(name: string): boolean {
|
|
120
|
-
return name.startsWith('.') || name === 'node_modules'
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* The route a file answers at — its path below the root without its extension.
|
|
125
|
-
*
|
|
126
|
-
* 23.14 — `media/[id].abide` and `media/getGrid.ts` are `/media/[id]` and `/media/getGrid`; an
|
|
127
|
-
* `index.abide`, a `layout.abide` and an `error.abide` are their directory's. `path` is relative to
|
|
128
|
-
* the app's root, as `fileKind`'s is.
|
|
129
|
-
*/
|
|
130
|
-
export function routeOf(path: string): string {
|
|
131
|
-
const parts = path.split('/')
|
|
132
|
-
const file = parts.pop() as string
|
|
133
|
-
// The extension alone: `[...rest].abide` carries dots of its own.
|
|
134
|
-
if (!DIRECTORY_FILES.has(file)) parts.push(file.replace(EXTENSION, ''))
|
|
135
|
-
return `/${parts.filter((part) => part !== '').join('/')}`
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const EXTENSION = /\.(?:abide|ts)$/
|
|
139
|
-
|
|
140
|
-
/** 23.14, 23.18 — the names whose route is their directory's, and so a route's wherever they stand. */
|
|
141
|
-
export const DIRECTORY_FILES = new Set([
|
|
142
|
-
'index.abide',
|
|
143
|
-
'layout.abide',
|
|
144
|
-
'error.abide',
|
|
145
|
-
])
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The route the hooks and private handlers of the file at `path` are filed at.
|
|
149
|
-
*
|
|
150
|
-
* 37.1 — an address's own route; under a `lib` or a `tests` directory, the route of the directory
|
|
151
|
-
* holding it, so a component an admin page renders runs under the admin route's rungs; under a
|
|
152
|
-
* `server` or a `public` directory, and for a dependency's file, the root's.
|
|
153
|
-
*/
|
|
154
|
-
export function scopeOf(path: string): string {
|
|
155
|
-
// 38.82 — the launcher answers at no route, and files what it calls at the root's.
|
|
156
|
-
if (path === LAUNCHER_PAGE) return '/'
|
|
157
|
-
const place = placeOf(path)
|
|
158
|
-
if (place === 'address') return routeOf(path)
|
|
159
|
-
if (place !== 'lib' && place !== 'tests') return '/'
|
|
160
|
-
const parts = path.split('/')
|
|
161
|
-
// The fence `placeOf` named is the first one, and its name is its place.
|
|
162
|
-
const fence = parts.indexOf(place)
|
|
163
|
-
return `/${parts.slice(0, fence).join('/')}`
|
|
164
|
-
}
|
|
165
|
-
|
|
166
43
|
/**
|
|
167
44
|
* A `sort` comparator over two parsed patterns: negative where `a` is the more specific.
|
|
168
45
|
*
|
package/src/shared/rpc.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// handler's own.
|
|
15
15
|
|
|
16
16
|
import { type Failed, isFailedNamed } from './Failed.ts'
|
|
17
|
-
import { filledSegments } from './filledPath.ts'
|
|
18
17
|
import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
|
|
18
|
+
import { HandlerRoute } from './HandlerRoute.ts'
|
|
19
19
|
import { compose, type Middleware } from './Middleware.ts'
|
|
20
20
|
import { principalRotated } from './principalRotated.ts'
|
|
21
21
|
import { RPC_SERVER } from './RPC_SERVER.ts'
|
|
@@ -49,8 +49,7 @@ import { MUTATION } from './reactive/MUTATION.ts'
|
|
|
49
49
|
import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
|
|
50
50
|
import { isForwarder } from './reactive/scoped.ts'
|
|
51
51
|
import { checkArgs } from './refuse.ts'
|
|
52
|
-
import {
|
|
53
|
-
import { pathRefusal, rpcRequest } from './rpcRequest.ts'
|
|
52
|
+
import { rpcRequest } from './rpcRequest.ts'
|
|
54
53
|
import type { Schema } from './Schema.ts'
|
|
55
54
|
|
|
56
55
|
export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
@@ -374,28 +373,22 @@ export function declare(method: Method): Declare {
|
|
|
374
373
|
//
|
|
375
374
|
// 15.8 — AND REFUSED AHEAD OF THE HANDLER where the args leave a parameter of its route no
|
|
376
375
|
// spelling (30.3, 30.19), as the wire refuses them: a render and the browser's call then
|
|
377
|
-
// reach the same refusal, and there is no request for the render to report.
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
376
|
+
// reach the same refusal, and there is no request for the render to report.
|
|
377
|
+
//
|
|
378
|
+
// THE ROUTE IS COMPILED ONCE PER ADDRESS, which is stamped after the declaration (16.10),
|
|
379
|
+
// and every call, `rpc.url` and `rpc.raw` read it.
|
|
380
|
+
let route: HandlerRoute | undefined
|
|
381
|
+
const routeAt = (address: string): HandlerRoute => {
|
|
382
|
+
if (route?.address !== address) route = new HandlerRoute(address)
|
|
383
|
+
return route
|
|
384
|
+
}
|
|
381
385
|
const call = (given?: unknown, options?: CallOptions): unknown => {
|
|
382
386
|
const args = normalized(contract, given)
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
routeSegments =
|
|
387
|
-
address?.includes('[') === true
|
|
388
|
-
? parseRoutePattern(address)
|
|
389
|
-
: null
|
|
390
|
-
}
|
|
391
|
-
if (routeSegments !== null) {
|
|
387
|
+
const route =
|
|
388
|
+
rpc.address === undefined ? undefined : routeAt(rpc.address)
|
|
389
|
+
if (route?.segments !== undefined) {
|
|
392
390
|
try {
|
|
393
|
-
|
|
394
|
-
routeSegments,
|
|
395
|
-
(args ?? {}) as Record<string, unknown>,
|
|
396
|
-
routed as string,
|
|
397
|
-
pathRefusal,
|
|
398
|
-
)
|
|
391
|
+
route.path(args as Record<string, unknown> | undefined)
|
|
399
392
|
} catch (refused) {
|
|
400
393
|
return reactive(() => refused)
|
|
401
394
|
}
|
|
@@ -428,13 +421,13 @@ export function declare(method: Method): Declare {
|
|
|
428
421
|
guarded: rungs.length > 0,
|
|
429
422
|
isError: isFailedNamed,
|
|
430
423
|
url(args?: Record<string, unknown>): string {
|
|
431
|
-
return rpcRequest(method, addressOf(rpc), args).url
|
|
424
|
+
return rpcRequest(method, routeAt(addressOf(rpc)), args).url
|
|
432
425
|
},
|
|
433
426
|
raw(
|
|
434
427
|
args?: Record<string, unknown>,
|
|
435
428
|
init?: RequestInit,
|
|
436
429
|
): Promise<Response> {
|
|
437
|
-
const asked = rpcRequest(method, addressOf(rpc), args)
|
|
430
|
+
const asked = rpcRequest(method, routeAt(addressOf(rpc)), args)
|
|
438
431
|
// `Headers`, not a spread — the caller's may be a `Headers` itself, which spreads empty.
|
|
439
432
|
const headers = new Headers(asked.init.headers)
|
|
440
433
|
for (const [name, value] of new Headers(init?.headers))
|
package/src/shared/rpcRequest.ts
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// to the byte, or the browser's call misses the request that was already started for it. `key` is
|
|
4
4
|
// that agreement: method, URL and body, which is what makes two requests the same one.
|
|
5
5
|
//
|
|
6
|
-
// A `GET` or `DELETE` carries its args in the URL, in `queryText`'s spelling, and a mutation
|
|
7
|
-
// body; 19.7 is where text becomes the declared type again. 16.9 — whatever the method, a
|
|
8
|
-
// of the handler's route rides in the path, and the server puts it back among the args
|
|
6
|
+
// A `GET` or `DELETE` carries its args in the URL, in `queryText`'s spelling (30.28), and a mutation
|
|
7
|
+
// in its body; 19.7 is where text becomes the declared type again. 16.9 — whatever the method, a
|
|
8
|
+
// parameter of the handler's route rides in the path, and the server puts it back among the args
|
|
9
|
+
// (16.65).
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import { HandlerRoute } from './HandlerRoute.ts'
|
|
11
12
|
import { queryText } from './queryText.ts'
|
|
12
|
-
import { validationError } from './refuse.ts'
|
|
13
13
|
import type { Method } from './rpc.ts'
|
|
14
14
|
|
|
15
15
|
export type RpcRequest = {
|
|
@@ -31,44 +31,25 @@ export function argsInURL(method: Method): boolean {
|
|
|
31
31
|
return method === 'GET' || method === 'DELETE'
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The request a call is: its URL, body, `fetch` init and the `key` both ends match it by.
|
|
36
|
+
*
|
|
37
|
+
* `route` is the handler's, compiled where the caller holds one; an address is compiled here.
|
|
38
|
+
*/
|
|
35
39
|
export function rpcRequest(
|
|
36
40
|
method: Method,
|
|
37
|
-
|
|
41
|
+
route: HandlerRoute | string,
|
|
38
42
|
given: Record<string, unknown> | undefined,
|
|
39
43
|
): RpcRequest {
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const { path, rest } = (
|
|
45
|
+
typeof route === 'string' ? new HandlerRoute(route) : route
|
|
46
|
+
).split(given)
|
|
47
|
+
let url = path
|
|
42
48
|
let body: string | undefined
|
|
43
|
-
// A route with parameters: they are written into the path and leave the args, so the query or
|
|
44
|
-
// body never carries one twice (16.66). 15.8 — one no address carries (30.3, 30.19) is refused as
|
|
45
|
-
// args a gate refused, so every caller answers it as the wire answers a 422.
|
|
46
|
-
if (address.includes('[')) {
|
|
47
|
-
const params = given ?? {}
|
|
48
|
-
url = filledPath(address, params, address, pathRefusal)
|
|
49
|
-
const inPath = parametersOf(address)
|
|
50
|
-
let rest: Record<string, unknown> | undefined
|
|
51
|
-
for (const name in params) {
|
|
52
|
-
if (inPath.has(name)) continue
|
|
53
|
-
rest ??= {}
|
|
54
|
-
rest[name] = params[name]
|
|
55
|
-
}
|
|
56
|
-
args = rest
|
|
57
|
-
}
|
|
58
49
|
if (argsInURL(method)) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// one-member list `['']` — so 19.7 reads it back as `[]` rather than losing the member.
|
|
63
|
-
for (const name in args) {
|
|
64
|
-
const value = args[name]
|
|
65
|
-
if (!Array.isArray(value) || value.length !== 0) continue
|
|
66
|
-
const bare = bareName(name)
|
|
67
|
-
text = text === '' ? bare : `${text}&${bare}`
|
|
68
|
-
}
|
|
69
|
-
if (text !== '') url = `${url}?${text}`
|
|
70
|
-
}
|
|
71
|
-
} else if (args !== undefined) body = JSON.stringify(args)
|
|
50
|
+
const text = rest === undefined ? '' : queryText(rest)
|
|
51
|
+
if (text !== '') url = `${url}?${text}`
|
|
52
|
+
} else if (rest !== undefined) body = JSON.stringify(rest)
|
|
72
53
|
// 18.14 — A COPY IS ITS OWN CACHE and keeps its own `max-age`. What it asks the server is asked
|
|
73
54
|
// because it went stale or was invalidated, and the browser's HTTP cache, which hears neither,
|
|
74
55
|
// would answer it with the value it was dropping. `rpc.raw` is handed the same, under whatever
|
|
@@ -85,13 +66,3 @@ export function rpcRequest(
|
|
|
85
66
|
}
|
|
86
67
|
return { method, url, body, init, key: `${method} ${url} ${body ?? ''}` }
|
|
87
68
|
}
|
|
88
|
-
|
|
89
|
-
/** 15.8 — a parameter no address carries, refused as args a gate refused. */
|
|
90
|
-
export function pathRefusal(name: string, message: string): unknown {
|
|
91
|
-
return validationError<Record<string, unknown>>({ [name]: [message] })
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** A query member's name in the encoding `URLSearchParams` gives it, without its `=`. */
|
|
95
|
-
export function bareName(name: string): string {
|
|
96
|
-
return new URLSearchParams([[name, '']]).toString().slice(0, -1)
|
|
97
|
-
}
|
|
@@ -5,44 +5,19 @@
|
|
|
5
5
|
import { EARLY_CALLS } from './EARLY_CALLS.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* What
|
|
9
|
-
*
|
|
8
|
+
* What the early-call script leaves per call: the request in flight, and once it has answered, the
|
|
9
|
+
* `Response`. The script decides nothing about it — `answerOf` reads it as it reads any answer.
|
|
10
10
|
*/
|
|
11
|
-
export type
|
|
12
|
-
|
|
13
|
-
statusText: string | undefined
|
|
14
|
-
type: string | null | undefined
|
|
15
|
-
/**
|
|
16
|
-
* The answer's version, or `null` where it named none (18.5).
|
|
17
|
-
*
|
|
18
|
-
* A `null` is a value built per request.
|
|
19
|
-
*/
|
|
20
|
-
version: string | null | undefined
|
|
21
|
-
/** The answer's `cache-control`, whose `max-age` is how long the copy may serve it (21.8). */
|
|
22
|
-
life: string | null | undefined
|
|
23
|
-
text: string | undefined
|
|
24
|
-
/**
|
|
25
|
-
* A binary answer's body read to the end (16.34), set in place of `text` — a body read as text
|
|
26
|
-
* is not the bytes it carried.
|
|
27
|
-
*/
|
|
28
|
-
bytes: Uint8Array | undefined
|
|
29
|
-
/**
|
|
30
|
-
* A streamed answer's body, left unread so its chunks are taken as they land (16.32).
|
|
31
|
-
*
|
|
32
|
-
* Set in place of `text` for a handler the build marked as streaming, and by the early-call
|
|
33
|
-
* script for any framed answer.
|
|
34
|
-
*/
|
|
35
|
-
body: ReadableStream<Uint8Array> | null | undefined
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* What the early-call script leaves per call: the request in flight, and once it has answered, what
|
|
40
|
-
* it answered.
|
|
41
|
-
*/
|
|
42
|
-
export type EarlyCall = Answer & {
|
|
43
|
-
promise: Promise<void>
|
|
11
|
+
export type EarlyCall = {
|
|
12
|
+
promise: Promise<unknown>
|
|
44
13
|
/** `1` where the server built the page's markup from this call settled (41.25). */
|
|
45
14
|
painted: 1 | undefined
|
|
15
|
+
/**
|
|
16
|
+
* Set once the request has answered — and, where `painted`, once its body is read to the end into
|
|
17
|
+
* `bytes`, so hydration's wait covers the body too. Unset where the request failed.
|
|
18
|
+
*/
|
|
19
|
+
response: Response | undefined
|
|
20
|
+
bytes: ArrayBuffer | undefined
|
|
46
21
|
}
|
|
47
22
|
|
|
48
23
|
function startedCalls(): Record<string, EarlyCall | undefined> | undefined {
|
|
@@ -75,10 +50,10 @@ export function takeEarlyCall(key: string): EarlyCall | undefined {
|
|
|
75
50
|
export function paintedCallsLanded(): Promise<unknown> | undefined {
|
|
76
51
|
const started = startedCalls()
|
|
77
52
|
if (started === undefined) return undefined
|
|
78
|
-
let waiting: Promise<
|
|
53
|
+
let waiting: Promise<unknown>[] | undefined
|
|
79
54
|
for (const key in started) {
|
|
80
55
|
const early = started[key]
|
|
81
|
-
if (early?.painted !== 1 || early.
|
|
56
|
+
if (early?.painted !== 1 || early.response !== undefined) continue
|
|
82
57
|
if (waiting === undefined) waiting = []
|
|
83
58
|
waiting.push(early.promise)
|
|
84
59
|
}
|
package/src/shared/url.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function url<P extends RouteLiteral>(
|
|
|
89
89
|
const address = mounted(
|
|
90
90
|
filledPath(pattern ?? '/', params, `url('${pattern}')`),
|
|
91
91
|
)
|
|
92
|
-
// 30.6 — the query half, last
|
|
92
|
+
// 30.6 — the query half, last; 30.28 — in the spelling an `Rpc`'s args are written in.
|
|
93
93
|
const search =
|
|
94
94
|
query === undefined
|
|
95
95
|
? ''
|