@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
@@ -55,6 +55,7 @@ export {
55
55
  // modules into the static closure of the entry every page imports.
56
56
  export { navigate } from './navigate.ts'
57
57
  export { online } from './online.ts'
58
+ export { outbox, type QueuedCall } from './outbox.ts'
58
59
  export { onMenu } from './onMenu.ts'
59
60
  export { type Principal, principal } from './principal.ts'
60
61
  // EMITTED CODE'S: a prop the child writes (33.6, 33.7).
@@ -93,6 +94,7 @@ export {
93
94
  type Shared,
94
95
  type Store,
95
96
  structural,
97
+ synced,
96
98
  type Transformer,
97
99
  watch,
98
100
  } from './reactive/index.ts'
@@ -0,0 +1,116 @@
1
+ // 46 — THE CALLS A BROWSER HAS NOT DELIVERED YET, asked of a selection as a probe is: `outbox()` for
2
+ // every one, `outbox(logCall)` and `logCall.outbox(pattern)` for one handler's. What it answers is a
3
+ // plain list, so a page counts, filters and walks it as it would any other, and each call carries
4
+ // what can be done with it.
5
+ //
6
+ // THE FACE, NOT THE QUEUE: the queue is the browser's (`#ui/outboxQueue.ts`), which the wire loads and
7
+ // which shows its line here through `showLine` and `showSending`. On a server nothing shows one, and
8
+ // `outbox` answers no calls (46.15).
9
+
10
+ import type { Failed } from './Failed.ts'
11
+ import type { JsonValue } from './JsonValue.ts'
12
+ import { ambient, type Reactive, type Selection } from './reactive/index.ts'
13
+ import { matchesPattern } from './reactive/matchesPattern.ts'
14
+ import { OUTBOX_SENDING } from './OUTBOX_SENDING.ts'
15
+ import type { Method } from './rpc.ts'
16
+
17
+ /** One call `outbox` holds: what it sends, when it was queued, how often it was sent, and its value. */
18
+ export type QueuedCall<Value = unknown> = {
19
+ /** The call's `idempotency-key`, the same on every send (46.10). */
20
+ id: string
21
+ method: Method
22
+ url: string
23
+ args: Record<string, JsonValue> | undefined
24
+ queuedAt: number
25
+ attempts: number
26
+ /**
27
+ * 46.18 — the value the call settled: its stand-in until the server answers, and its refusal as
28
+ * `error()` where the server refused it.
29
+ */
30
+ value: Reactive<Value, Value, Failed<string, unknown>>
31
+ /** 46.12 — sends it again, at once. */
32
+ retry(): void
33
+ /** 46.13 — drops it, sent or not. */
34
+ discard(): void
35
+ }
36
+
37
+ /** A call as the queue shows it: the handler it is for, which a selection is matched against. */
38
+ export type ShownCall = QueuedCall & { address: string }
39
+
40
+ /** What a handler's calls settle to, read off its own `outbox` member; `unknown` for any other. */
41
+ type ValueOf<Chosen> = Chosen extends {
42
+ outbox(pattern?: never): QueuedCall<infer Value>[]
43
+ }
44
+ ? Value
45
+ : unknown
46
+
47
+ /** A handler as a selection names it: its method and the address it is mounted at. */
48
+ type Handler = { method: Method; address: string | undefined }
49
+
50
+ /** A call as a selection is matched against it: its handler, and the args a pattern narrows. */
51
+ type Matched = Pick<ShownCall, 'method' | 'address' | 'args'>
52
+
53
+ const LINE = ambient<ShownCall[]>([])
54
+ /** The call in flight, or `undefined` where none is. */
55
+ const SENDING = ambient<Matched | undefined>(undefined)
56
+
57
+ // 46.11 — a bare `busy` hears a call in flight, which no value holds.
58
+ OUTBOX_SENDING.any = () => SENDING() !== undefined
59
+
60
+ /** 46.1 — shows the queue's line, oldest first. */
61
+ export function showLine(calls: ShownCall[]): void {
62
+ LINE.set(calls)
63
+ }
64
+
65
+ /** 46.11 — shows the call being sent, or that none is. */
66
+ export function showSending(call: Matched | undefined): void {
67
+ SENDING.set(call)
68
+ }
69
+
70
+ /**
71
+ * The calls waiting to be delivered and the ones refused, oldest first: every one, or the ones a
72
+ * handler names, narrowed by `pattern` as its own members narrow (46.1, 46.16).
73
+ */
74
+ export function outbox<Chosen extends Selection>(
75
+ selection?: Chosen,
76
+ pattern?: Record<string, unknown>,
77
+ ): QueuedCall<ValueOf<Chosen>>[] {
78
+ const line = LINE() as QueuedCall<ValueOf<Chosen>>[]
79
+ if (selection === undefined) return line
80
+ const handler = handlerOf(selection)
81
+ const named: QueuedCall<ValueOf<Chosen>>[] = []
82
+ if (handler === undefined) return named
83
+ for (const call of line)
84
+ if (forHandler(call as ShownCall, handler, pattern)) named.push(call)
85
+ return named
86
+ }
87
+
88
+ /** 46.11 — whether a call `outbox` holds for `handler`, matching `pattern`, is in flight. */
89
+ export function sendingFor(
90
+ handler: Handler,
91
+ pattern: Record<string, unknown> | undefined,
92
+ ): boolean {
93
+ const call = SENDING()
94
+ return call !== undefined && forHandler(call, handler, pattern)
95
+ }
96
+
97
+ function forHandler(
98
+ call: Matched,
99
+ handler: Handler,
100
+ pattern: Record<string, unknown> | undefined,
101
+ ): boolean {
102
+ return (
103
+ call.method === handler.method &&
104
+ call.address === handler.address &&
105
+ matchesPattern(call.args, pattern)
106
+ )
107
+ }
108
+
109
+ /** The handler a selection names, or `undefined` where it names none — a value, a key, or tags. */
110
+ function handlerOf(selection: object): Handler | undefined {
111
+ const handler = selection as Partial<Handler>
112
+ return typeof handler.method === 'string' &&
113
+ typeof handler.address === 'string'
114
+ ? (handler as Handler)
115
+ : undefined
116
+ }
@@ -66,6 +66,7 @@ export type Keyed<
66
66
  ) => Reactive<Stored, Accepted, Failures, Produced>) & {
67
67
  busy(pattern?: Partial<Args>): boolean
68
68
  ready(pattern?: Partial<Args>): boolean
69
+ synced(pattern?: Partial<Args>): boolean
69
70
  refresh(pattern?: Partial<Args>): void
70
71
  invalidate(pattern?: Partial<Args>): void
71
72
  /** The entries reached so far. Not app surface — the statics and the tests read it. */
@@ -462,6 +463,14 @@ export function createKeyed<
462
463
  return false
463
464
  return true
464
465
  },
466
+ // 13.18 — ALL, as `ready` asks: one entry written here is one the server has not seen.
467
+ synced: (pattern?: Partial<Args>): boolean => {
468
+ askMembership()
469
+ for (const entry of entries.values())
470
+ if (matchesPattern(argsOf(entry), pattern) && !entry.synced())
471
+ return false
472
+ return true
473
+ },
465
474
  refresh: (pattern?: Partial<Args>): void => {
466
475
  for (const entry of entries.values())
467
476
  if (matchesPattern(argsOf(entry), pattern)) entry.refresh()
@@ -225,6 +225,7 @@ export type Reactive<
225
225
  busy(): boolean
226
226
  ready(): boolean
227
227
  error(): Failures | undefined
228
+ synced(): boolean
228
229
  /**
229
230
  * Narrows a caught error to this value's failure of that name.
230
231
  *
@@ -731,6 +732,13 @@ class Cell {
731
732
  declare tags: string[] | null
732
733
  /** Whether `declare` built this value for a mutation, which a sweep invalidates (13.16). */
733
734
  declare mutation: boolean
735
+ /**
736
+ * Whether what is held was written here since the producer last produced (3.10).
737
+ *
738
+ * Only ever true on a value with a body: a value with no producer has nothing to be out of
739
+ * step with (3.12). Moved by `writtenHere` and `producedAgain` alone, each owning its probe move.
740
+ */
741
+ declare local: boolean
734
742
  /** `null` on every value declaring no `store`, which is what keeps `serve` from persisting. */
735
743
  declare store: Persisted | null
736
744
  /**
@@ -881,6 +889,7 @@ class Cell {
881
889
  : tags
882
890
  this.mutation =
883
891
  (options as { [MUTATION]?: true } | undefined)?.[MUTATION] === true
892
+ this.local = false
884
893
  const store = options?.store
885
894
  this.store =
886
895
  store === undefined
@@ -1518,6 +1527,9 @@ function drainPending(): void {
1518
1527
  * One production. A write that `identity` deems a repeat mints nothing and wakes nobody (5.2).
1519
1528
  */
1520
1529
  function produce(cell: Cell, produced: unknown): number {
1530
+ // 3.11 — A PRODUCTION THE PRODUCER MADE puts the value back in step; one a write here made is
1531
+ // `writtenHere`'s to report, after it lands.
1532
+ if (cell.local && WRITING === 0) producedAgain(cell)
1521
1533
  // 7.9: AN ACCEPTED PRODUCTION SETTLES THE RELOAD AN INVALIDATE OWED — ahead of the gate, since
1522
1534
  // a write 5.2 collapses was still accepted. Left standing, the next read unserved this value
1523
1535
  // and reloaded over it. A look above (`OWES_ABOVE`) is not this value's mark and stays: 11.8
@@ -2048,6 +2060,13 @@ function absorb(cell: Cell, result: unknown, answered: boolean): void {
2048
2060
  else if (isAsyncGenerator(settled)) {
2049
2061
  promoteToStream(cell, settled)
2050
2062
  probesMoved(cell)
2063
+ }
2064
+ // 3.10 — WHAT A WRITE HERE SETTLED TO, and a body's stand-in for an answer it could not
2065
+ // get (16.81), are held as written here rather than produced.
2066
+ else if (!answered) acceptHere(cell, settled)
2067
+ else if (settled instanceof StandIn) {
2068
+ acceptHere(cell, settled.value)
2069
+ writtenHere(cell)
2051
2070
  } else accept(cell, settled)
2052
2071
  },
2053
2072
  (failed) => {
@@ -2414,6 +2433,13 @@ function run(cell: Cell): void {
2414
2433
  promoteToStream(cell, produced)
2415
2434
  return
2416
2435
  }
2436
+ // 16.81 — a stand-in the body could hand back at once: an answer a page started early (41.21)
2437
+ // that the worker answered from a kept copy (16.85).
2438
+ if (!primitive && produced instanceof StandIn) {
2439
+ acceptHere(cell, produced.value)
2440
+ writtenHere(cell)
2441
+ return
2442
+ }
2417
2443
  if (
2418
2444
  cell.bareWrite &&
2419
2445
  typeof (produced as PromiseLike<unknown>).then !== 'function'
@@ -2654,6 +2680,7 @@ function beginAccumulation(cell: Cell, stream: Stream): void {
2654
2680
  * whatever `identity` says about its last element.
2655
2681
  */
2656
2682
  function acceptChunk(cell: Cell, stream: Stream, chunk: unknown): void {
2683
+ if (cell.local) producedAgain(cell)
2657
2684
  stream.chunks.push(chunk)
2658
2685
  cell.ordinal += 1
2659
2686
  // 3.8: `ready` from the first chunk, and `error` cleared by an accepted production (4.11).
@@ -3242,6 +3269,147 @@ function readyNow(cell: Cell): boolean {
3242
3269
  return walkReady(cell, PROBE_PASS)
3243
3270
  }
3244
3271
 
3272
+ /**
3273
+ * Whether this value and everything it reads or adopts holds what its producer made (3.10, 3.13).
3274
+ *
3275
+ * `ready`'s quantifier, and for `ready`'s reason: a figure computed from a value written here is no
3276
+ * more the producer's than that value is.
3277
+ */
3278
+ function walkSynced(cell: Cell, pass: number): boolean {
3279
+ if (cell.local) return false
3280
+ const adoption = cell.adoption
3281
+ if (cell.sources.length === 0 && adoption === null) return true
3282
+ if (cell.probePass === pass) return true
3283
+ cell.probePass = pass
3284
+ if (adoption !== null && !walkSynced(adoption.source, pass)) return false
3285
+ const sources = cell.sources
3286
+ for (let index = 0; index < sources.length; index += 1)
3287
+ if (!walkSynced(sources[index] as Cell, pass)) return false
3288
+ return true
3289
+ }
3290
+
3291
+ function syncedNow(cell: Cell): boolean {
3292
+ if (cell.local) return false
3293
+ if (cell.sources.length === 0 && cell.adoption === null) return true
3294
+ PROBE_PASS += 1
3295
+ return walkSynced(cell, PROBE_PASS)
3296
+ }
3297
+
3298
+ /**
3299
+ * How many writes made here are on the stack, which `produce` reads to tell one from a production.
3300
+ *
3301
+ * A COUNT RATHER THAN A FLAG, so a write made from inside another's gate — a `transform` writing a
3302
+ * second value — leaves the outer one's mark standing when it returns.
3303
+ */
3304
+ let WRITING = 0
3305
+
3306
+ /** 3.10 — marks a value with a producer as written here, telling the guards that asked. */
3307
+ function writtenHere(cell: Cell): void {
3308
+ if (cell.body === null || cell.local) return
3309
+ cell.local = true
3310
+ probesMoved(cell)
3311
+ }
3312
+
3313
+ /** 3.11 — puts a value back in step with its producer, telling the guards that asked. */
3314
+ function producedAgain(cell: Cell): void {
3315
+ if (!cell.local) return
3316
+ cell.local = false
3317
+ probesMoved(cell)
3318
+ }
3319
+
3320
+ /** `accept`, as a write made here rather than a production. */
3321
+ function acceptHere(cell: Cell, value: unknown): unknown {
3322
+ WRITING += 1
3323
+ try {
3324
+ return accept(cell, value)
3325
+ } finally {
3326
+ WRITING -= 1
3327
+ }
3328
+ }
3329
+
3330
+ /**
3331
+ * A write over a value with a body: held as written here where it moved what is held (3.10).
3332
+ *
3333
+ * A WRITE THE IDENTITY GATE COLLAPSED MOVED NOTHING, so it leaves the value as in step as it was; a
3334
+ * write that settles later is marked now, being a write here whenever it lands.
3335
+ */
3336
+ function setOverProducer(cell: Cell, value: unknown): unknown {
3337
+ const ordinal = cell.ordinal
3338
+ cell.generation += 1
3339
+ WRITING += 1
3340
+ let answer: unknown
3341
+ try {
3342
+ answer = setThroughGates(cell, value)
3343
+ } finally {
3344
+ WRITING -= 1
3345
+ }
3346
+ if (
3347
+ answer === undefined &&
3348
+ !isReactive(value) &&
3349
+ (cell.ordinal !== ordinal || isThenable(value))
3350
+ )
3351
+ writtenHere(cell)
3352
+ return answer
3353
+ }
3354
+
3355
+ /**
3356
+ * What a body hands back in place of an answer it could not get: held as written here (16.81).
3357
+ *
3358
+ * A CLASS, so the one test that tells it from a value is `instanceof`, made only on the arm that
3359
+ * settles a body's promise.
3360
+ */
3361
+ class StandIn {
3362
+ value: unknown
3363
+ constructor(value: unknown) {
3364
+ this.value = value
3365
+ }
3366
+ }
3367
+
3368
+ /** A body's stand-in for the answer it could not get, which `synced` reads false over (16.81). */
3369
+ export function standIn(value: unknown): unknown {
3370
+ return new StandIn(value)
3371
+ }
3372
+
3373
+ /**
3374
+ * Writes `value` as its producer made it, putting it back in step (3.11).
3375
+ *
3376
+ * WHAT A FOLLOWED COPY IS SENT IS THE PRODUCER'S, whoever carries it in: the server's value
3377
+ * arriving over the mux is a production of the copy, not a write made here (18.10).
3378
+ */
3379
+ export function produceInto(value: Reactive<unknown>, next: unknown): void {
3380
+ const cell = (value as unknown as Callable)[CELL]
3381
+ // A stream's write stands over its accumulation (9.11), as `STREAM_PROTOTYPE.set` makes it.
3382
+ if (ownStream(cell) !== null) writeStream(cell, next)
3383
+ else {
3384
+ cell.generation += 1
3385
+ setThroughGates(cell, next)
3386
+ }
3387
+ producedAgain(cell)
3388
+ }
3389
+
3390
+ /** `produceInto` for a patch the producer sent (18.11). */
3391
+ export function patchInto(
3392
+ value: Reactive<unknown>,
3393
+ mutate: (value: unknown) => void,
3394
+ ): void {
3395
+ const cell = (value as unknown as Callable)[CELL]
3396
+ patchCell(cell, mutate)
3397
+ producedAgain(cell)
3398
+ }
3399
+
3400
+ /**
3401
+ * Refuses `value` as its producer did, leaving what it holds standing (46.7): a call queued under a
3402
+ * stand-in that the server refused once it was delivered.
3403
+ */
3404
+ export function refuseInto(value: Reactive<unknown>, failure: unknown): void {
3405
+ refused((value as unknown as Callable)[CELL], failure)
3406
+ }
3407
+
3408
+ /** Whether `value` holds what was written here, without subscribing — for a caller that is not a reader. */
3409
+ export function writtenLocally(value: Reactive<unknown>): boolean {
3410
+ return (value as unknown as Callable)[CELL].local
3411
+ }
3412
+
3245
3413
  /**
3246
3414
  * A promise of the value once it is servable and not busy, or of its failure.
3247
3415
  *
@@ -4140,6 +4308,13 @@ export function readyOver(tags: string[] | null): boolean {
4140
4308
  return true
4141
4309
  }
4142
4310
 
4311
+ /** Whether every value in the selection is in step with its producer; a reader subscribes to it. */
4312
+ export function syncedOver(tags: string[] | null): boolean {
4313
+ trackSignal(tags)
4314
+ for (const cell of inReach(tags)) if (!syncedNow(cell)) return false
4315
+ return true
4316
+ }
4317
+
4143
4318
  /**
4144
4319
  * Invalidates every value in the selection, and what each adopts.
4145
4320
  *
@@ -4221,6 +4396,11 @@ const MEMBERS = {
4221
4396
  ready(this: Callable): boolean {
4222
4397
  return isReady(this[CELL])
4223
4398
  },
4399
+ synced(this: Callable): boolean {
4400
+ const cell = this[CELL]
4401
+ probe(cell)
4402
+ return syncedNow(cell)
4403
+ },
4224
4404
  error(this: Callable): unknown {
4225
4405
  const cell = this[CELL]
4226
4406
  // 3.3 ON THE THIRD PROBE. `error` is a probe by the rulebook's own term, so a body
@@ -4239,6 +4419,7 @@ const MEMBERS = {
4239
4419
  isError: isFailedNamed,
4240
4420
  set(this: Callable, value: unknown): unknown {
4241
4421
  const cell = this[CELL]
4422
+ if (cell.body !== null) return setOverProducer(cell, value)
4242
4423
  cell.generation += 1
4243
4424
  // The settled, untransformed write is the overwhelming common shape, and the
4244
4425
  // general path is two call frames and a thenable probe behind it. Inlined here
@@ -4257,7 +4438,9 @@ const MEMBERS = {
4257
4438
  return setThroughGates(cell, value)
4258
4439
  },
4259
4440
  patch(this: Callable, mutate: (value: unknown) => void): void {
4260
- patchCell(this[CELL], mutate)
4441
+ const cell = this[CELL]
4442
+ patchCell(cell, mutate)
4443
+ writtenHere(cell)
4261
4444
  },
4262
4445
  refresh(this: Callable): void {
4263
4446
  eachAdopted(this[CELL], refreshOne)
@@ -4514,7 +4697,15 @@ const STREAM_PROTOTYPE = brandedPrototype({
4514
4697
  if (cycle !== undefined) throw cycle
4515
4698
  return undefined
4516
4699
  }
4517
- return writeStream(cell, value)
4700
+ WRITING += 1
4701
+ let answer: unknown
4702
+ try {
4703
+ answer = writeStream(cell, value)
4704
+ } finally {
4705
+ WRITING -= 1
4706
+ }
4707
+ if (answer === undefined) writtenHere(cell)
4708
+ return answer
4518
4709
  },
4519
4710
  })
4520
4711
 
@@ -4536,7 +4727,7 @@ const ADOPTING_PROTOTYPE = brandedPrototype({
4536
4727
  },
4537
4728
  patch(this: Callable, mutate: (value: unknown) => void): void {
4538
4729
  const adoption = this[CELL].adoption
4539
- if (adoption === null) patchCell(this[CELL], mutate)
4730
+ if (adoption === null) MEMBERS.patch.call(this, mutate)
4540
4731
  else (adoption.source.callable as Reactive<unknown>).patch(mutate)
4541
4732
  },
4542
4733
  })
@@ -4587,6 +4778,9 @@ const SIMPLE_PROTOTYPE = brandedPrototype({
4587
4778
  }
4588
4779
  cell.value = value
4589
4780
  cell.ordinal += 1
4781
+ // 3.10 — a write over a producer is one `synced` hears. Never a production here, so no
4782
+ // `WRITING` count: the short write does not reach `produce`.
4783
+ if (cell.body !== null) writtenHere(cell)
4590
4784
  // `setDirect`, with `plain` known true by `simple`.
4591
4785
  cell.direct = cell.state === CLEAN
4592
4786
  if (cell.readers.length !== 0) markReaders(cell, DIRTY)
@@ -7,6 +7,7 @@
7
7
  export const SELECTION_MEMBERS = [
8
8
  'busy',
9
9
  'ready',
10
+ 'synced',
10
11
  'refresh',
11
12
  'invalidate',
12
13
  ] as const
@@ -7,6 +7,7 @@
7
7
  // ONE RECORD PER VALUE, IN A SIDE TABLE, not a field on the cell. Every value would carry the field
8
8
  // and only a called one ever fills it, so the table is what keeps a plain `reactive` paying nothing.
9
9
 
10
+ import { LOAD_OFFLINE } from '../LOAD_OFFLINE.ts'
10
11
  import { LOAD_SIGNAL } from '../LOAD_SIGNAL.ts'
11
12
  import { aborted, refuse } from '../refuse.ts'
12
13
  // Through the barrel, for the cycle it names: rpc reaches this module ahead of the barrel.
@@ -81,13 +82,18 @@ export function callLoad(
81
82
  args: unknown,
82
83
  signal: AbortSignal | undefined,
83
84
  timeout: number | undefined,
85
+ // 16.81 — read by a browser's wire as its body starts, and by nothing on a server (16.84).
86
+ offline?: (args: unknown) => unknown,
84
87
  ): unknown {
85
88
  const ending = signal === undefined ? undefined : new AbortController()
86
89
  LOAD_SIGNAL.current = ending?.signal
90
+ LOAD_OFFLINE.current = offline
87
91
  let reached: unknown
88
92
  let attended: Attended | undefined
89
93
  try {
90
94
  reached = reach(args)
95
+ const standing = LOAD_OFFLINE.standing
96
+ if (standing !== undefined) standing.value = reached
91
97
  attended = ATTENDED.get(reached as object)
92
98
  // 16.62 — the callers of an aborted load stopped waiting, and this one has not.
93
99
  // 15.19 — and of one whose last answer failed transiently, which runs again here where an
@@ -101,6 +107,8 @@ export function callLoad(
101
107
  (reached as { refresh(): void }).refresh()
102
108
  } finally {
103
109
  LOAD_SIGNAL.current = undefined
110
+ LOAD_OFFLINE.current = undefined
111
+ LOAD_OFFLINE.standing = undefined
104
112
  }
105
113
  if (!isReactive(reached)) return reached
106
114
  const load = loadOf(reached)
@@ -15,6 +15,7 @@
15
15
 
16
16
  import type { Failed } from '../Failed.ts'
17
17
  import type { Issues } from '../Issues.ts'
18
+ import { OUTBOX_SENDING } from '../OUTBOX_SENDING.ts'
18
19
  import { RPC_SERVER } from '../RPC_SERVER.ts'
19
20
  import type { Schema } from '../Schema.ts'
20
21
  import { isReactive } from './isReactive.ts'
@@ -29,6 +30,7 @@ import {
29
30
  type ReactiveOptions,
30
31
  readyOver,
31
32
  refreshOver,
33
+ syncedOver,
32
34
  } from './Reactive.ts'
33
35
  import { outsideScope, scoped } from './scoped.ts'
34
36
  import { share } from './share.ts'
@@ -50,13 +52,18 @@ export {
50
52
  isStreaming,
51
53
  loadOf,
52
54
  moveItem,
55
+ patchInto,
56
+ produceInto,
53
57
  type Reactive,
54
58
  type ReactiveOptions,
59
+ refuseInto,
55
60
  rowItem,
56
61
  type Store,
57
62
  servedCellOf,
63
+ standIn,
58
64
  streamingOrSettled,
59
65
  type Transformer,
66
+ writtenLocally,
60
67
  } from './Reactive.ts'
61
68
  export { STREAMING } from './STREAMING.ts'
62
69
  export {
@@ -82,6 +89,7 @@ export type Selection =
82
89
  | {
83
90
  busy(): boolean
84
91
  ready(): boolean
92
+ synced(): boolean
85
93
  refresh(): void
86
94
  invalidate(): void
87
95
  }
@@ -477,7 +485,7 @@ function build(
477
485
  // tag arm takes none at all.
478
486
  type PatternOf<
479
487
  Chosen,
480
- Member extends 'busy' | 'ready' | 'refresh' | 'invalidate',
488
+ Member extends 'busy' | 'ready' | 'synced' | 'refresh' | 'invalidate',
481
489
  > =
482
490
  Chosen extends Record<Member, (pattern?: infer Pattern) => unknown>
483
491
  ? Pattern
@@ -497,6 +505,9 @@ export function busy<Chosen extends Selection>(
497
505
  pattern?: PatternOf<Chosen, 'busy'>,
498
506
  ): boolean {
499
507
  const sweep = sweepOf(selection)
508
+ // 46.11 — a bare `busy` hears a call the outbox is sending, which no value holds.
509
+ if (sweep === null)
510
+ return busyOver(sweep) || OUTBOX_SENDING.any?.() === true
500
511
  if (sweep !== undefined) return busyOver(sweep)
501
512
  return (selection as { busy(pattern: unknown): boolean }).busy(pattern)
502
513
  }
@@ -514,6 +525,17 @@ export function ready<Chosen extends Selection>(
514
525
  if (sweep !== undefined) return readyOver(sweep)
515
526
  return (selection as { ready(pattern: unknown): boolean }).ready(pattern)
516
527
  }
528
+ /**
529
+ * Whether every entry in the selection holds what its producer made — ALL, as `ready` asks (13.18).
530
+ */
531
+ export function synced<Chosen extends Selection>(
532
+ selection?: Chosen,
533
+ pattern?: PatternOf<Chosen, 'synced'>,
534
+ ): boolean {
535
+ const sweep = sweepOf(selection)
536
+ if (sweep !== undefined) return syncedOver(sweep)
537
+ return (selection as { synced(pattern: unknown): boolean }).synced(pattern)
538
+ }
517
539
  /**
518
540
  * Stale-while-revalidate over the selection: reloads it while serving what it holds.
519
541
  *
package/src/shared/rpc.ts CHANGED
@@ -18,6 +18,7 @@ import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
18
18
  import { HandlerRoute } from './HandlerRoute.ts'
19
19
  import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
20
20
  import { compose, type Middleware } from './Middleware.ts'
21
+ import { outbox, type QueuedCall, sendingFor } from './outbox.ts'
21
22
  import { principalRotated } from './principalRotated.ts'
22
23
  import { RPC_SERVER } from './RPC_SERVER.ts'
23
24
  import {
@@ -56,8 +57,14 @@ import type { Schema } from './Schema.ts'
56
57
 
57
58
  export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
58
59
 
59
- /** What a call is handed beside its args (16.48). */
60
- export type CallOptions = { signal?: AbortSignal }
60
+ /**
61
+ * What a call is handed beside its args: who may end its load (16.48), and what stands in for an
62
+ * answer a browser could not get (16.81).
63
+ */
64
+ export type CallOptions<Args = unknown, Value = unknown> = {
65
+ signal?: AbortSignal
66
+ offline?(args: Args): Value
67
+ }
61
68
 
62
69
  /**
63
70
  * A declared handler: callable for its `Reactive`, with the handler's own members and its mount.
@@ -71,8 +78,8 @@ export type CallOptions = { signal?: AbortSignal }
71
78
  */
72
79
  export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
73
80
  ...args: undefined extends Args
74
- ? [args?: Args, options?: CallOptions]
75
- : [args: Args, options?: CallOptions]
81
+ ? [args?: Args, options?: CallOptions<Args, NoInfer<Value>>]
82
+ : [args: Args, options?: CallOptions<Args, NoInfer<Value>>]
76
83
  ) => Reactive<
77
84
  Value,
78
85
  Value,
@@ -88,6 +95,9 @@ export type Rpc<Value, Args, Failures = never, Produced = Value> = ((
88
95
  */
89
96
  busy(pattern?: Partial<Args>): boolean
90
97
  ready(pattern?: Partial<Args>): boolean
98
+ synced(pattern?: Partial<Args>): boolean
99
+ /** 46.1 — the calls the outbox holds for this handler, narrowed by a pattern. */
100
+ outbox(pattern?: Partial<Args>): QueuedCall<Value>[]
91
101
  refresh(pattern?: Partial<Args>): void
92
102
  invalidate(pattern?: Partial<Args>): void
93
103
  isError<Name extends string>(
@@ -404,6 +414,7 @@ export function declare(method: Method): Declare {
404
414
  args,
405
415
  options?.signal,
406
416
  rpc.timeout ?? RPC_SERVER.timeout(),
417
+ options?.offline as ((args: unknown) => unknown) | undefined,
407
418
  )
408
419
  RPC_SERVER.reached?.(rpc, args, reached)
409
420
  return reached
@@ -457,7 +468,8 @@ export function declare(method: Method): Declare {
457
468
  const members = rpc as unknown as Record<string, unknown>
458
469
  // 7.10, 7.18 — A TRIGGER ON A SERVER REACHES EVERY BROWSER HOLDING WHAT IT NAMES, through the
459
470
  // mux (18.3). `null` in a browser, where the trigger is the copy's own.
460
- for (const name of SELECTION_MEMBERS)
471
+ for (const name of SELECTION_MEMBERS) {
472
+ if (name === 'busy') continue
461
473
  members[name] =
462
474
  name === 'invalidate' || name === 'refresh'
463
475
  ? (pattern?: unknown): unknown => {
@@ -466,6 +478,14 @@ export function declare(method: Method): Declare {
466
478
  return done
467
479
  }
468
480
  : (pattern?: unknown): unknown => face[name](pattern)
481
+ }
482
+ // 46.11 — BUSY TOO WHILE A CALL THE OUTBOX HOLDS FOR IT IS IN FLIGHT: a queued call sent is work
483
+ // in flight for this handler, as a call made now is. 46.1 — and the calls it holds for it.
484
+ members.busy = (pattern?: unknown): boolean =>
485
+ (face.busy(pattern) as boolean) ||
486
+ sendingFor(rpc, pattern as Record<string, unknown> | undefined)
487
+ members.outbox = (pattern?: unknown): QueuedCall[] =>
488
+ outbox(rpc as never, pattern as Record<string, unknown> | undefined)
469
489
  // 21.8, 18.5 — a `GET` over a `global` value, the value it is serving for some args: a cache
470
490
  // may hold it for as long as it lives, and the mux may feed it. A request-local one is a
471
491
  // forwarder to a value built per request (11.32).