@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.
- package/CHANGELOG.md +29 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +27 -27
- package/docs/RULEBOOK.md +68 -24
- package/docs/abide.md +31 -46
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +46 -89
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -25
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +12 -10
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +183 -478
- 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/projectShadow.ts +1 -1
- 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 +152 -146
- 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 +59 -73
- 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/generatedSurfaces.ts +12 -53
- 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/versionOf.ts +1 -2
- 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/RESERVED_DIRECTORIES.ts +6 -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 +69 -124
- package/src/shared/reactive/Reactive.ts +296 -162
- package/src/shared/reactive/callLoad.ts +4 -18
- 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 -110
- 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 -57
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// 38.14, 38.35, 38.36 — ONE RESPONSE, ANSWERED IN ONE PROCESS AND SENT FROM ANOTHER. `abide dev`'s
|
|
2
|
+
// listener holds the connection and its worker holds the app, so a request crosses as a message and
|
|
3
|
+
// its answer comes back as a head and then a chunk per pull. Both halves of that protocol are here —
|
|
4
|
+
// `serveOver` is the worker's, `askOver` the listener's — over a channel neither half knows the kind
|
|
5
|
+
// of, so the protocol is read in one file and runs in one test process (`responseRelay.test.ts`).
|
|
6
|
+
// What `serveDev` and `devWorker` keep is the processes and the sockets.
|
|
7
|
+
|
|
8
|
+
import { plainAnswer } from './plainAnswer.ts'
|
|
9
|
+
|
|
10
|
+
/** One direction of the line between the halves: bun's IPC in `abide dev`, a pair of queues in a test. */
|
|
11
|
+
export type Channel<Message> = { postMessage(message: Message): void }
|
|
12
|
+
|
|
13
|
+
/** A request as it crosses to the worker: its id, and what the worker rebuilds it from. */
|
|
14
|
+
export type Ask = {
|
|
15
|
+
id: number
|
|
16
|
+
method: string
|
|
17
|
+
url: string
|
|
18
|
+
headers: [string, string][]
|
|
19
|
+
body: ArrayBuffer | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** What the listener's half sends: a request, and a pull or cancel of its body. */
|
|
23
|
+
export type ToRelay =
|
|
24
|
+
| ({ kind: 'request' } & Ask)
|
|
25
|
+
| { kind: 'pull'; id: number }
|
|
26
|
+
| { kind: 'cancel'; id: number }
|
|
27
|
+
|
|
28
|
+
/** What the worker's half sends: a request's idle timeout raised, and its head, each chunk, and its end. */
|
|
29
|
+
export type FromRelay =
|
|
30
|
+
| { kind: 'timeout'; id: number; seconds: number }
|
|
31
|
+
| {
|
|
32
|
+
kind: 'head'
|
|
33
|
+
id: number
|
|
34
|
+
status: number
|
|
35
|
+
statusText: string
|
|
36
|
+
headers: [string, string][]
|
|
37
|
+
empty: boolean
|
|
38
|
+
/** 37.3 — whether the listener upgrades this request to the mux (`admitsMux`). */
|
|
39
|
+
admitted: boolean
|
|
40
|
+
/** A body whose length the answer declared, sent with its head rather than pulled. */
|
|
41
|
+
whole: ArrayBuffer | undefined
|
|
42
|
+
}
|
|
43
|
+
| { kind: 'chunk'; id: number; bytes: ArrayBuffer }
|
|
44
|
+
| { kind: 'end'; id: number }
|
|
45
|
+
| { kind: 'fail'; id: number; message: string }
|
|
46
|
+
|
|
47
|
+
// A status whose response has no body to send, which `Response` refuses to be built with one.
|
|
48
|
+
const BODILESS = new Set([101, 204, 205, 304])
|
|
49
|
+
|
|
50
|
+
/** A throw, as the text a reader is shown: its stack where it has one. */
|
|
51
|
+
export function describe(thrown: unknown): string {
|
|
52
|
+
return thrown instanceof Error
|
|
53
|
+
? (thrown.stack ?? thrown.message)
|
|
54
|
+
: String(thrown)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The worker's half: answers each request the listener sends with `fetch`, and its body a chunk per
|
|
59
|
+
* pull. `take` is handed every `ToRelay` message; `timeout` raises a request's idle timeout where the
|
|
60
|
+
* listener holds its connection (16.22); `hangUpAll` aborts every request still being answered.
|
|
61
|
+
*/
|
|
62
|
+
export function serveOver(
|
|
63
|
+
channel: Channel<FromRelay>,
|
|
64
|
+
answering: {
|
|
65
|
+
fetch(request: Request): Promise<Response>
|
|
66
|
+
/** 37.3 — whether the answer admits the request to the mux. */
|
|
67
|
+
admits(request: Request, response: Response): boolean
|
|
68
|
+
/** The answer to a throw out of `fetch`, which is an answer and not a dropped message. */
|
|
69
|
+
failed(why: string): Response
|
|
70
|
+
/** What a failure in the middle of an html body is shown as, appended to it. */
|
|
71
|
+
failureMarkup(why: string): string
|
|
72
|
+
},
|
|
73
|
+
) {
|
|
74
|
+
// 38.96 — EACH REQUEST'S SIGNAL, by request id, until its answer ends: the listener holds the
|
|
75
|
+
// connection, so a hang-up reaches the worker as a `cancel` rather than through bun.
|
|
76
|
+
const hangUps = new Map<number, AbortController>()
|
|
77
|
+
// Each answer whose head has gone out, by request id: what its chunks are read from, and whether a
|
|
78
|
+
// failure in the middle can still be shown as markup.
|
|
79
|
+
const bodies = new Map<
|
|
80
|
+
number,
|
|
81
|
+
{ reader: ReadableStreamDefaultReader<Uint8Array>; html: boolean }
|
|
82
|
+
>()
|
|
83
|
+
// 16.22 — each request built here, known by the ask it was built from.
|
|
84
|
+
const asks = new WeakMap<Request, number>()
|
|
85
|
+
|
|
86
|
+
const ended = (id: number): void => {
|
|
87
|
+
hangUps.delete(id)
|
|
88
|
+
bodies.delete(id)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Answers one request, sending back its head and holding its body for pulls.
|
|
93
|
+
*
|
|
94
|
+
* A THROW BEFORE THE HEAD IS AN ANSWER, not a dropped message. The listener is waiting on a head
|
|
95
|
+
* for this id and nothing else will send one, so an escape left the reader's tab hanging until
|
|
96
|
+
* they gave up.
|
|
97
|
+
*/
|
|
98
|
+
async function answer(ask: Ask): Promise<void> {
|
|
99
|
+
const hangUp = new AbortController()
|
|
100
|
+
hangUps.set(ask.id, hangUp)
|
|
101
|
+
const request = new Request(ask.url, {
|
|
102
|
+
method: ask.method,
|
|
103
|
+
headers: ask.headers,
|
|
104
|
+
body: ask.body,
|
|
105
|
+
signal: hangUp.signal,
|
|
106
|
+
})
|
|
107
|
+
asks.set(request, ask.id)
|
|
108
|
+
let response: Response
|
|
109
|
+
try {
|
|
110
|
+
response = await answering.fetch(request)
|
|
111
|
+
} catch (thrown) {
|
|
112
|
+
response = answering.failed(describe(thrown))
|
|
113
|
+
}
|
|
114
|
+
const body = BODILESS.has(response.status) ? null : response.body
|
|
115
|
+
// A DECLARED LENGTH IS A BODY ALREADY WHOLE, and it crosses with its head: re-streamed through
|
|
116
|
+
// the listener, bun meets a first read that has to wait on the worker and answers it chunked,
|
|
117
|
+
// dropping the length the app declared.
|
|
118
|
+
const whole =
|
|
119
|
+
body !== null && response.headers.has('content-length')
|
|
120
|
+
? await response.arrayBuffer()
|
|
121
|
+
: undefined
|
|
122
|
+
channel.postMessage({
|
|
123
|
+
kind: 'head',
|
|
124
|
+
id: ask.id,
|
|
125
|
+
status: response.status,
|
|
126
|
+
statusText: response.statusText,
|
|
127
|
+
headers: [...response.headers.entries()],
|
|
128
|
+
empty: body === null,
|
|
129
|
+
admitted: answering.admits(request, response),
|
|
130
|
+
whole,
|
|
131
|
+
})
|
|
132
|
+
if (body === null || whole !== undefined) {
|
|
133
|
+
ended(ask.id)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
bodies.set(ask.id, {
|
|
137
|
+
reader: body.getReader(),
|
|
138
|
+
html: (response.headers.get('content-type') ?? '').startsWith(
|
|
139
|
+
'text/html',
|
|
140
|
+
),
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sends the next chunk of an answer's body, or its end.
|
|
146
|
+
*
|
|
147
|
+
* ONE CHUNK PER ASK. A throw in the middle ends the stream in an error, as `abide start` does —
|
|
148
|
+
* and where the answer is html a handler streams, what broke goes out as markup first, a reader
|
|
149
|
+
* shown a blank tab otherwise being told nothing. A page is never here: it is built whole (41.1).
|
|
150
|
+
*/
|
|
151
|
+
async function pull(id: number): Promise<void> {
|
|
152
|
+
const reading = bodies.get(id)
|
|
153
|
+
if (reading === undefined) return
|
|
154
|
+
try {
|
|
155
|
+
const { done, value } = await reading.reader.read()
|
|
156
|
+
if (done) {
|
|
157
|
+
ended(id)
|
|
158
|
+
channel.postMessage({ kind: 'end', id })
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
// THE VIEW'S OWN BYTES. A chunk may be a view on a buffer its producer still owns — a
|
|
162
|
+
// compressor's `Buffer` is a slice of a shared pool, and `Buffer#slice` copies nothing —
|
|
163
|
+
// so sending `value.buffer` sent the whole pool.
|
|
164
|
+
const bytes = new Uint8Array(value).buffer as ArrayBuffer
|
|
165
|
+
channel.postMessage({ kind: 'chunk', id, bytes })
|
|
166
|
+
} catch (thrown) {
|
|
167
|
+
ended(id)
|
|
168
|
+
const why = describe(thrown)
|
|
169
|
+
if (reading.html)
|
|
170
|
+
channel.postMessage({
|
|
171
|
+
kind: 'chunk',
|
|
172
|
+
id,
|
|
173
|
+
bytes: new TextEncoder().encode(
|
|
174
|
+
answering.failureMarkup(why),
|
|
175
|
+
).buffer as ArrayBuffer,
|
|
176
|
+
})
|
|
177
|
+
channel.postMessage({ kind: 'fail', id, message: why })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
take(message: ToRelay): Promise<void> | undefined {
|
|
183
|
+
if (message.kind === 'request') return answer(message)
|
|
184
|
+
if (message.kind === 'pull') return pull(message.id)
|
|
185
|
+
const reading = bodies.get(message.id)
|
|
186
|
+
hangUps.get(message.id)?.abort()
|
|
187
|
+
ended(message.id)
|
|
188
|
+
return reading?.reader.cancel()
|
|
189
|
+
},
|
|
190
|
+
timeout(request: Request, seconds: number): void {
|
|
191
|
+
const id = asks.get(request)
|
|
192
|
+
if (id !== undefined)
|
|
193
|
+
channel.postMessage({ kind: 'timeout', id, seconds })
|
|
194
|
+
},
|
|
195
|
+
hangUpAll(): void {
|
|
196
|
+
for (const hangUp of hangUps.values()) hangUp.abort()
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// A request in flight: the request itself — because a worker replaced before it answered answers
|
|
202
|
+
// nothing, and the ask has to be put to the one that exists (38.24) — the channel it was asked over,
|
|
203
|
+
// and, once its head has come back, the stream the body is fed into and the pull waiting on the
|
|
204
|
+
// worker's next chunk (38.35).
|
|
205
|
+
type InFlight = {
|
|
206
|
+
// 16.22 — the request bun handed the listener, which is the one its idle timeout is raised on.
|
|
207
|
+
request: Request
|
|
208
|
+
headers: [string, string][]
|
|
209
|
+
body: ArrayBuffer | null
|
|
210
|
+
settle: (response: Response) => void
|
|
211
|
+
asked: Channel<ToRelay> | undefined
|
|
212
|
+
stream: ReadableStreamDefaultController<Uint8Array> | null
|
|
213
|
+
pulled: (() => void) | null
|
|
214
|
+
// A failure the worker sent behind a chunk still queued, raised on the pull after it — raising it
|
|
215
|
+
// at once would discard the chunk, which is the markup saying what broke.
|
|
216
|
+
failed: string | null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The listener's half: asks each request of the worker on its channel, and settles it with the
|
|
221
|
+
* response the worker's head and chunks describe. `take` is handed every `FromRelay` message;
|
|
222
|
+
* `replace` points it at a worker's channel — there is none until the first — or at none while one
|
|
223
|
+
* compiles; `endAll` ends what the worker that is gone was answering.
|
|
224
|
+
*/
|
|
225
|
+
export function askOver(
|
|
226
|
+
raiseTimeout: (request: Request, seconds: number) => void,
|
|
227
|
+
) {
|
|
228
|
+
let channel: Channel<ToRelay> | undefined
|
|
229
|
+
const inFlight = new Map<number, InFlight>()
|
|
230
|
+
// ASKED BEFORE THERE IS ANYONE TO ASK. A replacement ends the worker and the next one does not
|
|
231
|
+
// exist until its compile finishes, so a request arriving in that window waits here.
|
|
232
|
+
const queued: InFlight[] = []
|
|
233
|
+
// 37.3 — the answers the worker said `admitsMux`, which the listener upgrades on.
|
|
234
|
+
const admitted = new WeakSet<Response>()
|
|
235
|
+
let nextId = 0
|
|
236
|
+
|
|
237
|
+
// NOT TRANSFERRED. Handing the buffer over detaches it here, and a request re-asked of the
|
|
238
|
+
// replacement would post a body that is no longer there.
|
|
239
|
+
function forward(waiting: InFlight): void {
|
|
240
|
+
if (channel === undefined) {
|
|
241
|
+
queued.push(waiting)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
nextId += 1
|
|
245
|
+
const id = nextId
|
|
246
|
+
waiting.asked = channel
|
|
247
|
+
inFlight.set(id, waiting)
|
|
248
|
+
channel.postMessage({
|
|
249
|
+
kind: 'request',
|
|
250
|
+
id,
|
|
251
|
+
method: waiting.request.method,
|
|
252
|
+
url: waiting.request.url,
|
|
253
|
+
headers: waiting.headers,
|
|
254
|
+
body: waiting.body,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 38.35, 38.36 — THE HEAD SETTLES THE RESPONSE, and its body pulls one chunk from the worker per
|
|
259
|
+
// read the connection makes: a high-water mark of 0 asks for nothing ahead of the reader.
|
|
260
|
+
function settleHead(
|
|
261
|
+
id: number,
|
|
262
|
+
waiting: InFlight,
|
|
263
|
+
head: Extract<FromRelay, { kind: 'head' }>,
|
|
264
|
+
): void {
|
|
265
|
+
const init = {
|
|
266
|
+
status: head.status,
|
|
267
|
+
statusText: head.statusText,
|
|
268
|
+
headers: head.headers,
|
|
269
|
+
}
|
|
270
|
+
if (head.empty || head.whole !== undefined) {
|
|
271
|
+
inFlight.delete(id)
|
|
272
|
+
const answered = new Response(head.whole ?? null, init)
|
|
273
|
+
// The admission has no body, so an admitted answer is always settled here.
|
|
274
|
+
if (head.admitted) admitted.add(answered)
|
|
275
|
+
waiting.settle(answered)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
const asked = waiting.asked
|
|
279
|
+
const body = new ReadableStream<Uint8Array>(
|
|
280
|
+
{
|
|
281
|
+
start(controller) {
|
|
282
|
+
waiting.stream = controller
|
|
283
|
+
},
|
|
284
|
+
pull(controller) {
|
|
285
|
+
if (waiting.failed !== null) {
|
|
286
|
+
inFlight.delete(id)
|
|
287
|
+
controller.error(new Error(waiting.failed))
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
return new Promise<void>((done) => {
|
|
291
|
+
waiting.pulled = done
|
|
292
|
+
asked?.postMessage({ kind: 'pull', id })
|
|
293
|
+
})
|
|
294
|
+
},
|
|
295
|
+
cancel() {
|
|
296
|
+
inFlight.delete(id)
|
|
297
|
+
asked?.postMessage({ kind: 'cancel', id })
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{ highWaterMark: 0 },
|
|
301
|
+
)
|
|
302
|
+
waiting.settle(new Response(body, init))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
/**
|
|
307
|
+
* The worker's answer to `request`.
|
|
308
|
+
*
|
|
309
|
+
* A REQUEST AS A MESSAGE. `Request` does not survive a structured clone, so what crosses is
|
|
310
|
+
* what the worker rebuilds one from — and the body is collected ONCE, HERE, which is what lets
|
|
311
|
+
* it be asked again of a replacement (38.24): a body can only be read off a `Request` once.
|
|
312
|
+
*/
|
|
313
|
+
async ask(request: Request): Promise<Response> {
|
|
314
|
+
const body =
|
|
315
|
+
request.body === null ? null : await request.arrayBuffer()
|
|
316
|
+
return await new Promise<Response>((settle) => {
|
|
317
|
+
forward({
|
|
318
|
+
request,
|
|
319
|
+
headers: identityOnly(request.headers),
|
|
320
|
+
body,
|
|
321
|
+
settle,
|
|
322
|
+
asked: undefined,
|
|
323
|
+
stream: null,
|
|
324
|
+
pulled: null,
|
|
325
|
+
failed: null,
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
},
|
|
329
|
+
/** Whether the worker admitted this answer's request to the mux. 37.3. */
|
|
330
|
+
admitted(response: Response): boolean {
|
|
331
|
+
return admitted.has(response)
|
|
332
|
+
},
|
|
333
|
+
take(message: FromRelay): void {
|
|
334
|
+
const waiting = inFlight.get(message.id)
|
|
335
|
+
if (waiting === undefined) return
|
|
336
|
+
if (message.kind === 'timeout') {
|
|
337
|
+
raiseTimeout(waiting.request, message.seconds)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
if (message.kind === 'head') {
|
|
341
|
+
settleHead(message.id, waiting, message)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
const stream = waiting.stream
|
|
345
|
+
const pulled = waiting.pulled
|
|
346
|
+
waiting.pulled = null
|
|
347
|
+
if (stream === null) return
|
|
348
|
+
if (message.kind === 'chunk')
|
|
349
|
+
stream.enqueue(new Uint8Array(message.bytes))
|
|
350
|
+
else if (message.kind === 'end') {
|
|
351
|
+
inFlight.delete(message.id)
|
|
352
|
+
stream.close()
|
|
353
|
+
} else waiting.failed = message.message
|
|
354
|
+
pulled?.()
|
|
355
|
+
},
|
|
356
|
+
/** Asks the worker on `next` from now on — every request that waited for one included. */
|
|
357
|
+
replace(next: Channel<ToRelay> | undefined): void {
|
|
358
|
+
channel = next
|
|
359
|
+
if (next === undefined) return
|
|
360
|
+
for (const again of queued.splice(0)) forward(again)
|
|
361
|
+
},
|
|
362
|
+
/**
|
|
363
|
+
* Ends what a worker that is gone was answering. A request whose head has not gone out is
|
|
364
|
+
* asked again of the next (38.24) where `reask`, and answered 500 where not — the request in
|
|
365
|
+
* hand may be what ended the app (38.41). 38.37 — one whose head has gone out cannot be
|
|
366
|
+
* answered again, and ends as `abide start`'s does.
|
|
367
|
+
*/
|
|
368
|
+
endAll(why: string, reask: boolean): void {
|
|
369
|
+
for (const held of inFlight.values()) {
|
|
370
|
+
if (held.stream !== null) {
|
|
371
|
+
held.stream.error(new Error(why))
|
|
372
|
+
held.pulled?.()
|
|
373
|
+
} else if (reask) queued.push(held)
|
|
374
|
+
else held.settle(plainAnswer(why, 500))
|
|
375
|
+
}
|
|
376
|
+
inFlight.clear()
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 38.38 — ASKED WITHOUT `accept-encoding`, so the app answers uncompressed. A failure after the head
|
|
382
|
+
// is shown as markup appended to the stream, and markup appended to a compressed stream is a stream
|
|
383
|
+
// no browser can decode. Localhost buys nothing from the bytes saved.
|
|
384
|
+
function identityOnly(headers: Headers): [string, string][] {
|
|
385
|
+
const kept: [string, string][] = []
|
|
386
|
+
for (const [name, value] of headers)
|
|
387
|
+
if (name !== 'accept-encoding') kept.push([name, value])
|
|
388
|
+
return kept
|
|
389
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// is `ledger-linux-x64`, and that is what a reader types.
|
|
4
4
|
|
|
5
5
|
import { type Host, runCommands } from './commands.ts'
|
|
6
|
-
import { type AppOrigin, bootApp } from './createApp.ts'
|
|
7
|
-
import { type BuildParts, builtSources
|
|
6
|
+
import { type AppHandle, type AppOrigin, bootApp } from './createApp.ts'
|
|
7
|
+
import { type BuildParts, builtSources } from './serveBuild.ts'
|
|
8
8
|
|
|
9
9
|
export async function runCompiled(
|
|
10
10
|
origin: AppOrigin,
|
|
@@ -25,18 +25,15 @@ export async function runCompiled(
|
|
|
25
25
|
}))
|
|
26
26
|
},
|
|
27
27
|
})
|
|
28
|
+
// 38.49 — THE APP IT HOLDS, whatever the flags say.
|
|
29
|
+
const app: AppHandle = {
|
|
30
|
+
package: origin.package,
|
|
31
|
+
handlers: async () => parts.handlers,
|
|
32
|
+
boot: async () => bootApp(origin, () => builtSources(parts)),
|
|
33
|
+
}
|
|
28
34
|
const host: Host = {
|
|
29
35
|
name: process.execPath.slice(process.execPath.lastIndexOf('/') + 1),
|
|
30
|
-
|
|
31
|
-
appPackage: () => origin.package,
|
|
32
|
-
boot: () => bootApp(origin, () => builtSources(parts)),
|
|
33
|
-
// 38.49.
|
|
34
|
-
serve: ({ values: { port } }) =>
|
|
35
|
-
serveApp(
|
|
36
|
-
origin,
|
|
37
|
-
() => builtSources(parts),
|
|
38
|
-
port === undefined ? undefined : Number(port),
|
|
39
|
-
),
|
|
36
|
+
app: () => app,
|
|
40
37
|
source: undefined,
|
|
41
38
|
}
|
|
42
39
|
const code = await runCommands(host, Bun.argv.slice(2))
|
package/src/server/runDesktop.ts
CHANGED
|
@@ -22,12 +22,11 @@ import {
|
|
|
22
22
|
sendTyped,
|
|
23
23
|
stringOf,
|
|
24
24
|
} from './appKit.ts'
|
|
25
|
-
import {
|
|
25
|
+
import { configuredMount, dataDirectory } from './config.ts'
|
|
26
26
|
import { configFields } from './configFields.ts'
|
|
27
|
-
import { type App, type AppOrigin,
|
|
27
|
+
import { type App, type AppOrigin, bootEntered, enterApp } from './createApp.ts'
|
|
28
28
|
import { executableName } from './executableName.ts'
|
|
29
29
|
import { HOOKS, inOrder, reportUnexpected } from './hooks.ts'
|
|
30
|
-
import { installServer } from './installServer.ts'
|
|
31
30
|
import { launcherOf } from './launcherOptions.ts'
|
|
32
31
|
import { type Launch, launcherPage } from './launcherPage.ts'
|
|
33
32
|
import { installMenu, menuBar, type NativeItem } from './nativeMenu.ts'
|
|
@@ -84,11 +83,9 @@ async function launch(
|
|
|
84
83
|
load: () => ReturnType<typeof builtSources>,
|
|
85
84
|
{ launch: launchModule, document, icon, accent }: Desktop,
|
|
86
85
|
): Promise<void> {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// `onLauncher` is filed.
|
|
91
|
-
await origin.hooks?.()
|
|
86
|
+
// 37.1 — ENTERED ONCE, before the window: the start form reads `onConfig` and the launcher's
|
|
87
|
+
// `onLauncher` is filed. Every start after it boots in what this entered (`bootEntered`).
|
|
88
|
+
await enterApp(origin)
|
|
92
89
|
// 38.85 — a launcher page that fails to load leaves the built-in page, with its defaults.
|
|
93
90
|
const launchPage = await launchModule?.().catch((thrown: unknown) => {
|
|
94
91
|
reportUnexpected(thrown)
|
|
@@ -228,7 +225,7 @@ async function launch(
|
|
|
228
225
|
else process.env[name] = value
|
|
229
226
|
let app: App
|
|
230
227
|
try {
|
|
231
|
-
app = await
|
|
228
|
+
app = await bootEntered(load)
|
|
232
229
|
} catch (thrown) {
|
|
233
230
|
failure = refused('start', thrown)
|
|
234
231
|
return failure
|
package/src/server/serveBuild.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// what it found, so the addresses the server answers and the addresses the client router knows
|
|
7
7
|
// are two readings of one walk rather than two walks that have to agree (30.8).
|
|
8
8
|
|
|
9
|
-
import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
10
9
|
import {
|
|
11
10
|
type Compilation,
|
|
12
11
|
serveCompilation,
|
|
@@ -24,15 +23,10 @@ import {
|
|
|
24
23
|
MANIFEST,
|
|
25
24
|
PUBLIC_DIRECTORY,
|
|
26
25
|
} from './CLIENT_BUILD.ts'
|
|
27
|
-
import {
|
|
28
|
-
import { stopOnSignalOrFault } from './faultPolicy.ts'
|
|
26
|
+
import { bundleServer, type ServedFile } from './clientBundle.ts'
|
|
29
27
|
import type { Manifest } from './manifestOf.ts'
|
|
30
28
|
import { namesUnder } from './namesUnder.ts'
|
|
31
|
-
import {
|
|
32
|
-
ENCODINGS,
|
|
33
|
-
type Encoding,
|
|
34
|
-
negotiateEncoding,
|
|
35
|
-
} from './negotiateEncoding.ts'
|
|
29
|
+
import { ENCODINGS } from './negotiateEncoding.ts'
|
|
36
30
|
import { runBuild } from './runBuild.ts'
|
|
37
31
|
|
|
38
32
|
/**
|
|
@@ -49,23 +43,6 @@ export type BuildParts = Omit<SourceParts, 'client' | 'explained'> & {
|
|
|
49
43
|
client: Map<string, string>
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
/**
|
|
53
|
-
* The app booted (`bootApp`) and listening, stopped on a signal or an unawaited fault.
|
|
54
|
-
*
|
|
55
|
-
* Listening on `LISTENING.server`, for the caller to say where.
|
|
56
|
-
*/
|
|
57
|
-
export async function serveApp(
|
|
58
|
-
origin: AppOrigin,
|
|
59
|
-
load: () => Promise<AppSources>,
|
|
60
|
-
port: number | undefined,
|
|
61
|
-
): Promise<App> {
|
|
62
|
-
const app = await bootApp(origin, load)
|
|
63
|
-
await app.listen(port === undefined ? {} : { port })
|
|
64
|
-
// ONCE SERVING, and not before: a boot that fails is 37.6's to fail hard.
|
|
65
|
-
stopOnSignalOrFault(app)
|
|
66
|
-
return app
|
|
67
|
-
}
|
|
68
|
-
|
|
69
46
|
/**
|
|
70
47
|
* Serves the compilation `abide build` kept in `out` to this process's imports; `false` where there
|
|
71
48
|
* is none, because the app under `root` holds nothing to build.
|
|
@@ -128,62 +105,38 @@ export function pathsUnder(directory: string): Map<string, string> {
|
|
|
128
105
|
|
|
129
106
|
/** The sources an `App` is booted over, out of the parts `abide build` produced. */
|
|
130
107
|
export function builtSources(parts: BuildParts): Promise<AppSources> {
|
|
131
|
-
// THE BUNDLE, ENUMERATED RATHER THAN RESOLVED. A request names a file in this set or it names
|
|
132
|
-
// nothing — there is no path to normalize, so `..` is not a case that has to be got right,
|
|
133
|
-
// and what is servable is exactly what the build produced.
|
|
134
|
-
//
|
|
135
108
|
// THE PRECOMPRESSED COPIES ARE NOT ADDRESSES OF THEIR OWN. They are the same file in another
|
|
136
109
|
// encoding (38.5), so they are found under the name they encode and served as that name.
|
|
137
110
|
const suffixes = Object.values(ENCODING_SUFFIX) as string[]
|
|
138
|
-
const
|
|
139
|
-
for (const name of parts.client
|
|
111
|
+
const files = new Map<string, ServedFile>()
|
|
112
|
+
for (const [name, path] of parts.client) {
|
|
140
113
|
if (suffixes.some((suffix) => name.endsWith(suffix))) continue
|
|
141
|
-
const
|
|
142
|
-
for (const encoding of ENCODINGS)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
assets.set(name, encodings)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const framework = (
|
|
149
|
-
pathname: string,
|
|
150
|
-
request: Request,
|
|
151
|
-
): Response | undefined => {
|
|
152
|
-
if (!pathname.startsWith(`${CLIENT_ADDRESS}/`)) return undefined
|
|
153
|
-
const name = pathname.slice(CLIENT_ADDRESS.length + 1)
|
|
154
|
-
const encodings = assets.get(name)
|
|
155
|
-
if (encodings === undefined) return undefined
|
|
156
|
-
// THE TYPE IS THE NAME'S, not the path's: a compiled app's path carries a hash the
|
|
157
|
-
// bundler put before the extension, and a `.br`'s extension names nothing.
|
|
158
|
-
const type = Bun.file(name).type
|
|
159
|
-
// 21.5 — EVERY NAME IN THE BUNDLE CARRIES A HASH OF ITS BYTES, the entry's too (38.5),
|
|
160
|
-
// so a name never answers two different files and a year is safe for all of them. A
|
|
161
|
-
// deploy is new names in a new document, never new bytes at an old address.
|
|
162
|
-
const headers: Record<string, string> = {
|
|
163
|
-
'cache-control': 'public, max-age=31536000, immutable',
|
|
164
|
-
'content-type': type,
|
|
165
|
-
vary: 'accept-encoding',
|
|
114
|
+
const encoded: ServedFile['encoded'] = {}
|
|
115
|
+
for (const encoding of ENCODINGS) {
|
|
116
|
+
const copy = parts.client.get(name + ENCODING_SUFFIX[encoding])
|
|
117
|
+
if (copy !== undefined) encoded[encoding] = () => Bun.file(copy)
|
|
166
118
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
headers,
|
|
175
|
-
})
|
|
176
|
-
headers['content-encoding'] = encoding
|
|
177
|
-
return new Response(
|
|
178
|
-
Bun.file(
|
|
179
|
-
parts.client.get(name + ENCODING_SUFFIX[encoding]) as string,
|
|
180
|
-
),
|
|
181
|
-
{ headers },
|
|
182
|
-
)
|
|
119
|
+
files.set(name, {
|
|
120
|
+
// THE TYPE IS THE NAME'S, not the path's: a compiled app's path carries a hash the
|
|
121
|
+
// bundler put before the extension, and a `.br`'s extension names nothing.
|
|
122
|
+
type: Bun.file(name).type,
|
|
123
|
+
body: () => Bun.file(path),
|
|
124
|
+
encoded,
|
|
125
|
+
})
|
|
183
126
|
}
|
|
184
127
|
return appSources({
|
|
185
128
|
...parts,
|
|
186
129
|
explained: undefined,
|
|
187
|
-
client: {
|
|
130
|
+
client: {
|
|
131
|
+
manifest: parts.manifest,
|
|
132
|
+
head: '',
|
|
133
|
+
// 21.5 — EVERY NAME IN THE BUNDLE CARRIES A HASH OF ITS BYTES, the entry's too (38.5), so
|
|
134
|
+
// a name never answers two different files and a year is safe for all of them. A deploy
|
|
135
|
+
// is new names in a new document, never new bytes at an old address.
|
|
136
|
+
framework: bundleServer(
|
|
137
|
+
files,
|
|
138
|
+
'public, max-age=31536000, immutable',
|
|
139
|
+
),
|
|
140
|
+
},
|
|
188
141
|
})
|
|
189
142
|
}
|