@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.
- package/CHANGELOG.md +18 -0
- package/docs/REGISTRY.md +38 -16
- package/docs/RULEBOOK.md +173 -12
- package/docs/abide.md +266 -24
- package/package.json +4 -2
- package/src/compiler/REACTIVE_MEMBERS.ts +1 -0
- package/src/compiler/classifyTemplate.ts +21 -14
- package/src/compiler/emitClient.ts +5 -1
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/IMAGE_TYPES.ts +10 -0
- package/src/server/OFFLINE_STALE.ts +10 -0
- package/src/server/answerRequest.ts +39 -10
- package/src/server/answerRpc.ts +25 -5
- package/src/server/answerWorker.ts +173 -28
- package/src/server/assistant.ts +81 -12
- package/src/server/claudeApi.ts +25 -20
- package/src/server/claudeCode.ts +30 -10
- package/src/server/createApp.ts +8 -2
- package/src/server/generatedSurfaces.ts +48 -14
- package/src/server/index.ts +2 -0
- package/src/server/render.ts +3 -2
- package/src/server/seedBuffer.ts +28 -9
- package/src/shared/KEEP_HEADER.ts +7 -0
- package/src/shared/KEEP_OPENED.ts +7 -0
- package/src/shared/KEPT_HEADER.ts +6 -0
- package/src/shared/LOAD_OFFLINE.ts +15 -0
- package/src/shared/OUTBOX_SENDING.ts +9 -0
- package/src/shared/REACHED_MESSAGE.ts +7 -0
- package/src/shared/answerOf.ts +14 -2
- package/src/shared/framing.ts +10 -2
- package/src/shared/index.ts +2 -0
- package/src/shared/outbox.ts +116 -0
- package/src/shared/reactive/Keyed.ts +9 -0
- package/src/shared/reactive/Reactive.ts +197 -3
- package/src/shared/reactive/SELECTION_MEMBERS.ts +1 -0
- package/src/shared/reactive/callLoad.ts +8 -0
- package/src/shared/reactive/index.ts +23 -1
- package/src/shared/rpc.ts +25 -5
- package/src/shared/rpcRequest.ts +12 -8
- package/src/ui/Router.ts +28 -7
- package/src/ui/bindFiles.ts +42 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/outboxQueue.ts +427 -0
- package/src/ui/socketWire.ts +31 -2
- 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 {
|
|
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
|
|
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(
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
*
|