@abide/abide 0.114.0 → 0.115.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 +12 -0
- package/docs/REGISTRY.md +24 -5
- package/docs/RULEBOOK.md +137 -11
- package/docs/abide.md +198 -8
- package/package.json +5 -3
- package/src/compiler/REACTIVE_MEMBERS.ts +1 -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 +111 -13
- package/src/server/createApp.ts +8 -2
- package/src/server/installSourceMaps.ts +8 -4
- 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/answerOf.ts +14 -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/outboxQueue.ts +427 -0
- package/src/ui/socketWire.ts +31 -2
- package/src/ui/wire.ts +111 -10
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
// 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
|
-
/**
|
|
60
|
-
|
|
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).
|
package/src/shared/rpcRequest.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type RpcRequest = {
|
|
|
17
17
|
url: string
|
|
18
18
|
/** The JSON body of a mutation, or `undefined` where the args ride in the URL. */
|
|
19
19
|
body: string | undefined
|
|
20
|
-
/** What `fetch` is handed: the method, the JSON content-type beside a body, and `
|
|
20
|
+
/** What `fetch` is handed: the method, the JSON content-type beside a body, and its `cache`. */
|
|
21
21
|
init: RequestInit
|
|
22
22
|
key: string
|
|
23
23
|
}
|
|
@@ -40,6 +40,7 @@ export function rpcRequest(
|
|
|
40
40
|
method: Method,
|
|
41
41
|
route: HandlerRoute | string,
|
|
42
42
|
given: Record<string, unknown> | undefined,
|
|
43
|
+
cache: RequestCache = 'no-cache',
|
|
43
44
|
): RpcRequest {
|
|
44
45
|
const { path, rest } = (
|
|
45
46
|
typeof route === 'string' ? new HandlerRoute(route) : route
|
|
@@ -50,17 +51,20 @@ export function rpcRequest(
|
|
|
50
51
|
const text = rest === undefined ? '' : queryText(rest)
|
|
51
52
|
if (text !== '') url = `${url}?${text}`
|
|
52
53
|
} else if (rest !== undefined) body = JSON.stringify(rest)
|
|
53
|
-
// 18.14 — A COPY IS ITS OWN CACHE and keeps its own `max-age`. What it asks the server
|
|
54
|
-
// because it went stale
|
|
55
|
-
// would answer it with the value it was dropping
|
|
56
|
-
// `
|
|
57
|
-
// the
|
|
54
|
+
// 18.14 — A COPY IS ITS OWN CACHE and keeps its own `max-age`. What it asks the server once it
|
|
55
|
+
// holds an answer is asked because it went stale, was invalidated or refreshed, and a cache
|
|
56
|
+
// below it, which hears none of the three, would answer it with the value it was dropping — so
|
|
57
|
+
// `no-cache`. A page's first load in a page the worker answers is the browser's to switch to
|
|
58
|
+
// `force-cache` (16.80), where a kept copy is what opens the page without the network (45.18);
|
|
59
|
+
// the outbox's send asks `no-store`. `rpc.raw` is handed `no-cache`, under whatever `init` its
|
|
60
|
+
// caller passes. `no-cache` rather than `no-store`: a `fetch` the app writes itself to the same
|
|
61
|
+
// address still reads 21.8's `max-age`.
|
|
58
62
|
const init: RequestInit =
|
|
59
63
|
body === undefined
|
|
60
|
-
? { method, cache
|
|
64
|
+
? { method, cache }
|
|
61
65
|
: {
|
|
62
66
|
method,
|
|
63
|
-
cache
|
|
67
|
+
cache,
|
|
64
68
|
headers: { 'content-type': 'application/json' },
|
|
65
69
|
body,
|
|
66
70
|
}
|