@abide/abide 0.114.1 → 0.116.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 (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/docs/REGISTRY.md +38 -16
  3. package/docs/RULEBOOK.md +173 -12
  4. package/docs/abide.md +266 -24
  5. package/package.json +4 -2
  6. package/src/compiler/REACTIVE_MEMBERS.ts +1 -0
  7. package/src/compiler/classifyTemplate.ts +21 -14
  8. package/src/compiler/emitClient.ts +5 -1
  9. package/src/server/BRIDGE_VERSION.ts +1 -1
  10. package/src/server/IMAGE_TYPES.ts +10 -0
  11. package/src/server/OFFLINE_STALE.ts +10 -0
  12. package/src/server/answerRequest.ts +39 -10
  13. package/src/server/answerRpc.ts +25 -5
  14. package/src/server/answerWorker.ts +173 -28
  15. package/src/server/assistant.ts +81 -12
  16. package/src/server/claudeApi.ts +25 -20
  17. package/src/server/claudeCode.ts +30 -10
  18. package/src/server/createApp.ts +8 -2
  19. package/src/server/generatedSurfaces.ts +48 -14
  20. package/src/server/index.ts +2 -0
  21. package/src/server/render.ts +3 -2
  22. package/src/server/seedBuffer.ts +28 -9
  23. package/src/shared/KEEP_HEADER.ts +7 -0
  24. package/src/shared/KEEP_OPENED.ts +7 -0
  25. package/src/shared/KEPT_HEADER.ts +6 -0
  26. package/src/shared/LOAD_OFFLINE.ts +15 -0
  27. package/src/shared/OUTBOX_SENDING.ts +9 -0
  28. package/src/shared/REACHED_MESSAGE.ts +7 -0
  29. package/src/shared/answerOf.ts +14 -2
  30. package/src/shared/framing.ts +10 -2
  31. package/src/shared/index.ts +2 -0
  32. package/src/shared/outbox.ts +116 -0
  33. package/src/shared/reactive/Keyed.ts +9 -0
  34. package/src/shared/reactive/Reactive.ts +197 -3
  35. package/src/shared/reactive/SELECTION_MEMBERS.ts +1 -0
  36. package/src/shared/reactive/callLoad.ts +8 -0
  37. package/src/shared/reactive/index.ts +23 -1
  38. package/src/shared/rpc.ts +25 -5
  39. package/src/shared/rpcRequest.ts +12 -8
  40. package/src/ui/Router.ts +28 -7
  41. package/src/ui/bindFiles.ts +42 -0
  42. package/src/ui/index.ts +1 -0
  43. package/src/ui/outboxQueue.ts +427 -0
  44. package/src/ui/socketWire.ts +31 -2
  45. package/src/ui/wire.ts +111 -10
package/src/ui/wire.ts CHANGED
@@ -17,19 +17,28 @@
17
17
  // An entry of its own (`@abide/abide/wire`) rather than a name on the runtime barrel: only a generated
18
18
  // wrapper imports it, and a page with no handler pays nothing for it.
19
19
 
20
- import { type Answer, answered, answerOf } from '#shared/answerOf.ts'
20
+ import {
21
+ type Answer,
22
+ answered,
23
+ answerOf,
24
+ noAnswer,
25
+ unanswered,
26
+ } from '#shared/answerOf.ts'
21
27
  import type { CopyOptions } from '#shared/CopyOptions.ts'
22
28
  import { isFailed } from '#shared/Failed.ts'
23
29
  import { answerChunks } from '#shared/framing.ts'
24
30
  import { HandlerRoute } from '#shared/HandlerRoute.ts'
31
+ import type { JsonValue } from '#shared/JsonValue.ts'
32
+ import { LOAD_OFFLINE } from '#shared/LOAD_OFFLINE.ts'
25
33
  import { LOAD_SIGNAL } from '#shared/LOAD_SIGNAL.ts'
26
- import { reactive, structural } from '#shared/reactive/index.ts'
34
+ import { reactive, standIn, structural } from '#shared/reactive/index.ts'
27
35
  import {
28
36
  type Defaults,
29
37
  declareDefaults,
30
38
  } from '#shared/reactive/parameterDefaults.ts'
31
39
  import { declare, type Method } from '#shared/rpc.ts'
32
- import { rpcRequest } from '#shared/rpcRequest.ts'
40
+ import { type RpcRequest, rpcRequest } from '#shared/rpcRequest.ts'
41
+ import { enqueue, undelivered } from './outboxQueue.ts'
33
42
  import { followRpc } from './socketWire.ts'
34
43
 
35
44
  /** The browser's `Rpc` for a handler: the server's declaration over a body that fetches. */
@@ -43,18 +52,90 @@ export function wire(
43
52
  ): unknown {
44
53
  // 16.9 — the handler's route, compiled once for every call through the copy.
45
54
  const route = new HandlerRoute(address)
55
+ // 16.80 — the requests this page has had an answer to, whose next load asks past every cache.
56
+ // A first load is the worker's to answer from its copy, where one answers this page: anywhere
57
+ // else the cache below is the browser's, which would hand back a week-stale answer as the server's.
58
+ const answeredOnce = new Set<string>()
59
+ const requestOf = (args: Record<string, unknown> | undefined) => {
60
+ const request = rpcRequest(method, route, args)
61
+ if (
62
+ !answeredOnce.has(request.key) &&
63
+ navigator.serviceWorker?.controller
64
+ )
65
+ request.init.cache = 'force-cache'
66
+ return request
67
+ }
68
+ // 16.81 — WHAT STANDS IN FOR A `GET`'s ANSWER, per args, as the last call reaching them gave it:
69
+ // a reload no call starts — a refresh, the network coming back — has no `offline` of its own.
70
+ const standIns = new Map<string, (args: unknown) => unknown>()
46
71
  // ONE PARAMETER, so the value is keyed by args whatever the handler declared (16.6's arity rule):
47
72
  // an unkeyed handler is reached with no args, which is one key.
48
73
  const body = (args: Record<string, unknown> | undefined) => {
49
- // 16.49 — the load's own signal, set by the call starting it and read before anything awaits.
74
+ // 16.49, 16.81 — the load's own signal and stand-in, set by the call starting it and read
75
+ // before anything awaits.
50
76
  const signal = LOAD_SIGNAL.current
77
+ const request = requestOf(args)
78
+ let offline = LOAD_OFFLINE.current
79
+ // A reload no call started — a refresh, or the one `whenOnline` asks — that gets no answer
80
+ // waits before asking again, where asking at once would get none on every tick.
81
+ const retryIn = offline === undefined ? UNANSWERED_RETRY_MS : 0
82
+ if (method === 'GET') {
83
+ if (offline === undefined) offline = standIns.get(request.key)
84
+ else standIns.set(request.key, offline)
85
+ }
86
+ // A STAND-IN IS FOR A COPY HOLDING NOTHING: a reload that gets no answer over one this page
87
+ // already holds keeps serving it (7.4, 7.19) rather than trading it for a default.
88
+ const first = !answeredOnce.has(request.key)
89
+ // 16.82, 46.10 — a mutation that may be queued carries the key it is sent under every time.
90
+ const queued =
91
+ offline !== undefined && method !== 'GET'
92
+ ? queuedCall(request, address, args)
93
+ : undefined
94
+ let standing: { value: unknown } | undefined
95
+ if (queued !== undefined)
96
+ LOAD_OFFLINE.standing = standing = { value: undefined }
97
+ const stoodIn = (failed: Answer): unknown => {
98
+ const value = (offline as (args: unknown) => unknown)(args)
99
+ if (queued === undefined) {
100
+ whenOnline(args, retryIn)
101
+ return standIn(value)
102
+ }
103
+ return enqueue(queued, standing as { value: unknown }, value).then(
104
+ (kept) => (kept ? standIn(value) : answered(failed)),
105
+ )
106
+ }
107
+ // 16.83 — BEHIND CALLS NOT YET DELIVERED A CALL WAITS ITS TURN, rather than overtaking them.
108
+ if (queued !== undefined && undelivered() !== 0)
109
+ return Promise.resolve(stoodIn(noAnswer()))
51
110
  const settle = (answer: Answer): unknown => {
111
+ if (answer.ok) answeredOnce.add(request.key)
112
+ if (
113
+ offline !== undefined &&
114
+ unanswered(answer) &&
115
+ (first || queued !== undefined)
116
+ )
117
+ return stoodIn(answer)
52
118
  follow(args, answer)
119
+ // 16.85 — A COPY THE WORKER KEPT IS NOT THE SERVER'S ANSWER: held as written here, and
120
+ // asked of the network once there is one.
121
+ if (answer.kept) {
122
+ whenOnline(args, 0)
123
+ return standIn(answered(answer))
124
+ }
53
125
  return answered(answer)
54
126
  }
55
- const answer = answerOf(rpcRequest(method, route, args), false, signal)
127
+ const answer = answerOf(request, false, signal)
56
128
  return answer instanceof Promise ? answer.then(settle) : settle(answer)
57
129
  }
130
+ // 16.81, 16.85 — A COPY HOLDING A STAND-IN ASKS THE NETWORK AGAIN once there is one: now, past the
131
+ // landing (or `delay` past it), where the browser says it is online, and when it says so otherwise.
132
+ const whenOnline = (args: unknown, delay: number): void => {
133
+ if (method !== 'GET') return
134
+ const again = (): void =>
135
+ (value as (args: unknown) => { refresh(): void })(args).refresh()
136
+ if (navigator.onLine) setTimeout(again, delay)
137
+ else addEventListener('online', again, { once: true })
138
+ }
58
139
  // 16.32 — A STREAMING HANDLER'S COPY IS A STREAM OF ITS OWN, whose chunks land as the answer's
59
140
  // lines arrive: collected into one value, a page reading it showed nothing until the handler's
60
141
  // last chunk. A refusal is thrown, which is how a stream refuses (9.6).
@@ -64,11 +145,9 @@ export function wire(
64
145
  // 16.49 — read on the first line: the core's pump takes the first step inside the reach, so
65
146
  // this runs while the call that started the load still has its signal set.
66
147
  const signal = LOAD_SIGNAL.current
67
- const answer = await answerOf(
68
- rpcRequest(method, route, args),
69
- true,
70
- signal,
71
- )
148
+ const request = requestOf(args)
149
+ const answer = await answerOf(request, true, signal)
150
+ if (answer.ok) answeredOnce.add(request.key)
72
151
  follow(args, answer)
73
152
  if (answer.body !== undefined && answer.body !== null) {
74
153
  yield* answerChunks(answer.body, answer.framing)
@@ -112,6 +191,28 @@ export function wire(
112
191
  return rpc
113
192
  }
114
193
 
194
+ /** How long a reload that got no answer waits to ask again where the browser says it is online. */
195
+ const UNANSWERED_RETRY_MS = 5_000
196
+
197
+ /** A mutation's call as the outbox keeps it, under the key it is sent with every time (46.10). */
198
+ function queuedCall(
199
+ request: RpcRequest,
200
+ address: string,
201
+ args: Record<string, unknown> | undefined,
202
+ ): Parameters<typeof enqueue>[0] {
203
+ const id = crypto.randomUUID()
204
+ const headers = new Headers(request.init.headers)
205
+ headers.set('idempotency-key', id)
206
+ request.init.headers = headers
207
+ return {
208
+ id,
209
+ method: request.method,
210
+ address,
211
+ url: request.url,
212
+ args: args as Record<string, JsonValue> | undefined,
213
+ }
214
+ }
215
+
115
216
  /**
116
217
  * The build's copy options as a value takes them, `structural` resolved to its function.
117
218
  *