@abide/abide 0.103.0 → 0.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +27 -27
  4. package/docs/RULEBOOK.md +68 -24
  5. package/docs/abide.md +31 -46
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +46 -89
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -25
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +12 -10
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +183 -478
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/projectShadow.ts +1 -1
  25. package/src/compiler/readSources.ts +27 -42
  26. package/src/compiler/readsIn.ts +5 -16
  27. package/src/compiler/resolverIn.ts +77 -0
  28. package/src/server/abideModules.ts +152 -146
  29. package/src/server/answerRequest.ts +18 -4
  30. package/src/server/answerSocket.ts +136 -76
  31. package/src/server/answerValue.ts +28 -15
  32. package/src/server/appTree.ts +59 -73
  33. package/src/server/claudeApi.ts +2 -5
  34. package/src/server/clientBundle.ts +198 -0
  35. package/src/server/coerceArgs.ts +1 -1
  36. package/src/server/commands.ts +93 -57
  37. package/src/server/createApp.ts +90 -16
  38. package/src/server/deferredBuilds.ts +175 -0
  39. package/src/server/devWorker.ts +107 -319
  40. package/src/server/faceOf.ts +120 -0
  41. package/src/server/fenceServer.ts +10 -18
  42. package/src/server/generatedSurfaces.ts +12 -53
  43. package/src/server/index.ts +3 -0
  44. package/src/server/placement.ts +167 -0
  45. package/src/server/remote.ts +14 -42
  46. package/src/server/render.ts +25 -17
  47. package/src/server/renderComponent.ts +9 -5
  48. package/src/server/requestScope.ts +10 -4
  49. package/src/server/responseRelay.ts +389 -0
  50. package/src/server/runCompiled.ts +9 -12
  51. package/src/server/runDesktop.ts +6 -9
  52. package/src/server/serveBuild.ts +26 -73
  53. package/src/server/serveDev.ts +19 -176
  54. package/src/server/streamEvent.ts +29 -0
  55. package/src/server/versionOf.ts +1 -2
  56. package/src/server/withAbideSources.ts +2 -1
  57. package/src/shared/HandlerRoute.ts +68 -0
  58. package/src/shared/REFUSAL_KEY.ts +5 -0
  59. package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
  60. package/src/shared/answerOf.ts +105 -78
  61. package/src/shared/filledPath.ts +1 -9
  62. package/src/shared/framing.ts +127 -128
  63. package/src/shared/queryText.ts +15 -7
  64. package/src/shared/reactive/Cursor.ts +69 -124
  65. package/src/shared/reactive/Reactive.ts +296 -162
  66. package/src/shared/reactive/callLoad.ts +4 -18
  67. package/src/shared/reactive/index.ts +11 -8
  68. package/src/shared/reactive/pairReactive.ts +1 -1
  69. package/src/shared/reactive/scoped.ts +4 -3
  70. package/src/shared/reactive/share.ts +11 -1
  71. package/src/shared/routePattern.ts +0 -110
  72. package/src/shared/rpc.ts +17 -24
  73. package/src/shared/rpcRequest.ts +18 -47
  74. package/src/shared/takeEarlyCall.ts +12 -37
  75. package/src/shared/url.ts +1 -1
  76. package/src/ui/Level.ts +276 -0
  77. package/src/ui/List.ts +17 -17
  78. package/src/ui/Router.ts +20 -87
  79. package/src/ui/applyHead.ts +92 -79
  80. package/src/ui/index.ts +4 -2
  81. package/src/ui/reconcile.ts +4 -110
  82. package/src/ui/rows.ts +123 -2
  83. package/src/ui/wire.ts +12 -27
  84. package/src/compiler/hooksCalled.ts +0 -35
  85. package/src/server/clientBuildOptions.ts +0 -54
  86. package/src/server/refuseAddressCollision.ts +0 -37
  87. package/src/shared/reactive/chunksOfStream.ts +0 -57
  88. package/src/ui/MOUNTING_LEVEL.ts +0 -18
@@ -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 { type Problem, problemOf } from '#shared/Failed.ts'
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 Peer = {
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.peer, { op: member, id: follower.id })
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.peer, { op: member, id: follower.id })
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
- function send(peer: Peer, frame: ServerFrame): void {
161
- peer.send(JSON.stringify(frame))
162
- }
163
-
164
- function failure(error: unknown): Problem {
165
- return problemOf(failureOf(error))
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 `peer`. */
184
- connect(request: Request, peer: Peer): MuxConnection {
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(this, request, peer)
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
- send(this.peer, {
271
- op: 'error',
360
+ this.peer.send(
272
361
  id,
273
- error: failure(
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
- send(this.peer, {
286
- op: 'error',
374
+ this.peer.send(
287
375
  id,
288
- error: failure(crossingRefusal(this.crossing, 'subscription')),
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
- send(this.peer, { op: 'error', id, error: failure(thrown) })
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
- // AN EVENT STREAM OPENS ON A COMMENT, so a proxy has bytes to pass before the first event.
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
- send(text) {
465
- controller.enqueue(encoder.encode(eventOf(text)))
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.peer, { op: 'invalidate', id: one.id })
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 valueTail: string | undefined
581
- // ONE BODY PER VERSION A FOLLOWER STANDS AT, since followers a burst left at different places
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 patchTails: Map<number, string> | undefined
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 patchTail = since === undefined ? undefined : patchTails?.get(since)
589
- if (patchTail === undefined && since !== undefined) {
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
- patchTail = `,"base":${since},"version":${version},"ops":${JSON.stringify(ops)}}`
593
- patchTails ??= new Map()
594
- patchTails.set(since, patchTail)
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 (patchTail === undefined) {
598
- if (valueTail === undefined) {
654
+ if (frame === undefined) {
655
+ if (valueFrame === undefined) {
599
656
  const held = (serving as { peek(): unknown }).peek()
600
- valueTail =
601
- held === undefined
602
- ? `,"version":${version}}`
603
- : `,"version":${version},"value":${JSON.stringify(held)}}`
657
+ valueFrame = new Frame(
658
+ 'value',
659
+ undefined,
660
+ version,
661
+ held === undefined ? undefined : JSON.stringify(held),
662
+ )
604
663
  }
605
- one.peer.send(`{"op":"value","id":${one.id}${valueTail}`)
606
- } else one.peer.send(`{"op":"patch","id":${one.id}${patchTail}`)
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-cache')
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
- if (framing === 'bytes')
173
- headers.set('content-type', 'application/octet-stream')
174
- else if (framing === 'jsonl') headers.set('content-type', lines)
175
- else sseHeaders(headers)
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(SSE_OPENER))
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(`data: ${JSON.stringify(chunk)}\n\n`),
277
+ ENCODER.encode(
278
+ streamEvent(
279
+ undefined,
280
+ undefined,
281
+ JSON.stringify(chunk),
282
+ ),
283
+ ),
271
284
  )
272
285
  },
273
286
  cancel() {