@abide/abide 0.124.1 → 0.125.1

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 (74) hide show
  1. package/ABIDE.md +4 -0
  2. package/CHANGELOG.md +50 -0
  3. package/bin/abide.ts +3 -2
  4. package/docs/REGISTRY.md +67 -40
  5. package/docs/RULEBOOK.md +206 -18
  6. package/docs/abide.md +393 -67
  7. package/package.json +1 -1
  8. package/src/compiler/appMap.ts +14 -1
  9. package/src/compiler/compileApp.ts +1 -0
  10. package/src/compiler/declaredExports.ts +41 -0
  11. package/src/compiler/handlersUnder.ts +1 -0
  12. package/src/compiler/hookCalls.ts +1 -0
  13. package/src/compiler/projectShadow.ts +52 -2
  14. package/src/compiler/readsIn.ts +39 -34
  15. package/src/server/PageRender.ts +7 -8
  16. package/src/server/abideModules.ts +22 -9
  17. package/src/server/addressed.ts +14 -0
  18. package/src/server/answerRequest.ts +115 -52
  19. package/src/server/answerSocket.ts +6 -5
  20. package/src/server/answerWorker.ts +34 -23
  21. package/src/server/assistant.ts +141 -35
  22. package/src/server/banner.ts +9 -5
  23. package/src/server/bridgeRelay.ts +3 -3
  24. package/src/server/callerOf.ts +5 -0
  25. package/src/server/claudeCode.ts +20 -11
  26. package/src/server/commands.ts +14 -7
  27. package/src/server/createApp.ts +65 -12
  28. package/src/server/devWorker.ts +18 -16
  29. package/src/server/faceOf.ts +3 -1
  30. package/src/server/generatedSurfaces.ts +6 -5
  31. package/src/server/handlerFileMount.ts +2 -0
  32. package/src/server/hooks.ts +54 -8
  33. package/src/server/index.ts +21 -4
  34. package/src/server/invokeHandler.ts +1 -1
  35. package/src/server/mountHandlers.ts +9 -7
  36. package/src/server/principal.ts +3 -2
  37. package/src/server/render.ts +11 -2
  38. package/src/server/requestScope.ts +61 -1
  39. package/src/server/responseRelay.ts +9 -3
  40. package/src/server/serveDev.ts +18 -3
  41. package/src/server/sqliteStore.ts +65 -0
  42. package/src/server/testMount.ts +1 -0
  43. package/src/shared/AssistantEvent.ts +40 -0
  44. package/src/shared/BrowserHooks.ts +73 -0
  45. package/src/shared/CURRENT_ROUTER.ts +3 -1
  46. package/src/shared/Issues.ts +11 -3
  47. package/src/shared/Middleware.ts +14 -7
  48. package/src/shared/UNEXPECTED.ts +1 -1
  49. package/src/shared/answerOf.ts +9 -4
  50. package/src/shared/imagePiece.ts +85 -0
  51. package/src/shared/index.ts +17 -0
  52. package/src/shared/isServer.ts +15 -0
  53. package/src/shared/navigate.ts +10 -3
  54. package/src/shared/onError.ts +51 -0
  55. package/src/shared/onRefusal.ts +47 -0
  56. package/src/shared/online.ts +5 -1
  57. package/src/shared/reactive/Keyed.ts +12 -1
  58. package/src/shared/reactive/Reactive.ts +53 -25
  59. package/src/shared/reactive/cellShape.ts +4 -3
  60. package/src/shared/reactive/poll.ts +49 -0
  61. package/src/shared/reactive/scoped.ts +10 -1
  62. package/src/shared/reactive/scoping.ts +14 -0
  63. package/src/shared/reactive/watch.ts +19 -8
  64. package/src/shared/resultsOf.ts +23 -0
  65. package/src/shared/route.ts +18 -4
  66. package/src/shared/rpc.ts +125 -30
  67. package/src/shared/seededDocument.ts +5 -6
  68. package/src/shared/storedValue.ts +32 -0
  69. package/src/shared/webStore.ts +54 -0
  70. package/src/ui/Router.ts +42 -9
  71. package/src/ui/outboxQueue.ts +1 -1
  72. package/src/ui/wire.ts +4 -4
  73. package/src/shared/KEEP_OPENED.ts +0 -7
  74. /package/src/{server → shared}/IMAGE_TYPES.ts +0 -0
@@ -0,0 +1,23 @@
1
+ import type { AssistantEvent } from './AssistantEvent.ts'
2
+ import type { Reactive } from './reactive/Reactive.ts'
3
+ import type { Rpc } from './rpc.ts'
4
+
5
+ /** What every call to `handler` in `events` answered, in order, a failed call's left out. 44.51. */
6
+ export function resultsOf<Value>(
7
+ events: readonly AssistantEvent[],
8
+ // Any `Rpc`, whatever its `Args`: only the value is read off its call.
9
+ handler: ((...args: never) => Reactive<Value, never, unknown, unknown>) &
10
+ Pick<Rpc<Value, never>, 'method' | 'address'>,
11
+ ): Value[] {
12
+ const values: Value[] = []
13
+ for (const event of events)
14
+ if (
15
+ event.type === 'result' &&
16
+ !event.failed &&
17
+ event.handler !== undefined &&
18
+ event.handler.method === handler.method &&
19
+ event.handler.address === handler.address
20
+ )
21
+ values.push(event.value as Value)
22
+ return values
23
+ }
@@ -1,4 +1,4 @@
1
- // 23.1 — WHERE WE ARE, as four values rather than one over a composite, so a reader of `params`
1
+ // 23.1 — WHERE WE ARE, as five values rather than one over a composite, so a reader of `params`
2
2
  // does not wake for a hash change and a progress bar reading `navigating` does not wake for either.
3
3
  //
4
4
  // FILLED BY WHOEVER RESOLVES THE ADDRESS: the client router per navigation, and a server's page lane
@@ -7,23 +7,37 @@
7
7
  import type { Params } from './matchRoute.ts'
8
8
  import { ambient } from './reactive/index.ts'
9
9
 
10
- /** The current address: its `url`, `params`, pattern `name`, and whether it is `navigating`. */
10
+ /**
11
+ * The current address: its `url`, `params`, pattern `name`, its `index` in this document's history,
12
+ * and whether it is `navigating`.
13
+ */
11
14
  export const route = {
12
15
  url: ambient<URL>(),
13
16
  params: ambient<Params>(),
14
17
  /** 23.6 — the resolution path: the literal the address resolved to, `/invoices/[id]`. */
15
18
  name: ambient<string>(),
19
+ /**
20
+ * 23.28, 23.29 — the entry's place in the history this app's router wrote: 0 where a document
21
+ * load arrived, and one past the entry it was pushed from.
22
+ */
23
+ index: ambient(0),
16
24
  navigating: ambient(false),
17
25
  }
18
26
 
19
27
  /**
20
- * Sets `route`'s url, params and name to an address just resolved.
28
+ * Sets `route`'s url, params, name and index to an address just resolved.
21
29
  *
22
30
  * 23.2 — ONE `URL` PER NAVIGATION, built here as the route resolves, so a reader holding the last
23
31
  * one is not handed a value that moved under it.
24
32
  */
25
- export function arrive(at: string, pattern: string, params: Params): void {
33
+ export function arrive(
34
+ at: string,
35
+ pattern: string,
36
+ params: Params,
37
+ index: number,
38
+ ): void {
26
39
  route.url.set(new URL(at))
27
40
  route.params.set(params)
28
41
  route.name.set(pattern)
42
+ route.index.set(index)
29
43
  }
package/src/shared/rpc.ts CHANGED
@@ -122,7 +122,7 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
122
122
  url(...args: undefined extends Args ? [args?: Args] : [args: Args]): string
123
123
  /** The same call, handed back as the `Response` (16.42). */
124
124
  raw(args?: Args, init?: RequestInit): Promise<Response>
125
- /** Where the handler is mounted, stamped by whoever mounted it — `undefined` until then. */
125
+ /** Where the handler answers: its route under the mount, once its file has loaded (16.88). */
126
126
  address: string | undefined
127
127
  keyed: boolean
128
128
  timeout: number | undefined
@@ -183,9 +183,51 @@ export type RpcContext<Args = unknown> = { request: Request; args: () => Args }
183
183
  */
184
184
  const SENT: unique symbol = Symbol('abide.sent')
185
185
 
186
- export type RpcOptions<Args = unknown> = {
186
+ /**
187
+ * What a rung's `next` answers as a declaration types it: opaque, so what the rung answers in the
188
+ * handler's place is told apart from the answer it passes on.
189
+ */
190
+ declare const REACHED: unique symbol
191
+ type Reached = { readonly [REACHED]: true }
192
+
193
+ /**
194
+ * The refusals a declaration's rungs answer with, read off what each answers.
195
+ *
196
+ * 16.86 — A REFUSAL A RUNG NAMES IS THE `Rpc`'s, and one it does not name is not: a `Failed` whose
197
+ * name is `string` (a `Middleware` declaring no refusals) would widen every name the union narrows
198
+ * by, and a rung answering `unknown` names none.
199
+ */
200
+ type RungRefusals<Answers extends readonly unknown[]> =
201
+ Answers[number] extends infer Answer
202
+ ? Answer extends Failed<infer Name, unknown>
203
+ ? string extends Name
204
+ ? never
205
+ : Answer
206
+ : never
207
+ : never
208
+
209
+ /** One rung of a handler's `middleware`, answering `Answer` in the handler's place or passing on. */
210
+ type HandlerRung<Args, Answer> = (
211
+ next: (ctx?: RpcContext<Args>) => Promise<Reached>,
212
+ ctx: RpcContext<Args>,
213
+ ) => Answer | Promise<Answer>
214
+
215
+ export type RpcOptions<
216
+ Args = unknown,
217
+ Answers extends readonly unknown[] = unknown[],
218
+ > = {
187
219
  description?: string
188
- middleware?: Rung<RpcContext<Args>, unknown>[]
220
+ /**
221
+ * 16.86 — one rung per answer in `Answers`, each read in context: a rung's own refusals are
222
+ * inferred where the list is written at the declaration.
223
+ *
224
+ * The plain array arm is the context a list built by a GENERIC helper is typed in: the mapped
225
+ * arm is over `Answers`, still being inferred, so the helper's own `Args` read nothing there and
226
+ * are `unknown`. It adds no refusal of its own — `Answers` is still read off the list.
227
+ */
228
+ middleware?:
229
+ | { [At in keyof Answers]: HandlerRung<Args, Answers[At]> }
230
+ | readonly HandlerRung<Args, unknown>[]
189
231
  timeout?: number
190
232
  clients?: Clients
191
233
  crossOrigin?: boolean | string[]
@@ -221,20 +263,34 @@ type Held<Accepted> = Accepted extends
221
263
  * which is why a value handed in takes the address's alone: it has decided its own.
222
264
  */
223
265
  type Declare = {
224
- <Stored, Args, Accepted, Failures, Produced>(
266
+ <
267
+ Stored,
268
+ Args,
269
+ Accepted,
270
+ Failures,
271
+ Produced,
272
+ Answers extends readonly unknown[] = unknown[],
273
+ >(
225
274
  handler: Keyed<Stored, Args, Accepted, Failures, Produced>,
226
- options?: RpcOptions<Args>,
227
- ): Rpc<Stored, Args, Failures, Produced>
228
- <Stored, Accepted, Failures, Produced>(
275
+ options?: RpcOptions<Args, Answers>,
276
+ ): Rpc<Stored, Args, Failures | RungRefusals<Answers>, Produced>
277
+ <
278
+ Stored,
279
+ Accepted,
280
+ Failures,
281
+ Produced,
282
+ Answers extends readonly unknown[] = unknown[],
283
+ >(
229
284
  handler: Reactive<Stored, Accepted, Failures, Produced>,
230
- options?: RpcOptions<undefined>,
231
- ): Rpc<Stored, undefined, Failures, Produced>
285
+ options?: RpcOptions<undefined, Answers>,
286
+ ): Rpc<Stored, undefined, Failures | RungRefusals<Answers>, Produced>
232
287
  // 11.2 — a stream whose parameter is optional is keyed, for `reactive`'s reason.
233
288
  <
234
289
  Body extends (args?: never) => unknown,
235
290
  Shaped = ChunkOf<Body>[],
236
291
  Gate = Schema<ChunkOf<Body>[]>,
237
292
  ArgsGate = Schema<Parameters<Body>[0]>,
293
+ Answers extends readonly unknown[] = unknown[],
238
294
  >(
239
295
  handler: Body & OptionalParameter<Body> & Streams<Body> & Plain,
240
296
  options?: Shaping<
@@ -244,36 +300,49 @@ type Declare = {
244
300
  Gate,
245
301
  ArgsGate
246
302
  > &
247
- RpcOptions<Parameters<Body>[0]>,
303
+ RpcOptions<Parameters<Body>[0], Answers>,
248
304
  ): Rpc<
249
305
  Kept<Shaped>,
250
306
  Parameters<Body>[0],
251
- Refusals<Shaped, Gate, ChunkOf<Body>[], ArgsGate, Parameters<Body>[0]>,
307
+ | Refusals<Shaped, Gate, ChunkOf<Body>[], ArgsGate, Parameters<Body>[0]>
308
+ | RungRefusals<Answers>,
252
309
  ChunkOf<Body>
253
310
  >
254
- <Chunk, Shaped = Chunk[], Gate = Schema<Chunk[]>>(
311
+ <
312
+ Chunk,
313
+ Shaped = Chunk[],
314
+ Gate = Schema<Chunk[]>,
315
+ Answers extends readonly unknown[] = unknown[],
316
+ >(
255
317
  handler: (() =>
256
318
  AsyncGenerator<Chunk> | Promise<AsyncGenerator<Chunk>>) &
257
319
  Plain,
258
- options?: Shaping<Chunk[], Shaped, never, Gate> & RpcOptions<undefined>,
259
- ): Rpc<Kept<Shaped>, undefined, Refusals<Shaped, Gate, Chunk[]>, Chunk>
320
+ options?: Shaping<Chunk[], Shaped, never, Gate> &
321
+ RpcOptions<undefined, Answers>,
322
+ ): Rpc<
323
+ Kept<Shaped>,
324
+ undefined,
325
+ Refusals<Shaped, Gate, Chunk[]> | RungRefusals<Answers>,
326
+ Chunk
327
+ >
260
328
  <
261
329
  Chunk,
262
330
  Args,
263
331
  Shaped = Chunk[],
264
332
  Gate = Schema<Chunk[]>,
265
333
  ArgsGate = Schema<Args>,
334
+ Answers extends readonly unknown[] = unknown[],
266
335
  >(
267
336
  handler: ((
268
337
  args: Args,
269
338
  ) => AsyncGenerator<Chunk> | Promise<AsyncGenerator<Chunk>>) &
270
339
  Plain,
271
340
  options?: Shaping<Chunk[], Shaped, Args, Gate, ArgsGate> &
272
- RpcOptions<Args>,
341
+ RpcOptions<Args, Answers>,
273
342
  ): Rpc<
274
343
  Kept<Shaped>,
275
344
  Args,
276
- Refusals<Shaped, Gate, Chunk[], ArgsGate, Args>,
345
+ Refusals<Shaped, Gate, Chunk[], ArgsGate, Args> | RungRefusals<Answers>,
277
346
  Chunk
278
347
  >
279
348
  // 11.2 — an optional parameter is keyed, and ahead of the arm below for `reactive`'s reason.
@@ -282,6 +351,7 @@ type Declare = {
282
351
  Shaped = Awaited<ReturnType<Body>>,
283
352
  Gate = Schema<Awaited<ReturnType<Body>>>,
284
353
  ArgsGate = Schema<Parameters<Body>[0]>,
354
+ Answers extends readonly unknown[] = unknown[],
285
355
  >(
286
356
  handler: Body & OptionalParameter<Body> & Plain,
287
357
  options?: Shaping<
@@ -291,7 +361,7 @@ type Declare = {
291
361
  Gate,
292
362
  ArgsGate
293
363
  > &
294
- RpcOptions<Parameters<Body>[0]>,
364
+ RpcOptions<Parameters<Body>[0], Answers>,
295
365
  ): Rpc<
296
366
  Kept<Shaped>,
297
367
  Parameters<Body>[0],
@@ -302,42 +372,63 @@ type Declare = {
302
372
  Awaited<ReturnType<Body>>,
303
373
  ArgsGate,
304
374
  Parameters<Body>[0]
305
- >,
375
+ >
376
+ | RungRefusals<Answers>,
306
377
  AdoptedProduced<ReturnType<Body>, Kept<Shaped>>
307
378
  >
308
- <Computed, Shaped = Awaited<Computed>, Gate = Schema<Awaited<Computed>>>(
379
+ <
380
+ Computed,
381
+ Shaped = Awaited<Computed>,
382
+ Gate = Schema<Awaited<Computed>>,
383
+ Answers extends readonly unknown[] = unknown[],
384
+ >(
309
385
  handler: (() => Computed) & Plain,
310
386
  options?: Shaping<Awaited<Computed>, Shaped, never, Gate> &
311
- RpcOptions<undefined>,
387
+ RpcOptions<undefined, Answers>,
312
388
  ): Rpc<
313
389
  Kept<Shaped>,
314
390
  undefined,
315
- AdoptedFailures<Computed> | Refusals<Shaped, Gate, Awaited<Computed>>,
391
+ | AdoptedFailures<Computed>
392
+ | Refusals<Shaped, Gate, Awaited<Computed>>
393
+ | RungRefusals<Answers>,
316
394
  AdoptedProduced<Computed, Kept<Shaped>>
317
395
  >
396
+ // 16.1 — a value, `reactive`'s value overload given an address. Ahead of the arm below rather
397
+ // than last, so a body refused for its options is reported by that arm, naming them, and not as
398
+ // a value it never was: `Held` already turns every function away.
399
+ <
400
+ Accepted,
401
+ Shaped = Accepted,
402
+ Gate = Schema<Accepted>,
403
+ Answers extends readonly unknown[] = unknown[],
404
+ >(
405
+ handler: Held<Accepted>,
406
+ options?: Shaping<Accepted, Shaped, never, Gate> &
407
+ RpcOptions<undefined, Answers>,
408
+ ): Rpc<
409
+ Kept<Shaped>,
410
+ undefined,
411
+ Refusals<Shaped, Gate, Accepted> | RungRefusals<Answers>
412
+ >
318
413
  <
319
414
  Computed,
320
415
  Args,
321
416
  Shaped = Awaited<Computed>,
322
417
  Gate = Schema<Awaited<Computed>>,
323
418
  ArgsGate = Schema<Args>,
419
+ Answers extends readonly unknown[] = unknown[],
324
420
  >(
325
421
  handler: ((args: Args) => Computed) & Plain,
326
422
  options?: Shaping<Awaited<Computed>, Shaped, Args, Gate, ArgsGate> &
327
- RpcOptions<Args>,
423
+ RpcOptions<Args, Answers>,
328
424
  ): Rpc<
329
425
  Kept<Shaped>,
330
426
  Args,
331
427
  | AdoptedFailures<Computed>
332
- | Refusals<Shaped, Gate, Awaited<Computed>, ArgsGate, Args>,
428
+ | Refusals<Shaped, Gate, Awaited<Computed>, ArgsGate, Args>
429
+ | RungRefusals<Answers>,
333
430
  AdoptedProduced<Computed, Kept<Shaped>>
334
431
  >
335
- // 16.1 — a value, `reactive`'s value overload given an address. Last, so a function is a body.
336
- <Accepted, Shaped = Accepted, Gate = Schema<Accepted>>(
337
- handler: Held<Accepted>,
338
- options?: Shaping<Accepted, Shaped, never, Gate> &
339
- RpcOptions<undefined>,
340
- ): Rpc<Kept<Shaped>, undefined, Refusals<Shaped, Gate, Accepted>>
341
432
  }
342
433
 
343
434
  /** What a handler's selection members answer before a call has built its value: no entries' answers. */
@@ -379,7 +470,11 @@ export function declare(method: Method): Declare {
379
470
  // 16.11, 16.12 — THE RUNGS, IN REGISTRATION ORDER, around the reach. Built once, at the
380
471
  // declaration (16.14); a value keyed by args, so a second call with the same args in one
381
472
  // request joins the first (1.8) and a server files one per request (11.32).
382
- const rungs = options?.middleware ?? []
473
+ // `Reached` is the declaration's view of `next`; at run time it answers the handler's value.
474
+ const rungs = (options?.middleware ?? []) as readonly Rung<
475
+ RpcContext,
476
+ unknown
477
+ >[]
383
478
  // A rung that handed `next` a context of its own making chose those args.
384
479
  const argsOf = (ctx: RpcContext): unknown =>
385
480
  SENT in ctx ? (ctx as { [SENT]: unknown })[SENT] : ctx.args()
@@ -22,13 +22,12 @@ export function seededDocument<Document>(
22
22
  | Document
23
23
  | Failed<string, unknown>
24
24
  | Promise<Document | Failed<string, unknown>> {
25
- const answer = answerOf(
26
- rpcRequest('GET', mounted(address), undefined),
27
- false,
28
- undefined,
29
- )
25
+ const request = rpcRequest('GET', mounted(address), undefined)
26
+ const answer = answerOf(request, false, undefined)
30
27
  return (
31
- answer instanceof Promise ? answer.then(answered) : answered(answer)
28
+ answer instanceof Promise
29
+ ? answer.then((settled) => answered(settled, request))
30
+ : answered(answer, request)
32
31
  ) as
33
32
  | Document
34
33
  | Failed<string, unknown>
@@ -0,0 +1,32 @@
1
+ import { isRecord } from './isRecord.ts'
2
+ import { runSchema, type Schema } from './Schema.ts'
3
+
4
+ /** The deadline a value written now with `ttl` runs out at, `null` where it never does (8.26). */
5
+ export function expiresAfter(ttl: number): number | null {
6
+ return Number.isFinite(ttl) ? Date.now() + ttl : null
7
+ }
8
+
9
+ /**
10
+ * The value a shipped store read back, `undefined` where its `ttl` has passed.
11
+ *
12
+ * 8.27 — THE `schema` A RESTORE SKIPS (8.5) RUNS HERE, on what was read: a refusal throws, which
13
+ * the value takes as a miss and warns about (8.15) — an entry an older build wrote is one an app
14
+ * wants to hear about rather than serve. Shared by `webStore` and the server's `sqliteStore`.
15
+ */
16
+ export function storedValue(
17
+ kept: unknown,
18
+ schema: Schema<unknown> | undefined,
19
+ where: string,
20
+ ): unknown {
21
+ if (!isRecord(kept) || !('value' in kept))
22
+ throw new Error(`${where} holds something no abide store wrote.`)
23
+ const expires = kept.expires
24
+ if (typeof expires === 'number' && expires <= Date.now()) return undefined
25
+ if (schema === undefined) return kept.value
26
+ const checked = runSchema(schema, kept.value)
27
+ if (checked.issues !== null)
28
+ throw new Error(
29
+ `${where} holds a value its schema refuses: ${JSON.stringify(checked.issues)}`,
30
+ )
31
+ return checked.value
32
+ }
@@ -0,0 +1,54 @@
1
+ import type { Store } from './reactive/index.ts'
2
+ import type { Schema } from './Schema.ts'
3
+ import { expiresAfter, storedValue } from './storedValue.ts'
4
+
5
+ /**
6
+ * A `Store` kept in this browser's `localStorage`, or its `sessionStorage`, under `key`.
7
+ *
8
+ * 8.23 — WHERE THERE IS NO WEB STORAGE, A SERVER, THE STORE IS INERT: every `get` a miss and every
9
+ * `set` nothing, so the value a server renders is the one it would render with no store (8.3).
10
+ * The storage is looked up on each call rather than held, since a browser that refuses storage
11
+ * throws on the lookup — and a throw there is 8.15's miss and 8.12's warning, not a broken module.
12
+ */
13
+ export function webStore<Stored>(
14
+ key: string,
15
+ options?: { area?: 'local' | 'session'; schema?: Schema<Stored> },
16
+ ): Store<Stored> {
17
+ const name = options?.area === 'session' ? 'sessionStorage' : 'localStorage'
18
+ const schema = options?.schema as Schema<unknown> | undefined
19
+ const where = `${name}[${JSON.stringify(key)}]`
20
+ const area = (): Storage | undefined =>
21
+ (globalThis as unknown as Record<string, Storage | undefined>)[name]
22
+ return {
23
+ get: () => {
24
+ const storage = area()
25
+ if (storage === undefined) return undefined
26
+ const text = storage.getItem(key)
27
+ if (text === null) return undefined
28
+ const value = storedValue(JSON.parse(text), schema, where)
29
+ // 8.26 — an entry past its deadline is gone, not merely skipped.
30
+ if (value === undefined) storage.removeItem(key)
31
+ return value as Stored | undefined
32
+ },
33
+ set: (value, retention) => {
34
+ area()?.setItem(
35
+ key,
36
+ JSON.stringify({ value, expires: expiresAfter(retention.ttl) }),
37
+ )
38
+ },
39
+ // 8.24 — another document's write arrives as a `storage` event; this document's own never does.
40
+ watch: (changed) => {
41
+ if (typeof globalThis.addEventListener !== 'function')
42
+ return () => {}
43
+ const listener = (event: StorageEvent): void => {
44
+ if (
45
+ event.storageArea === area() &&
46
+ (event.key === key || event.key === null)
47
+ )
48
+ changed()
49
+ }
50
+ globalThis.addEventListener('storage', listener)
51
+ return () => globalThis.removeEventListener('storage', listener)
52
+ },
53
+ }
54
+ }
package/src/ui/Router.ts CHANGED
@@ -105,7 +105,12 @@ export class Router {
105
105
  * nothing hydrates over it, and leaving it is a document load.
106
106
  */
107
107
  private readonly errorPage: boolean
108
- /** The place in this document's history of the entry the address bar stands at (`Entry`). */
108
+ /**
109
+ * The place in this document's history of the entry the address bar stands at (`Entry`).
110
+ *
111
+ * 23.28 — `route.index` IS THIS, moved where `route.url` is (`arrive`) rather than here: a back
112
+ * step moves this before the page it arrives at has loaded.
113
+ */
109
114
  private index: number
110
115
  /**
111
116
  * 30.29 — the scroll each entry was left at, by its `index`.
@@ -127,6 +132,14 @@ export class Router {
127
132
  * Read off the compiled `<style>` blocks. Nothing here looks at the document's stylesheets.
128
133
  */
129
134
  private readonly transitions: boolean
135
+ /**
136
+ * 30.38 — what settles each `step` still waiting on the browser to say it has arrived.
137
+ *
138
+ * Settled ALL AT ONCE by the next arrival, rather than one per `popstate`: a browser that folds
139
+ * two queued `history.go`s into one traversal fires one event, and a step waiting for a second
140
+ * would never settle.
141
+ */
142
+ private readonly stepping: (() => void)[]
130
143
 
131
144
  constructor(region: Region, routes: Compiled[], transitions: boolean) {
132
145
  this.region = region
@@ -146,6 +159,7 @@ export class Router {
146
159
  this.scrolls = []
147
160
  this.prefetched = new Set()
148
161
  this.transitions = transitions
162
+ this.stepping = []
149
163
  }
150
164
 
151
165
  /**
@@ -199,12 +213,31 @@ export class Router {
199
213
  * within the standing page, back over an in-page `#anchor` jump, is `follow`'s: nothing asked,
200
214
  * nothing remounted. 30.29 — the scroll is put back once the page it belongs to is mounted.
201
215
  */
202
- popped(): Promise<void> {
203
- return this.travel(
204
- location.href,
205
- { replace: true, keepScroll: true },
206
- this.stepped(),
207
- )
216
+ async popped(): Promise<void> {
217
+ try {
218
+ await this.travel(
219
+ location.href,
220
+ { replace: true, keepScroll: true },
221
+ this.stepped(),
222
+ )
223
+ } finally {
224
+ for (const arrived of this.stepping.splice(0)) arrived()
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Steps the history by `by` entries, settling once the entry stepped to is arrived at.
230
+ *
231
+ * 30.38 — THE BROWSER MOVES THE BAR LATER and says so only by `popstate`, so the step settles in
232
+ * `popped`, once the arriving page is mounted and its scroll put back. 30.39 — `history.go(0)`
233
+ * reloads the document, which no `navigate` does, so a step of none is over before it starts.
234
+ */
235
+ step(by: number): Promise<void> {
236
+ if (by === 0) return Promise.resolve()
237
+ return new Promise((arrived) => {
238
+ this.stepping.push(arrived)
239
+ history.go(by)
240
+ })
208
241
  }
209
242
 
210
243
  /** `go`, arriving as `step` says: a push, or a history step with the scroll it restores. */
@@ -313,7 +346,7 @@ export class Router {
313
346
  options.replace === true || target.href === location.href,
314
347
  [scrollX, scrollY],
315
348
  )
316
- arrive(target.href, found.route.pattern, found.params)
349
+ arrive(target.href, found.route.pattern, found.params, this.index)
317
350
  this.mounted = target.pathname
318
351
  if (step.scroll !== undefined) scrollTo(step.scroll[0], step.scroll[1])
319
352
  else if (options.keepScroll !== true && target.hash !== '')
@@ -500,7 +533,7 @@ export class Router {
500
533
  * 23.1 — WRITTEN BEFORE THE ARRIVING PAGE'S SETUP, which reads it.
501
534
  */
502
535
  private arriveAt(target: URL, found: Found<Compiled>): void {
503
- arrive(target.href, found.route.pattern, found.params)
536
+ arrive(target.href, found.route.pattern, found.params, this.index)
504
537
  this.standing = found.route
505
538
  this.mounted = target.pathname
506
539
  }
@@ -186,7 +186,7 @@ async function sendLine(): Promise<void> {
186
186
  retryLater()
187
187
  return
188
188
  }
189
- const value = answeredValue(answer)
189
+ const value = answeredValue(answer, call)
190
190
  if (isFailed(value)) {
191
191
  call.error = problemOf(value)
192
192
  await shelf.put(call)
package/src/ui/wire.ts CHANGED
@@ -101,7 +101,7 @@ export function wire(
101
101
  return standIn(value)
102
102
  }
103
103
  return enqueue(queued, standing as { value: unknown }, value).then(
104
- (kept) => (kept ? standIn(value) : answered(failed)),
104
+ (kept) => (kept ? standIn(value) : answered(failed, request)),
105
105
  )
106
106
  }
107
107
  // 16.83 — BEHIND CALLS NOT YET DELIVERED A CALL WAITS ITS TURN, rather than overtaking them.
@@ -120,9 +120,9 @@ export function wire(
120
120
  // asked of the network once there is one.
121
121
  if (answer.kept) {
122
122
  whenOnline(args, 0)
123
- return standIn(answered(answer))
123
+ return standIn(answered(answer, request))
124
124
  }
125
- return answered(answer)
125
+ return answered(answer, request)
126
126
  }
127
127
  const answer = answerOf(request, false, signal)
128
128
  return answer instanceof Promise ? answer.then(settle) : settle(answer)
@@ -153,7 +153,7 @@ export function wire(
153
153
  yield* answerChunks(answer.body, answer.framing)
154
154
  return
155
155
  }
156
- const value = answered(answer)
156
+ const value = answered(answer, request)
157
157
  if (isFailed(value)) throw value
158
158
  if (Array.isArray(value)) yield* value
159
159
  }
@@ -1,7 +0,0 @@
1
- /**
2
- * The value of the offline worker's `abide-keep` header on the page a navigation opens, which then
3
- * hydrates and so is seeded as one (41.29, 45.23) — a page saved behind the one opened is not.
4
- *
5
- * A leaf, because the server writes it into the worker's script and reads it off the request.
6
- */
7
- export const KEEP_OPENED = 'open'
File without changes