@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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// built per request, since a trigger names args rather than a value. A `global` value is also fed its productions, a guarded one only to a
|
|
11
11
|
// subscription its rungs admitted when it opened (18.8) — they run once, in the connection's request.
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { problemOf } from '#shared/Failed.ts'
|
|
14
14
|
import { GLOBAL_GETS } from '#shared/GLOBAL_GETS.ts'
|
|
15
15
|
import { namedLogger } from '#shared/log.ts'
|
|
16
16
|
import { compose } from '#shared/Middleware.ts'
|
|
@@ -34,19 +34,103 @@ import {
|
|
|
34
34
|
releaseScope,
|
|
35
35
|
scopeFor,
|
|
36
36
|
} from './requestScope.ts'
|
|
37
|
+
import { EVENT_STREAM_OPENER, streamEvent } from './streamEvent.ts'
|
|
37
38
|
import { patchesSince, versionOf } from './versionOf.ts'
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* The far end of one connection to the mux, which its frames are written to.
|
|
41
|
+
* The far end of one connection to the mux, which its frames are written to as text.
|
|
41
42
|
*
|
|
42
43
|
* Bun's socket under `abide start`, and under `abide dev` a stand-in that posts each frame to the
|
|
43
44
|
* listener holding the real one (38.14).
|
|
44
45
|
*/
|
|
45
|
-
export type
|
|
46
|
+
export type Socket = {
|
|
46
47
|
send(text: string): unknown
|
|
47
48
|
close(code: number, reason: string): unknown
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
const ENCODER = new TextEncoder()
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One `ServerFrame`, for every follower it is sent to.
|
|
55
|
+
*
|
|
56
|
+
* 18.6 — ITS BODY IS SERIALIZED ONCE PER VERSION, and each encoding of it once however many
|
|
57
|
+
* followers it reaches: a socket is sent the JSON with its follower's id spliced in, and an event
|
|
58
|
+
* stream the event, which names no follower and is the same text for every stream.
|
|
59
|
+
*/
|
|
60
|
+
class Frame {
|
|
61
|
+
readonly op: ServerFrame['op']
|
|
62
|
+
readonly base: number | undefined
|
|
63
|
+
readonly version: number | undefined
|
|
64
|
+
// The value, the patch's ops or the refusal, as JSON.
|
|
65
|
+
readonly body: string | undefined
|
|
66
|
+
// What follows the id in the JSON, and the event's bytes, each written on first use.
|
|
67
|
+
private tail: string | undefined = undefined
|
|
68
|
+
private event: Uint8Array | undefined = undefined
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
op: ServerFrame['op'],
|
|
72
|
+
base: number | undefined,
|
|
73
|
+
version: number | undefined,
|
|
74
|
+
body: string | undefined,
|
|
75
|
+
) {
|
|
76
|
+
this.op = op
|
|
77
|
+
this.base = base
|
|
78
|
+
this.version = version
|
|
79
|
+
this.body = body
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** As `ServerFrame` JSON for `id`: `base` and `version` where it has them, then the body under its op's member. */
|
|
83
|
+
json(id: number): string {
|
|
84
|
+
if (this.tail === undefined) {
|
|
85
|
+
let tail = ''
|
|
86
|
+
if (this.base !== undefined) tail += `,"base":${this.base}`
|
|
87
|
+
if (this.version !== undefined) tail += `,"version":${this.version}`
|
|
88
|
+
if (this.body !== undefined)
|
|
89
|
+
tail += `,"${this.op === 'patch' ? 'ops' : this.op}":${this.body}`
|
|
90
|
+
this.tail = `${tail}}`
|
|
91
|
+
}
|
|
92
|
+
return `{"op":"${this.op}","id":${id}${this.tail}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** As the SSE event 18.20 makes it, encoded: the op its name, the version its id. */
|
|
96
|
+
asEvent(): Uint8Array {
|
|
97
|
+
this.event ??= ENCODER.encode(
|
|
98
|
+
streamEvent(
|
|
99
|
+
this.op,
|
|
100
|
+
this.version,
|
|
101
|
+
this.body ?? (this.op === 'value' ? 'null' : ''),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
return this.event
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A trigger's frame (7.10, 7.18), the same for every follower and every trigger. */
|
|
109
|
+
const TRIGGERS = {
|
|
110
|
+
invalidate: new Frame('invalidate', undefined, undefined, undefined),
|
|
111
|
+
refresh: new Frame('refresh', undefined, undefined, undefined),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** What a frame is written to, under the follower's id: a socket, or an event stream. */
|
|
115
|
+
type Peer = {
|
|
116
|
+
send(id: number, frame: Frame): void
|
|
117
|
+
close(code: number, reason: string): unknown
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A socket's peer: each frame as the `ServerFrame` JSON a browser parses. */
|
|
121
|
+
class SocketPeer implements Peer {
|
|
122
|
+
readonly socket: Socket
|
|
123
|
+
constructor(socket: Socket) {
|
|
124
|
+
this.socket = socket
|
|
125
|
+
}
|
|
126
|
+
send(id: number, frame: Frame): void {
|
|
127
|
+
this.socket.send(frame.json(id))
|
|
128
|
+
}
|
|
129
|
+
close(code: number, reason: string): unknown {
|
|
130
|
+
return this.socket.close(code, reason)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
50
134
|
const SOCKET_LOG = namedLogger('abide:socket')
|
|
51
135
|
|
|
52
136
|
// 18.3 — EVERY SUBSCRIPTION TO A HANDLER, by handler, for its triggers to reach (7.10, 7.18).
|
|
@@ -95,7 +179,7 @@ export function broadcast(
|
|
|
95
179
|
// the cell it is fed from (`FOLLOWED`), whichever trigger reached it — where the entry it is
|
|
96
180
|
// fed from is still the one standing.
|
|
97
181
|
if (attach(rpc as Mounted['handler'], follower) !== undefined) continue
|
|
98
|
-
send(follower.
|
|
182
|
+
follower.peer.send(follower.id, TRIGGERS[member])
|
|
99
183
|
}
|
|
100
184
|
}
|
|
101
185
|
|
|
@@ -111,7 +195,7 @@ export function swept(
|
|
|
111
195
|
for (const follower of followed.all) {
|
|
112
196
|
if (attach(rpc as Mounted['handler'], follower) !== undefined)
|
|
113
197
|
continue
|
|
114
|
-
send(follower.
|
|
198
|
+
follower.peer.send(follower.id, TRIGGERS[member])
|
|
115
199
|
}
|
|
116
200
|
}
|
|
117
201
|
}
|
|
@@ -157,12 +241,14 @@ function attach(rpc: Mounted['handler'], follower: Follower): unknown {
|
|
|
157
241
|
return serving
|
|
158
242
|
}
|
|
159
243
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
244
|
+
/** A refusal's frame: the failure `error` is, as Problem Details (15.21). */
|
|
245
|
+
function refusal(error: unknown): Frame {
|
|
246
|
+
return new Frame(
|
|
247
|
+
'error',
|
|
248
|
+
undefined,
|
|
249
|
+
undefined,
|
|
250
|
+
JSON.stringify(problemOf(failureOf(error))),
|
|
251
|
+
)
|
|
166
252
|
}
|
|
167
253
|
|
|
168
254
|
/**
|
|
@@ -180,11 +266,15 @@ export class Mux {
|
|
|
180
266
|
this.app = app
|
|
181
267
|
}
|
|
182
268
|
|
|
183
|
-
/** One connection, upgraded from `request`, whose frames go to `
|
|
184
|
-
connect(request: Request,
|
|
269
|
+
/** One connection, upgraded from `request`, whose frames go to `socket`. */
|
|
270
|
+
connect(request: Request, socket: Socket): MuxConnection {
|
|
185
271
|
if (SOCKET_LOG.enabled())
|
|
186
272
|
SOCKET_LOG.debug('open', new URL(request.url).pathname)
|
|
187
|
-
const connection = new MuxConnection(
|
|
273
|
+
const connection = new MuxConnection(
|
|
274
|
+
this,
|
|
275
|
+
request,
|
|
276
|
+
new SocketPeer(socket),
|
|
277
|
+
)
|
|
188
278
|
this.connections.add(connection)
|
|
189
279
|
return connection
|
|
190
280
|
}
|
|
@@ -267,26 +357,24 @@ export class MuxConnection {
|
|
|
267
357
|
: undefined
|
|
268
358
|
const followed = handler?.ui ? handler : undefined
|
|
269
359
|
if (followed === undefined) {
|
|
270
|
-
|
|
271
|
-
op: 'error',
|
|
360
|
+
this.peer.send(
|
|
272
361
|
id,
|
|
273
|
-
|
|
362
|
+
refusal(
|
|
274
363
|
refuse(
|
|
275
364
|
404,
|
|
276
365
|
`No handler a browser follows answers at ${address}.`,
|
|
277
366
|
),
|
|
278
367
|
),
|
|
279
|
-
|
|
368
|
+
)
|
|
280
369
|
return
|
|
281
370
|
}
|
|
282
371
|
// 18.2 — ANOTHER ORIGIN IS CLOSED OUT unless the handler declared it: the same gate, and the
|
|
283
372
|
// same own origin, as its http answer's (16.13, 16.24, 36.2).
|
|
284
373
|
if (closedTo(followed.handler.crossOrigin, this.crossing)) {
|
|
285
|
-
|
|
286
|
-
op: 'error',
|
|
374
|
+
this.peer.send(
|
|
287
375
|
id,
|
|
288
|
-
|
|
289
|
-
|
|
376
|
+
refusal(crossingRefusal(this.crossing, 'subscription')),
|
|
377
|
+
)
|
|
290
378
|
return
|
|
291
379
|
}
|
|
292
380
|
// A SUBSCRIPTION IS HELD FROM THE FRAME ON, not from when its rungs let it through: an
|
|
@@ -322,7 +410,7 @@ export class MuxConnection {
|
|
|
322
410
|
follow(this, id, followed, args, frame.version, admitted)
|
|
323
411
|
} catch (thrown) {
|
|
324
412
|
if (subscriptions.get(id) === pending) subscriptions.delete(id)
|
|
325
|
-
|
|
413
|
+
this.peer.send(id, refusal(thrown))
|
|
326
414
|
}
|
|
327
415
|
}
|
|
328
416
|
}
|
|
@@ -444,14 +532,12 @@ export function followAsEvents(
|
|
|
444
532
|
args: unknown,
|
|
445
533
|
held: number | undefined,
|
|
446
534
|
): ReadableStream<Uint8Array> {
|
|
447
|
-
const encoder = new TextEncoder()
|
|
448
535
|
let stop = (): void => {}
|
|
449
536
|
let unhold = (): void => {}
|
|
450
537
|
let ended = false
|
|
451
538
|
return new ReadableStream({
|
|
452
539
|
start(controller) {
|
|
453
|
-
|
|
454
|
-
controller.enqueue(encoder.encode(':\n\n'))
|
|
540
|
+
controller.enqueue(ENCODER.encode(EVENT_STREAM_OPENER))
|
|
455
541
|
// Ended by the app's stop as any body it holds open is, or that stop waits on it.
|
|
456
542
|
unhold = heldByApp(() => {
|
|
457
543
|
if (ended) return
|
|
@@ -461,8 +547,10 @@ export function followAsEvents(
|
|
|
461
547
|
})
|
|
462
548
|
stop = followWith(
|
|
463
549
|
{
|
|
464
|
-
|
|
465
|
-
|
|
550
|
+
// 18.6 — the event written and encoded once for every stream, from the body
|
|
551
|
+
// serialized once for every follower: nothing is parsed back.
|
|
552
|
+
send(_id, frame) {
|
|
553
|
+
controller.enqueue(frame.asEvent())
|
|
466
554
|
},
|
|
467
555
|
close() {
|
|
468
556
|
if (ended) return
|
|
@@ -487,37 +575,6 @@ export function followAsEvents(
|
|
|
487
575
|
})
|
|
488
576
|
}
|
|
489
577
|
|
|
490
|
-
/**
|
|
491
|
-
* One mux frame as the SSE event it is: `event`, `id` where it names a version, and `data`.
|
|
492
|
-
*
|
|
493
|
-
* 18.6 — READ OFF THE FRAME'S HEAD, never parsed: the body was serialized once for every follower
|
|
494
|
-
* (`sendAll`), and a parse and a stringify per event stream would pay it again per follower. Every
|
|
495
|
-
* frame this module writes is `{"op":…,"id":…`, numbers named `base` or `version`, and at most one
|
|
496
|
-
* body last — so the body is the rest of the text, as it was written.
|
|
497
|
-
*/
|
|
498
|
-
function eventOf(text: string): string {
|
|
499
|
-
const opEnd = text.indexOf('"', 7)
|
|
500
|
-
const op = text.slice(7, opEnd)
|
|
501
|
-
let id = ''
|
|
502
|
-
let data =
|
|
503
|
-
op === 'value' || op === 'error' ? 'null' : op === 'patch' ? '[]' : ''
|
|
504
|
-
// Past `,"id":` — a number, so the next comma is the next member's.
|
|
505
|
-
let at = text.indexOf(',', opEnd + 6)
|
|
506
|
-
while (at !== -1) {
|
|
507
|
-
const nameEnd = text.indexOf('"', at + 2)
|
|
508
|
-
const name = text.slice(at + 2, nameEnd)
|
|
509
|
-
const from = nameEnd + 2
|
|
510
|
-
if (name !== 'base' && name !== 'version') {
|
|
511
|
-
data = text.slice(from, -1)
|
|
512
|
-
break
|
|
513
|
-
}
|
|
514
|
-
at = text.indexOf(',', from)
|
|
515
|
-
if (name === 'version')
|
|
516
|
-
id = `id: ${text.slice(from, at === -1 ? -1 : at)}\n`
|
|
517
|
-
}
|
|
518
|
-
return `event: ${op}\n${id}data: ${data}\n\n`
|
|
519
|
-
}
|
|
520
|
-
|
|
521
578
|
// 18.6 — ONE WATCH PER VALUE SERVED, however many follow it, and each frame's body serialized once
|
|
522
579
|
// per version: every follower standing where the others stand is sent the same bytes, with its own
|
|
523
580
|
// id spliced in front.
|
|
@@ -544,7 +601,7 @@ function startFeed(serving: unknown): Feed {
|
|
|
544
601
|
const fed = new Set<Follower>()
|
|
545
602
|
// 7.10 — an invalidate of the value, however it was spelled, reaches every copy it feeds.
|
|
546
603
|
const invalidated = (): void => {
|
|
547
|
-
for (const one of fed) send(one.
|
|
604
|
+
for (const one of fed) one.peer.send(one.id, TRIGGERS.invalidate)
|
|
548
605
|
}
|
|
549
606
|
let followed = FOLLOWED.get(past)
|
|
550
607
|
if (followed === undefined) {
|
|
@@ -577,33 +634,36 @@ function startFeed(serving: unknown): Feed {
|
|
|
577
634
|
|
|
578
635
|
function sendAll(serving: unknown, fed: Iterable<Follower>): void {
|
|
579
636
|
const version = versionOf(serving)
|
|
580
|
-
let
|
|
581
|
-
// ONE
|
|
637
|
+
let valueFrame: Frame | undefined
|
|
638
|
+
// ONE FRAME PER VERSION A FOLLOWER STANDS AT, since followers a burst left at different places
|
|
582
639
|
// along the chain are each sent the writes since their own.
|
|
583
|
-
let
|
|
640
|
+
let patchFrames: Map<number, Frame> | undefined
|
|
584
641
|
for (const one of fed) {
|
|
585
642
|
if (one.sent === version) continue
|
|
586
643
|
// 18.11 — the writes, where the follower stands on the chain they were written along.
|
|
587
644
|
const since = one.sent
|
|
588
|
-
let
|
|
589
|
-
if (
|
|
645
|
+
let frame = since === undefined ? undefined : patchFrames?.get(since)
|
|
646
|
+
if (frame === undefined && since !== undefined) {
|
|
590
647
|
const ops = patchesSince(serving, since)
|
|
591
648
|
if (ops !== undefined) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
649
|
+
frame = new Frame('patch', since, version, JSON.stringify(ops))
|
|
650
|
+
patchFrames ??= new Map()
|
|
651
|
+
patchFrames.set(since, frame)
|
|
595
652
|
}
|
|
596
653
|
}
|
|
597
|
-
if (
|
|
598
|
-
if (
|
|
654
|
+
if (frame === undefined) {
|
|
655
|
+
if (valueFrame === undefined) {
|
|
599
656
|
const held = (serving as { peek(): unknown }).peek()
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
657
|
+
valueFrame = new Frame(
|
|
658
|
+
'value',
|
|
659
|
+
undefined,
|
|
660
|
+
version,
|
|
661
|
+
held === undefined ? undefined : JSON.stringify(held),
|
|
662
|
+
)
|
|
604
663
|
}
|
|
605
|
-
|
|
606
|
-
}
|
|
664
|
+
frame = valueFrame
|
|
665
|
+
}
|
|
666
|
+
one.peer.send(one.id, frame)
|
|
607
667
|
one.sent = version
|
|
608
668
|
}
|
|
609
669
|
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// `Values` a handler returned — framed one chunk per pull (16.32, 16.44–16.47).
|
|
5
5
|
|
|
6
6
|
import { isFailed, problemOf } from '#shared/Failed.ts'
|
|
7
|
-
import { refusalFrame } from '#shared/framing.ts'
|
|
8
7
|
import { compressAnswer } from './compressAnswer.ts'
|
|
9
8
|
import { prefers } from './prefers.ts'
|
|
10
9
|
import {
|
|
@@ -13,10 +12,13 @@ import {
|
|
|
13
12
|
heldByApp,
|
|
14
13
|
holdScope,
|
|
15
14
|
} from './requestScope.ts'
|
|
15
|
+
import {
|
|
16
|
+
EVENT_STREAM_OPENER,
|
|
17
|
+
refusalFrame,
|
|
18
|
+
streamEvent,
|
|
19
|
+
} from './streamEvent.ts'
|
|
16
20
|
|
|
17
21
|
const ENCODER = new TextEncoder()
|
|
18
|
-
// AN EVENT STREAM OPENS ON A COMMENT — `bodyOf` says why.
|
|
19
|
-
const SSE_OPENER = ':\n\n'
|
|
20
22
|
|
|
21
23
|
function isValues(
|
|
22
24
|
value: unknown,
|
|
@@ -70,6 +72,8 @@ export async function answerValue(
|
|
|
70
72
|
headers.set('content-type', 'text/html; charset=utf-8')
|
|
71
73
|
if (typeof value === 'string' || value instanceof Uint8Array)
|
|
72
74
|
return new Response(value as BodyInit, { status, headers })
|
|
75
|
+
// 16.75.
|
|
76
|
+
headers.set('x-accel-buffering', 'no')
|
|
73
77
|
return new Response(bodyOf(chunksOf(value), 'bytes'), {
|
|
74
78
|
status,
|
|
75
79
|
headers,
|
|
@@ -96,11 +100,11 @@ function isIterable(value: unknown): value is Iterable<unknown> {
|
|
|
96
100
|
|
|
97
101
|
/**
|
|
98
102
|
* The head of an event stream. 16.45 — never stored, whatever the value's own life said (21.8), and
|
|
99
|
-
* never buffered by a proxy in front.
|
|
103
|
+
* 16.75 — never buffered by a proxy in front.
|
|
100
104
|
*/
|
|
101
105
|
export function sseHeaders(headers: Headers): void {
|
|
102
106
|
headers.set('content-type', 'text/event-stream')
|
|
103
|
-
headers.set('cache-control', 'no-
|
|
107
|
+
headers.set('cache-control', 'private, no-store')
|
|
104
108
|
headers.set('x-accel-buffering', 'no')
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -169,10 +173,16 @@ export function framed(
|
|
|
169
173
|
first?: IteratorResult<unknown>,
|
|
170
174
|
lines = 'application/jsonl',
|
|
171
175
|
): Response {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
else
|
|
176
|
+
// 16.75 — whatever the framing: a proxy holding frames back until the end undoes one per pull.
|
|
177
|
+
// An event stream's head says so itself.
|
|
178
|
+
if (framing === 'sse') sseHeaders(headers)
|
|
179
|
+
else {
|
|
180
|
+
headers.set(
|
|
181
|
+
'content-type',
|
|
182
|
+
framing === 'bytes' ? 'application/octet-stream' : lines,
|
|
183
|
+
)
|
|
184
|
+
headers.set('x-accel-buffering', 'no')
|
|
185
|
+
}
|
|
176
186
|
return new Response(bodyOf(chunks, framing, first), {
|
|
177
187
|
status,
|
|
178
188
|
headers,
|
|
@@ -214,7 +224,7 @@ export function bodyOf(
|
|
|
214
224
|
return new ReadableStream<Uint8Array>({
|
|
215
225
|
start(controller) {
|
|
216
226
|
if (framing === 'sse')
|
|
217
|
-
controller.enqueue(ENCODER.encode(
|
|
227
|
+
controller.enqueue(ENCODER.encode(EVENT_STREAM_OPENER))
|
|
218
228
|
stopped = () => {
|
|
219
229
|
if (ended) return
|
|
220
230
|
release()
|
|
@@ -238,10 +248,7 @@ export function bodyOf(
|
|
|
238
248
|
if (framing !== 'bytes' && isFailed(thrown)) {
|
|
239
249
|
controller.enqueue(
|
|
240
250
|
ENCODER.encode(
|
|
241
|
-
refusalFrame(
|
|
242
|
-
problemOf(thrown),
|
|
243
|
-
framing === 'sse' ? 'data: ' : '',
|
|
244
|
-
),
|
|
251
|
+
refusalFrame(problemOf(thrown), framing),
|
|
245
252
|
),
|
|
246
253
|
)
|
|
247
254
|
controller.close()
|
|
@@ -267,7 +274,13 @@ export function bodyOf(
|
|
|
267
274
|
controller.enqueue(ENCODER.encode(`${JSON.stringify(chunk)}\n`))
|
|
268
275
|
else
|
|
269
276
|
controller.enqueue(
|
|
270
|
-
ENCODER.encode(
|
|
277
|
+
ENCODER.encode(
|
|
278
|
+
streamEvent(
|
|
279
|
+
undefined,
|
|
280
|
+
undefined,
|
|
281
|
+
JSON.stringify(chunk),
|
|
282
|
+
),
|
|
283
|
+
),
|
|
271
284
|
)
|
|
272
285
|
},
|
|
273
286
|
cancel() {
|
package/src/server/appTree.ts
CHANGED
|
@@ -5,23 +5,23 @@
|
|
|
5
5
|
// So listing a directory lists its routes.
|
|
6
6
|
//
|
|
7
7
|
// ONE WALK for the sources, the handler files, the server and lib modules, and the public files
|
|
8
|
-
// `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) —
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// `abide build` copies and `abide dev` answers, and the refusals (23.16, 23.17, 23.18, 23.22) — each
|
|
9
|
+
// file what `placeOf` says of its path, so the walk and every reader of one path agree, and ONE SKIP
|
|
10
|
+
// RULE, `skipped`, for every walk of the tree.
|
|
11
11
|
//
|
|
12
12
|
// A WALK RATHER THAN A GLOB: a glob descends `node_modules` and the build output before a filter
|
|
13
13
|
// can drop them.
|
|
14
14
|
|
|
15
15
|
import { type Dirent, existsSync, readdirSync } from 'node:fs' // bun has no directory listing or sync existence check of its own
|
|
16
|
+
import type { Refusals } from '#compiler/Refusals.ts'
|
|
16
17
|
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
17
18
|
import {
|
|
18
19
|
HOLDS_SERVER,
|
|
19
|
-
LIB_DIRECTORY,
|
|
20
20
|
PUBLIC_DIRECTORY,
|
|
21
21
|
RESERVED_DIRECTORIES,
|
|
22
22
|
SERVER_DIRECTORY,
|
|
23
23
|
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
24
|
-
import { DIRECTORY_FILES, routeOf, skipped } from '
|
|
24
|
+
import { DIRECTORY_FILES, placeOf, routeOf, skipped } from './placement.ts'
|
|
25
25
|
|
|
26
26
|
/** What one walk of the app finds, every path relative to the root. */
|
|
27
27
|
type AppFiles = {
|
|
@@ -35,23 +35,13 @@ type AppFiles = {
|
|
|
35
35
|
published: Map<string, string>
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** Told of each thing the tree refuses, at the path relative to the root. */
|
|
39
|
-
type Refuse = (path: string, message: string) => void
|
|
40
|
-
|
|
41
|
-
const THROW: Refuse = (_path, message) => {
|
|
42
|
-
throw new Error(message)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
38
|
/**
|
|
46
39
|
* The app's `.abide` sources, handler files, server and lib modules and public files, refusing a
|
|
47
|
-
* tree 23.16, 23.17, 23.18 or 23.22 forbids.
|
|
48
|
-
*
|
|
49
|
-
* `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
|
|
50
|
-
* and the checker's route table ignores them, since a refused tree is the build's to report.
|
|
40
|
+
* tree 23.16, 23.17, 23.18 or 23.22 forbids into `refusals`, each at its path below the root.
|
|
51
41
|
*
|
|
52
42
|
* A missing root is an app with nothing in it, not a fault.
|
|
53
43
|
*/
|
|
54
|
-
export function appFiles(root: string,
|
|
44
|
+
export function appFiles(root: string, refusals: Refusals): AppFiles {
|
|
55
45
|
const files: AppFiles = {
|
|
56
46
|
abide: [],
|
|
57
47
|
handlers: [],
|
|
@@ -61,7 +51,7 @@ export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
|
61
51
|
// 23.22, 38.25 — a root that is its package's directory holds the package's own files beside its
|
|
62
52
|
// pages.
|
|
63
53
|
const packaged = existsSync(`${root}/package.json`)
|
|
64
|
-
walk(root, '', undefined, files,
|
|
54
|
+
walk(root, '', undefined, files, refusals, packaged)
|
|
65
55
|
files.abide.sort()
|
|
66
56
|
files.handlers.sort()
|
|
67
57
|
files.modules.sort()
|
|
@@ -69,13 +59,14 @@ export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
|
69
59
|
}
|
|
70
60
|
|
|
71
61
|
// `reserved` is the nearest reserved directory above `relative` — its path and its name — or
|
|
72
|
-
// `undefined` where none is.
|
|
62
|
+
// `undefined` where none is: what 23.16 refuses is the nesting of two, which is a directory's shape
|
|
63
|
+
// rather than a file's place.
|
|
73
64
|
function walk(
|
|
74
65
|
directory: string,
|
|
75
66
|
relative: string,
|
|
76
67
|
reserved: { path: string; name: string } | undefined,
|
|
77
68
|
files: AppFiles,
|
|
78
|
-
|
|
69
|
+
refusals: Refusals,
|
|
79
70
|
packaged = false,
|
|
80
71
|
): void {
|
|
81
72
|
let entries: Dirent[]
|
|
@@ -99,9 +90,11 @@ function walk(
|
|
|
99
90
|
HOLDS_SERVER.has(reserved.name)
|
|
100
91
|
)
|
|
101
92
|
)
|
|
102
|
-
|
|
93
|
+
refusals.at(
|
|
103
94
|
path,
|
|
104
|
-
|
|
95
|
+
new Error(
|
|
96
|
+
`abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
|
|
97
|
+
),
|
|
105
98
|
)
|
|
106
99
|
within = { path, name: entry.name }
|
|
107
100
|
}
|
|
@@ -110,37 +103,43 @@ function walk(
|
|
|
110
103
|
`${path}/`,
|
|
111
104
|
within,
|
|
112
105
|
files,
|
|
113
|
-
|
|
106
|
+
refusals,
|
|
114
107
|
)
|
|
115
108
|
continue
|
|
116
109
|
}
|
|
117
110
|
const abide = path.endsWith('.abide')
|
|
118
111
|
if (abide) files.abide.push(path)
|
|
119
|
-
|
|
112
|
+
const place = placeOf(path)
|
|
113
|
+
if (place === 'address') {
|
|
120
114
|
if (!abide && !isModule(path)) {
|
|
121
115
|
if (packaged && relative === '' && !path.endsWith('.ts'))
|
|
122
116
|
continue
|
|
123
|
-
|
|
117
|
+
refusals.at(
|
|
124
118
|
path,
|
|
125
|
-
|
|
119
|
+
new Error(
|
|
120
|
+
`abide: ${path} is directly in an address directory, which holds pages, layouts, error pages and handler files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
|
|
121
|
+
),
|
|
126
122
|
)
|
|
127
123
|
continue
|
|
128
124
|
}
|
|
129
125
|
if (!abide) files.handlers.push(path)
|
|
130
126
|
// 23.17 — anything that could answer here would answer in abide's own namespace.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
const framework = frameworkRouteRefusal(routeOf(path), path)
|
|
128
|
+
if (framework !== undefined) refusals.at(path, framework)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
// Any other place is a fence's, and the walk passed through it.
|
|
132
|
+
const fence = reserved as { path: string; name: string }
|
|
133
|
+
if (DIRECTORY_FILES.has(entry.name))
|
|
134
|
+
refusals.at(
|
|
134
135
|
path,
|
|
135
|
-
|
|
136
|
+
new Error(
|
|
137
|
+
`abide: ${path} is under the ${fence.name} directory ${fence.path}, where nothing has an address, and ${entry.name} is a route's name. RULEBOOK 23.18.`,
|
|
138
|
+
),
|
|
136
139
|
)
|
|
137
|
-
else if (
|
|
138
|
-
files.published.set(path, publicAddress(path,
|
|
139
|
-
else if (
|
|
140
|
-
(reserved.name === SERVER_DIRECTORY ||
|
|
141
|
-
reserved.name === LIB_DIRECTORY) &&
|
|
142
|
-
isModule(path)
|
|
143
|
-
)
|
|
140
|
+
else if (place === 'public')
|
|
141
|
+
files.published.set(path, publicAddress(path, fence.path))
|
|
142
|
+
else if ((place === 'server' || place === 'lib') && isModule(path))
|
|
144
143
|
files.modules.push(path)
|
|
145
144
|
}
|
|
146
145
|
}
|
|
@@ -157,17 +156,16 @@ function isModule(path: string): boolean {
|
|
|
157
156
|
)
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
/**
|
|
161
|
-
export function
|
|
159
|
+
/** The refusal of `source` where `route` lies in the framework's own namespace, or `undefined`. 23.17. */
|
|
160
|
+
export function frameworkRouteRefusal(
|
|
162
161
|
route: string,
|
|
163
162
|
source: string,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)
|
|
163
|
+
): Error | undefined {
|
|
164
|
+
return route === ABIDE_ADDRESS || route.startsWith(`${ABIDE_ADDRESS}/`)
|
|
165
|
+
? new Error(
|
|
166
|
+
`abide: ${source} answers at ${route}, which is under ${ABIDE_ADDRESS}, where abide's own addresses are. RULEBOOK 23.17.`,
|
|
167
|
+
)
|
|
168
|
+
: undefined
|
|
171
169
|
}
|
|
172
170
|
|
|
173
171
|
// 36.11 — the directories above `public`, then the file's own path below it.
|
|
@@ -191,19 +189,6 @@ export function publishedUnder(
|
|
|
191
189
|
return paths
|
|
192
190
|
}
|
|
193
191
|
|
|
194
|
-
/**
|
|
195
|
-
* Every `.abide` under `root`, relative to it, in path order — the app's own, and none a
|
|
196
|
-
* dependency ships.
|
|
197
|
-
*
|
|
198
|
-
* Refuses nothing: the language server lists them on every check, and a tree 23.16 forbids is the
|
|
199
|
-
* build's to refuse (`appFiles`).
|
|
200
|
-
*/
|
|
201
|
-
export function abideUnder(root: string): string[] {
|
|
202
|
-
const found: string[] = []
|
|
203
|
-
for (const path of abideFilesUnder(root, '')) found.push(path)
|
|
204
|
-
return found.sort()
|
|
205
|
-
}
|
|
206
|
-
|
|
207
192
|
/**
|
|
208
193
|
* Whether an app lies under `root`: a `.abide` anywhere or a handler file, stopping at the first.
|
|
209
194
|
*
|
|
@@ -211,15 +196,13 @@ export function abideUnder(root: string): string[] {
|
|
|
211
196
|
* is as cheap as the tree allows.
|
|
212
197
|
*/
|
|
213
198
|
export function holdsApp(root: string): boolean {
|
|
214
|
-
return !
|
|
199
|
+
return !appSourcesUnder(root, '').next().done
|
|
215
200
|
}
|
|
216
201
|
|
|
217
|
-
// Every `.abide` under `directory
|
|
218
|
-
function*
|
|
202
|
+
// Every `.abide` under `directory`, and every handler file — a module whose place is an address.
|
|
203
|
+
function* appSourcesUnder(
|
|
219
204
|
directory: string,
|
|
220
205
|
relative: string,
|
|
221
|
-
handlers = false,
|
|
222
|
-
fenced = false,
|
|
223
206
|
): Generator<string> {
|
|
224
207
|
let entries: Dirent[]
|
|
225
208
|
try {
|
|
@@ -231,15 +214,10 @@ function* abideFilesUnder(
|
|
|
231
214
|
if (skipped(entry.name)) continue
|
|
232
215
|
const path = `${relative}${entry.name}`
|
|
233
216
|
if (entry.isDirectory())
|
|
234
|
-
yield*
|
|
235
|
-
`${directory}/${entry.name}`,
|
|
236
|
-
`${path}/`,
|
|
237
|
-
handlers,
|
|
238
|
-
fenced || RESERVED_DIRECTORIES.has(entry.name),
|
|
239
|
-
)
|
|
217
|
+
yield* appSourcesUnder(`${directory}/${entry.name}`, `${path}/`)
|
|
240
218
|
else if (
|
|
241
219
|
path.endsWith('.abide') ||
|
|
242
|
-
(
|
|
220
|
+
(isModule(path) && placeOf(path) === 'address')
|
|
243
221
|
)
|
|
244
222
|
yield path
|
|
245
223
|
}
|