@abide/abide 0.124.0 → 0.125.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/ABIDE.md +4 -0
- package/CHANGELOG.md +43 -0
- package/bin/abide.ts +3 -2
- package/docs/REGISTRY.md +67 -41
- package/docs/RULEBOOK.md +207 -20
- package/docs/abide.md +392 -68
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +29 -4
- package/src/compiler/appMap.ts +14 -1
- package/src/compiler/declaredExports.ts +41 -0
- package/src/compiler/handlersUnder.ts +1 -0
- package/src/compiler/hookCalls.ts +1 -0
- package/src/compiler/projectShadow.ts +53 -2
- package/src/compiler/readsIn.ts +39 -34
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/PageRender.ts +7 -8
- package/src/server/abideModules.ts +6 -0
- package/src/server/answerRequest.ts +115 -52
- package/src/server/answerSocket.ts +6 -5
- package/src/server/answerWorker.ts +34 -23
- package/src/server/assistant.ts +141 -35
- package/src/server/banner.ts +9 -5
- package/src/server/bridgeRelay.ts +3 -3
- package/src/server/callerOf.ts +5 -0
- package/src/server/claudeCode.ts +20 -11
- package/src/server/commands.ts +14 -7
- package/src/server/createApp.ts +65 -12
- package/src/server/devWorker.ts +18 -16
- package/src/server/generatedSurfaces.ts +6 -5
- package/src/server/handlerFileMount.ts +2 -0
- package/src/server/hooks.ts +54 -8
- package/src/server/index.ts +18 -4
- package/src/server/invokeHandler.ts +1 -1
- package/src/server/mountHandlers.ts +6 -0
- package/src/server/principal.ts +3 -2
- package/src/server/render.ts +11 -2
- package/src/server/requestScope.ts +61 -1
- package/src/server/responseRelay.ts +9 -3
- package/src/server/serveDev.ts +18 -3
- package/src/server/sqliteStore.ts +65 -0
- package/src/server/testMount.ts +1 -0
- package/src/shared/AssistantEvent.ts +40 -0
- package/src/shared/BrowserHooks.ts +73 -0
- package/src/shared/CURRENT_ROUTER.ts +3 -1
- package/src/shared/Issues.ts +11 -3
- package/src/shared/Middleware.ts +9 -9
- package/src/shared/UNEXPECTED.ts +1 -1
- package/src/shared/answerOf.ts +9 -4
- package/src/shared/imagePiece.ts +85 -0
- package/src/shared/index.ts +17 -0
- package/src/shared/isServer.ts +15 -0
- package/src/shared/navigate.ts +10 -3
- package/src/shared/onError.ts +51 -0
- package/src/shared/onRefusal.ts +47 -0
- package/src/shared/online.ts +5 -1
- package/src/shared/reactive/Keyed.ts +12 -1
- package/src/shared/reactive/Reactive.ts +65 -26
- package/src/shared/reactive/cellShape.ts +4 -3
- package/src/shared/reactive/poll.ts +49 -0
- package/src/shared/reactive/scoped.ts +10 -1
- package/src/shared/reactive/scoping.ts +14 -0
- package/src/shared/reactive/watch.ts +19 -8
- package/src/shared/resultsOf.ts +21 -0
- package/src/shared/route.ts +18 -4
- package/src/shared/rpc.ts +112 -26
- package/src/shared/seededDocument.ts +5 -6
- package/src/shared/storedValue.ts +32 -0
- package/src/shared/webStore.ts +54 -0
- package/src/ui/Router.ts +42 -9
- package/src/ui/outboxQueue.ts +1 -1
- package/src/ui/wire.ts +4 -4
- package/src/shared/KEEP_OPENED.ts +0 -7
- /package/src/{server → shared}/IMAGE_TYPES.ts +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 22.11 — WHICH SIDE THE WORK RUNNING IS ON, read where it runs rather than fixed at build or load.
|
|
2
|
+
//
|
|
3
|
+
// NOT `typeof document`: a test process preloads a DOM and runs both halves of a page through one
|
|
4
|
+
// instance of every module — `App.fetch` and `mount`'s render are a server's, `mount`'s hydration a
|
|
5
|
+
// browser's — so the globals are the same on both sides and a build-time constant is one answer for
|
|
6
|
+
// two. What tells them apart is what the server alone enters: a request scope, or a render (the
|
|
7
|
+
// provider's `serverStops`). A process with no document at all is a server whatever it is doing.
|
|
8
|
+
|
|
9
|
+
import { serverStops } from './reactive/scoping.ts'
|
|
10
|
+
|
|
11
|
+
/** Whether the work running is a server's. */
|
|
12
|
+
export function isServer(): boolean {
|
|
13
|
+
if (typeof document === 'undefined') return true
|
|
14
|
+
return serverStops() !== undefined
|
|
15
|
+
}
|
package/src/shared/navigate.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CURRENT_ROUTER, type NavigateOptions } from './CURRENT_ROUTER.ts'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Navigates the document's router to a URL
|
|
4
|
+
* Navigates the document's router to a URL, or steps its history by a number of entries, without
|
|
5
|
+
* reloading; rejects where there is no router.
|
|
5
6
|
*
|
|
6
7
|
* 30.7 — NAVIGATES, AND MUST NOT RELOAD THE DOCUMENT. The whole of that clause lives in the client
|
|
7
8
|
* router; this is the app-facing name, and it sits on the isomorphic seam because that is the
|
|
@@ -11,8 +12,13 @@ import { CURRENT_ROUTER, type NavigateOptions } from './CURRENT_ROUTER.ts'
|
|
|
11
12
|
* refuses rather than resolving quietly — a `navigate` that no-ops during a render is a page that
|
|
12
13
|
* renders the wrong thing and says nothing.
|
|
13
14
|
*/
|
|
15
|
+
export function navigate(by: number): Promise<void>
|
|
14
16
|
export function navigate(
|
|
15
|
-
url
|
|
17
|
+
url?: URL | string,
|
|
18
|
+
options?: NavigateOptions,
|
|
19
|
+
): Promise<void>
|
|
20
|
+
export function navigate(
|
|
21
|
+
to: URL | string | number = '',
|
|
16
22
|
options: NavigateOptions = {},
|
|
17
23
|
): Promise<void> {
|
|
18
24
|
const router = CURRENT_ROUTER.current
|
|
@@ -22,5 +28,6 @@ export function navigate(
|
|
|
22
28
|
'navigate() was called with no router in this document. A browser gets one from the routes the build emitted; a server render has no document to navigate (RULEBOOK 30.7).',
|
|
23
29
|
),
|
|
24
30
|
)
|
|
25
|
-
|
|
31
|
+
// 30.38 — A NUMBER IS A STEP THROUGH THE HISTORY, as `history.go` takes one.
|
|
32
|
+
return typeof to === 'number' ? router.step(to) : router.go(to, options)
|
|
26
33
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 37.16, 37.17, 37.18 — THE BROWSER'S `onError`: the name and the signature `@abide/abide/server`'s
|
|
2
|
+
// has, called in a `<script>` rather than a `<script server>`, and filed while the component that
|
|
3
|
+
// called it stands — so a layout's hears every page below it, as the server's hears every route
|
|
4
|
+
// below it. A server's is `#server/hooks.ts`'s, which `@abide/abide/server` exports over this one;
|
|
5
|
+
// this one called in a server's render files nothing.
|
|
6
|
+
//
|
|
7
|
+
// WHAT NOTHING CAUGHT IS THE BROWSER'S OWN ACCOUNT: the window's `error` event and its
|
|
8
|
+
// `unhandledrejection`, which a call's refusal no handler awaited or caught arrives as. 12.6's
|
|
9
|
+
// throws are handed through `UNEXPECTED` as they are on a server, and the one of them that then
|
|
10
|
+
// surfaces as uncaught (12.7) is not handed a second time.
|
|
11
|
+
|
|
12
|
+
import { BrowserHooks } from './BrowserHooks.ts'
|
|
13
|
+
import { UNEXPECTED } from './UNEXPECTED.ts'
|
|
14
|
+
|
|
15
|
+
const HOOKS = new BrowserHooks<[error: unknown]>(
|
|
16
|
+
'onError',
|
|
17
|
+
"abide: onError of `@abide/abide` files only in a component's <script>; a <script server> or a server module files the server's, of `@abide/abide/server`. RULEBOOK 37.21.",
|
|
18
|
+
listen,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// What 12.6 handed and 12.7 may yet surface as uncaught, held until the task that handed it is over.
|
|
22
|
+
const SURFACING: unknown[] = []
|
|
23
|
+
|
|
24
|
+
/** Files `hook` for every failure nothing caught while the calling component stands; returns what unfiles it. */
|
|
25
|
+
export function onError(hook: (error: unknown) => unknown): () => void {
|
|
26
|
+
return HOOKS.file(hook)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listen(): void {
|
|
30
|
+
// A SERVER'S SLOT IS LEFT AS IT IS: a test with a document and a server has both lanes.
|
|
31
|
+
UNEXPECTED.report ??= handedUnexpected
|
|
32
|
+
addEventListener('error', (event) => surfaced((event as ErrorEvent).error))
|
|
33
|
+
addEventListener('unhandledrejection', (event) =>
|
|
34
|
+
surfaced((event as PromiseRejectionEvent).reason),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handedUnexpected(error: unknown): void {
|
|
39
|
+
if (SURFACING.length === 0) setTimeout(() => (SURFACING.length = 0))
|
|
40
|
+
SURFACING.push(error)
|
|
41
|
+
HOOKS.hand(error)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function surfaced(error: unknown): void {
|
|
45
|
+
const at = SURFACING.indexOf(error)
|
|
46
|
+
if (at !== -1) {
|
|
47
|
+
SURFACING.splice(at, 1)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
HOOKS.hand(error)
|
|
51
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// 37.22-37.27 — THE BROWSER'S `onRefusal`: filed as the browser's `onError` is, and handed every
|
|
2
|
+
// refusal a call over http is answered with in this browser — a refusal settles as the value's
|
|
3
|
+
// `error` rather than throwing, so `onError` never hears one a page read.
|
|
4
|
+
//
|
|
5
|
+
// HANDED WHERE AN ANSWER BECOMES A `Failed`, `answered` in `answerOf.ts`, which every browser call
|
|
6
|
+
// reads its answer through — a handler's `wire`, a handler abide mounts, and a call `outbox` sends —
|
|
7
|
+
// so no caller carries a branch of its own. An aborted call is never answered (16.49), and a call
|
|
8
|
+
// 16.81 settles to a stand-in is not refused.
|
|
9
|
+
|
|
10
|
+
import { BrowserHooks } from './BrowserHooks.ts'
|
|
11
|
+
import type { Failed } from './Failed.ts'
|
|
12
|
+
import { untracked } from './reactive/tracking.ts'
|
|
13
|
+
import type { Method } from './rpc.ts'
|
|
14
|
+
|
|
15
|
+
export type RefusedCall = { method: Method; url: string }
|
|
16
|
+
|
|
17
|
+
const HOOKS = new BrowserHooks<
|
|
18
|
+
[refusal: Failed<string, unknown>, call: RefusedCall]
|
|
19
|
+
>(
|
|
20
|
+
'onRefusal',
|
|
21
|
+
"abide: onRefusal files only in a component's <script>. RULEBOOK 37.23.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
/** Files `hook` for every refusal a call is answered with while the calling component stands; returns what unfiles it. */
|
|
25
|
+
export function onRefusal(
|
|
26
|
+
hook: (refusal: Failed<string, unknown>, call: RefusedCall) => unknown,
|
|
27
|
+
): () => void {
|
|
28
|
+
return HOOKS.file(hook)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 37.24 — hands `refusal`, the answer to `call`, to every `onRefusal` filed. */
|
|
32
|
+
export function refused(
|
|
33
|
+
refusal: Failed<string, unknown>,
|
|
34
|
+
call: RefusedCall,
|
|
35
|
+
): void {
|
|
36
|
+
if (HOOKS.empty()) return
|
|
37
|
+
// UNTRACKED: an answer the server painted settled is read inside the value's body (41.27), and a
|
|
38
|
+
// hook's read there would subscribe that value to whatever the hook reads.
|
|
39
|
+
untracked(handRefused, refusal, { method: call.method, url: call.url })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handRefused(
|
|
43
|
+
refusal: Failed<string, unknown>,
|
|
44
|
+
call: RefusedCall,
|
|
45
|
+
): void {
|
|
46
|
+
HOOKS.hand(refusal, call)
|
|
47
|
+
}
|
package/src/shared/online.ts
CHANGED
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
// PURE, SO AN APP THAT NEVER NAMES `online` DOES NOT SHIP IT: the listeners are a side effect at
|
|
8
8
|
// load, and a top-level side effect keeps the module in the shared chunk whether anything reads it.
|
|
9
9
|
|
|
10
|
+
// A BODY RATHER THAN A VALUE where there is a window, so each scope's instance asks again: a test
|
|
11
|
+
// process has both a DOM and a server render, and the render's `online` is 23.8's (22.11).
|
|
12
|
+
|
|
13
|
+
import { isServer } from './isServer.ts'
|
|
10
14
|
import { ambient } from './reactive/index.ts'
|
|
11
15
|
|
|
12
16
|
/** Whether the caller can reach the app, as a `Reactive<boolean>`. */
|
|
13
17
|
export const online = /* @__PURE__ */ (() => {
|
|
14
18
|
if (typeof window === 'undefined') return ambient<boolean>(true)
|
|
15
|
-
const value = ambient<boolean>(navigator.onLine)
|
|
19
|
+
const value = ambient<boolean>(() => isServer() || navigator.onLine)
|
|
16
20
|
window.addEventListener('online', () => value.set(true))
|
|
17
21
|
window.addEventListener('offline', () => value.set(false))
|
|
18
22
|
return value
|
|
@@ -23,6 +23,7 @@ import { KEYED_BRAND } from './KEYED_BRAND.ts'
|
|
|
23
23
|
import { matchesPattern } from './matchesPattern.ts'
|
|
24
24
|
import { NotLanded } from './NotLanded.ts'
|
|
25
25
|
import { parameterDefaults } from './parameterDefaults.ts'
|
|
26
|
+
import { armPoll, refreshIdle, refuseBadPoll } from './poll.ts'
|
|
26
27
|
import {
|
|
27
28
|
abandonDeferred,
|
|
28
29
|
createReactive,
|
|
@@ -344,7 +345,8 @@ export function createKeyed<
|
|
|
344
345
|
// 11.45: A KEYED `ttl` IS THE LIFE OF AN ENTRY, not of a production inside one, so it is
|
|
345
346
|
// taken off the options every entry is built from — forwarded, it would build the
|
|
346
347
|
// unkeyed mechanism in every entry and evict nothing from this map.
|
|
347
|
-
|
|
348
|
+
// 7.20 — and `poll`, so one timer walks the entries rather than one per entry.
|
|
349
|
+
const { ttl, args, throttle, debounce, poll, ...rest } = options ?? {}
|
|
348
350
|
const gate = args as Schema<Args> | undefined
|
|
349
351
|
const entryOptions = rest
|
|
350
352
|
// 5.14, at the factory: a keyed value is capped one way or the other.
|
|
@@ -375,6 +377,15 @@ export function createKeyed<
|
|
|
375
377
|
for (const entry of entries.values()) unwatchStore(entry)
|
|
376
378
|
})
|
|
377
379
|
|
|
380
|
+
// 7.20, 7.23 — one timer over the entries held at each tick: an evicted entry is out of the map.
|
|
381
|
+
if (poll !== undefined) {
|
|
382
|
+
refuseBadPoll(`${label}()`, poll)
|
|
383
|
+
const stop = armPoll(poll, () => {
|
|
384
|
+
for (const entry of entries.values()) refreshIdle(entry)
|
|
385
|
+
})
|
|
386
|
+
if (owning()) owned(stop)
|
|
387
|
+
}
|
|
388
|
+
|
|
378
389
|
function askMembership(): void {
|
|
379
390
|
const reader = TRACKING.reader
|
|
380
391
|
if (reader !== null) trackProbe(membership, reader)
|
|
@@ -40,6 +40,7 @@ import { isThenable } from './isThenable.ts'
|
|
|
40
40
|
import { NO_INITIAL } from './NO_INITIAL.ts'
|
|
41
41
|
import { NotLanded } from './NotLanded.ts'
|
|
42
42
|
import { logOf, PatchLog, type PatchOp, sendInvalidate } from './patchLog.ts'
|
|
43
|
+
import { armPoll, refreshIdle, refuseBadPoll } from './poll.ts'
|
|
43
44
|
import { REACTIVE_BRAND } from './REACTIVE_BRAND.ts'
|
|
44
45
|
import { recordWrites } from './recordWrites.ts'
|
|
45
46
|
import { runInScope } from './scoping.ts'
|
|
@@ -55,7 +56,6 @@ import { STREAMING } from './STREAMING.ts'
|
|
|
55
56
|
import {
|
|
56
57
|
CHECK,
|
|
57
58
|
CLEAN,
|
|
58
|
-
detach,
|
|
59
59
|
DIRTY,
|
|
60
60
|
dropProbeReader,
|
|
61
61
|
newSubscriber,
|
|
@@ -147,6 +147,13 @@ export type ReactiveOptions<
|
|
|
147
147
|
* once — see `expiryFor`.
|
|
148
148
|
*/
|
|
149
149
|
ttl?: number
|
|
150
|
+
/**
|
|
151
|
+
* A refresh every this many milliseconds.
|
|
152
|
+
*
|
|
153
|
+
* RULEBOOK 7.20 through 7.25: `s.refresh` on a clock, skipped while the value is busy and
|
|
154
|
+
* stopped with the value. Inert on a value with no producer (7.24).
|
|
155
|
+
*/
|
|
156
|
+
poll?: number
|
|
150
157
|
/**
|
|
151
158
|
* The gate on the way in, run before `transform` on every way in.
|
|
152
159
|
*
|
|
@@ -560,7 +567,11 @@ function markReaders(cell: Cell, wanted: number): void {
|
|
|
560
567
|
const cap = below.cap
|
|
561
568
|
if (cap !== null && below.body !== null) {
|
|
562
569
|
if (windowHolds(below, cap)) {
|
|
563
|
-
|
|
570
|
+
// 5.15: the held run is a production held back, so `busy` moves as it is owed.
|
|
571
|
+
if (!cap.owed) {
|
|
572
|
+
cap.owed = true
|
|
573
|
+
probesMoved(below)
|
|
574
|
+
}
|
|
564
575
|
// BACK TO CLEAN so the next source production marks it again and re-arms.
|
|
565
576
|
// Left marked it takes this walk's own `state >= wanted` short-circuit,
|
|
566
577
|
// and the changes after the first become invisible to the window.
|
|
@@ -1008,8 +1019,8 @@ function serve(cell: Cell, value: unknown): void {
|
|
|
1008
1019
|
// servable — `armNext` reads the deadline off a production that has landed, and from
|
|
1009
1020
|
// `produce` it would be reading one that had not.
|
|
1010
1021
|
if (cell.expiry !== null) armNext(cell, cell.expiry)
|
|
1011
|
-
// 8.8: every production that reaches a reader is written
|
|
1012
|
-
// identity gate collapsed never arrives here.
|
|
1022
|
+
// 8.8: every production that reaches a reader is written but the two `persist` withholds, and
|
|
1023
|
+
// nothing else is — a write the identity gate collapsed never arrives here.
|
|
1013
1024
|
if (cell.store !== null) persist(cell, cell.store, value)
|
|
1014
1025
|
}
|
|
1015
1026
|
|
|
@@ -1174,6 +1185,8 @@ function openWindow(cell: Cell, cap: Cap): void {
|
|
|
1174
1185
|
cell.state = DIRTY
|
|
1175
1186
|
cell.direct = false
|
|
1176
1187
|
cascade(cell)
|
|
1188
|
+
// The close is a `busy` edge, as a held production's is.
|
|
1189
|
+
probesMoved(cell)
|
|
1177
1190
|
return
|
|
1178
1191
|
}
|
|
1179
1192
|
// A throttle's window ending with nothing held is the window simply over: the
|
|
@@ -1671,7 +1684,8 @@ function run(cell: Cell): void {
|
|
|
1671
1684
|
// where the cascade would try: `a` reads `b` and `b` reads `a`. Without this the
|
|
1672
1685
|
// recursion is unbounded and the failure is a stack overflow rather than a value.
|
|
1673
1686
|
if (cell.running) return
|
|
1674
|
-
// A released orphan read again (12.14) — see `tombstone`.
|
|
1687
|
+
// A released orphan read again (12.14) — see `tombstone`. A released stream's run is its next
|
|
1688
|
+
// start: the generator its body hands back is promoted onto the stream it already has (9.6).
|
|
1675
1689
|
if (!cell.alive) revive(cell as unknown as Subscriber)
|
|
1676
1690
|
cell.generation += 1
|
|
1677
1691
|
cell.state = CLEAN
|
|
@@ -1841,11 +1855,13 @@ function landOpening(cell: Cell): void {
|
|
|
1841
1855
|
* WHAT STARTS A PRODUCER, for the three callers that start one: construction, `refresh`
|
|
1842
1856
|
* and `invalidate`. A stream is dispatched HERE rather than inside `run`, which a recompute
|
|
1843
1857
|
* walks per node per write — and a stream is never recomputed, having no tracked source to
|
|
1844
|
-
* be marked by (14.13).
|
|
1858
|
+
* be marked by (14.13). A RELEASED stream (12.16) is the exception: it is owed a run, and the
|
|
1859
|
+
* run is what revives it and settles the debt, where a start here would leave the debt for the
|
|
1860
|
+
* next read to start it a second time.
|
|
1845
1861
|
*/
|
|
1846
1862
|
function startProducer(cell: Cell): void {
|
|
1847
1863
|
const stream = ownStream(cell)
|
|
1848
|
-
if (stream === null) run(cell)
|
|
1864
|
+
if (stream === null || !cell.alive) run(cell)
|
|
1849
1865
|
else startStream(cell, stream)
|
|
1850
1866
|
}
|
|
1851
1867
|
|
|
@@ -2465,6 +2481,11 @@ let PROBE_PASS = 0
|
|
|
2465
2481
|
*/
|
|
2466
2482
|
function walkBusy(cell: Cell, pass: number): boolean {
|
|
2467
2483
|
if (cell.loading) return true
|
|
2484
|
+
// 5.15 over a body: a run the window owes. Read here rather than written into `loading`,
|
|
2485
|
+
// which a load ending inside the window clears while the run is still owed. Only a value with
|
|
2486
|
+
// sources is ever owed, so `busyNow`'s leaf answer never needs it.
|
|
2487
|
+
const cap = cell.cap
|
|
2488
|
+
if (cap !== null && cap.owed) return true
|
|
2468
2489
|
const adoption = cell.adoption
|
|
2469
2490
|
if (cell.sources.length === 0 && adoption === null) return false
|
|
2470
2491
|
if (cell.probePass === pass) return false
|
|
@@ -3192,11 +3213,19 @@ function settleRestore(
|
|
|
3192
3213
|
restore(cell, persisted, value)
|
|
3193
3214
|
return
|
|
3194
3215
|
}
|
|
3195
|
-
// A MISS IS THE VALUE WITHOUT A STORE: the body runs, or the initial value lands (8.3).
|
|
3216
|
+
// A MISS IS THE VALUE WITHOUT A STORE: the body runs, or the initial value lands (8.3). The
|
|
3217
|
+
// body's run is a production the store is filled from; the initial value is the declaration,
|
|
3218
|
+
// and writing it back on every miss is what a server render did to a browser's store (8.8).
|
|
3196
3219
|
if (cell.body !== null) startProducer(cell)
|
|
3197
3220
|
else if (initial === undefined) landOpening(cell)
|
|
3198
|
-
else if (initial !== NO_INITIAL)
|
|
3199
|
-
|
|
3221
|
+
else if (initial !== NO_INITIAL) {
|
|
3222
|
+
persisted.withheld = true
|
|
3223
|
+
try {
|
|
3224
|
+
open(cell, initial)
|
|
3225
|
+
} finally {
|
|
3226
|
+
persisted.withheld = false
|
|
3227
|
+
}
|
|
3228
|
+
} else endLoad(cell, false)
|
|
3200
3229
|
}
|
|
3201
3230
|
|
|
3202
3231
|
/**
|
|
@@ -3207,13 +3236,13 @@ function settleRestore(
|
|
|
3207
3236
|
* it back.
|
|
3208
3237
|
*/
|
|
3209
3238
|
function restore(cell: Cell, persisted: Persisted, value: unknown): void {
|
|
3210
|
-
persisted.
|
|
3239
|
+
persisted.withheld = true
|
|
3211
3240
|
try {
|
|
3212
3241
|
const stream = cell.stream
|
|
3213
3242
|
if (stream === null) produce(cell, value)
|
|
3214
3243
|
else restoreStream(cell, stream, value as unknown[])
|
|
3215
3244
|
} finally {
|
|
3216
|
-
persisted.
|
|
3245
|
+
persisted.withheld = false
|
|
3217
3246
|
}
|
|
3218
3247
|
}
|
|
3219
3248
|
|
|
@@ -3252,7 +3281,7 @@ function restoreStream(cell: Cell, stream: Stream, chunks: unknown[]): void {
|
|
|
3252
3281
|
* asked at all and the written value stands in its place. Asking took the value BACKWARDS and left
|
|
3253
3282
|
* it there: `s.set(99)` over a store still holding 1 leaves a `set` in flight, a `refresh` in the
|
|
3254
3283
|
* same turn read 1 and produced it, and the queued write then landed 99 in the store — the cell
|
|
3255
|
-
* serving 1 and the store holding 99 for the life of the value, with `
|
|
3284
|
+
* serving 1 and the store holding 99 for the life of the value, with `withheld` stopping the
|
|
3256
3285
|
* reverted value from being written back and no probe moving to say so.
|
|
3257
3286
|
*/
|
|
3258
3287
|
function rereadStore(cell: Cell, persisted: Persisted): void {
|
|
@@ -3342,13 +3371,12 @@ function settleMiss(cell: Cell): void {
|
|
|
3342
3371
|
}
|
|
3343
3372
|
|
|
3344
3373
|
/**
|
|
3345
|
-
* Writes a production to the store, unless it
|
|
3374
|
+
* Writes a production to the store, unless 8.8 withholds it or it is a stream's.
|
|
3346
3375
|
*
|
|
3347
|
-
* A
|
|
3348
|
-
* persisted from here.
|
|
3376
|
+
* A stream is written at its close (8.9), not from here.
|
|
3349
3377
|
*/
|
|
3350
3378
|
function persist(cell: Cell, persisted: Persisted, value: unknown): void {
|
|
3351
|
-
if (persisted.
|
|
3379
|
+
if (persisted.withheld || cell.stream !== null) return
|
|
3352
3380
|
writeStore(cell, persisted, value)
|
|
3353
3381
|
}
|
|
3354
3382
|
|
|
@@ -3863,22 +3891,22 @@ function orphan(cell: Cell): void {
|
|
|
3863
3891
|
* Left subscribed, a value a dropped row built stayed on its sources' reader lists for the life of
|
|
3864
3892
|
* the page, and an adopting one's mirror ran on every write to what it adopted. Released, it holds
|
|
3865
3893
|
* what it last served; a read that reaches it later — it escaped its owner — runs the body again
|
|
3866
|
-
* and subscribes afresh, so it is never stale, and from then on it is nobody's to release.
|
|
3867
|
-
* stream is stopped
|
|
3894
|
+
* and subscribes afresh, so it is never stale, and from then on it is nobody's to release. 12.16 — a
|
|
3895
|
+
* stream's run is stopped and it is owed the same run: the body's next start (9.6), which serves
|
|
3896
|
+
* what the stopped one held until it says something (9.7) — a stream nothing pumps is the one value
|
|
3897
|
+
* a later read could never bring current.
|
|
3868
3898
|
*/
|
|
3869
3899
|
function releaseOrphan(cell: Cell): void {
|
|
3870
3900
|
cell.orphaned = false
|
|
3871
|
-
const stream = cell.stream
|
|
3872
3901
|
const own = ownStream(cell)
|
|
3873
3902
|
if (own !== null) stopStream(own)
|
|
3874
3903
|
dropAdoption(cell)
|
|
3875
3904
|
cell.aborted = false
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
} else detach(cell as unknown as Subscriber)
|
|
3905
|
+
// OWED A RUN, so a read that reaches it again runs it — and `run` revives it first. No `body`
|
|
3906
|
+
// test: only a value with one is ever orphaned (`createReactive`).
|
|
3907
|
+
tombstone(cell as unknown as Subscriber)
|
|
3908
|
+
cell.state = DIRTY
|
|
3909
|
+
cell.direct = false
|
|
3882
3910
|
}
|
|
3883
3911
|
|
|
3884
3912
|
RELEASING.unread = (source: Tracked): void => releaseOrphan(source as Cell)
|
|
@@ -3976,5 +4004,16 @@ export function createReactive<
|
|
|
3976
4004
|
else if (initial !== NO_INITIAL) open(cell, initial)
|
|
3977
4005
|
} else if (deferred) cell.loading = true
|
|
3978
4006
|
else startProducer(cell)
|
|
4007
|
+
// 7.20, 7.23, 7.24 — a `Keyed` takes `poll` off its entries' options and polls them itself.
|
|
4008
|
+
const poll = options?.poll
|
|
4009
|
+
if (poll !== undefined) {
|
|
4010
|
+
refuseBadPoll(label, poll)
|
|
4011
|
+
if (reloads(cell)) {
|
|
4012
|
+
const stop = armPoll(poll, () =>
|
|
4013
|
+
refreshIdle(self as unknown as Reactive<unknown>),
|
|
4014
|
+
)
|
|
4015
|
+
if (owning()) owned(stop)
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
3979
4018
|
return self as unknown as Reactive<Stored, Accepted, Failures, Produced>
|
|
3980
4019
|
}
|
|
@@ -102,9 +102,10 @@ export type Persisted = {
|
|
|
102
102
|
/** The value's own `ttl`, taken once, and infinity where it declares none (8.13). */
|
|
103
103
|
ttl: number
|
|
104
104
|
/**
|
|
105
|
-
* Set while a
|
|
105
|
+
* Set while a production 8.8 does not write back is minted: a restore, which is what was just
|
|
106
|
+
* read, and the initial value a miss falls back to, which the store never held.
|
|
106
107
|
*/
|
|
107
|
-
|
|
108
|
+
withheld: boolean
|
|
108
109
|
writing: boolean
|
|
109
110
|
pending: boolean
|
|
110
111
|
/**
|
|
@@ -601,7 +602,7 @@ export class Cell {
|
|
|
601
602
|
? store(args as never)
|
|
602
603
|
: store) as Store<unknown>,
|
|
603
604
|
ttl: options?.ttl ?? Number.POSITIVE_INFINITY,
|
|
604
|
-
|
|
605
|
+
withheld: false,
|
|
605
606
|
writing: false,
|
|
606
607
|
pending: false,
|
|
607
608
|
unconfirmed: undefined,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A REFRESH ON A CLOCK. RULEBOOK 7.20 through 7.25: `poll` is `s.refresh` (7.4) run every `poll`
|
|
2
|
+
// milliseconds, which is what an app otherwise writes as a `setInterval` in `onStart` and a
|
|
3
|
+
// `clearInterval` in `onStop` — and then has to remember for every value, keyed entry and request.
|
|
4
|
+
//
|
|
5
|
+
// NO FIELD ON THE CELL. The timer's stop is handed to whatever ends the value — its owner, or its
|
|
6
|
+
// `Keyed` — so a value declaring no `poll` carries nothing for it, and the read and write paths
|
|
7
|
+
// never see it. A `Keyed` polls with ONE timer over the entries it holds at each tick, so an entry
|
|
8
|
+
// its `ttl` evicted is out of the walk by being out of the map.
|
|
9
|
+
|
|
10
|
+
import { LONGEST_DELAY } from '../LONGEST_DELAY.ts'
|
|
11
|
+
import type { Disposer } from './watch.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Throws where `poll` is not a positive number of milliseconds (7.25).
|
|
15
|
+
*
|
|
16
|
+
* At construction, as 5.14 is: a zero or negative interval is a refresh per turn, and the app
|
|
17
|
+
* still has a stack for it here.
|
|
18
|
+
*/
|
|
19
|
+
export function refuseBadPoll(label: string, poll: number | undefined): void {
|
|
20
|
+
if (poll !== undefined && !(poll > 0))
|
|
21
|
+
throw new Error(
|
|
22
|
+
`${label} declares poll: ${poll}. A poll is a positive number of milliseconds.`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Runs `tick` every `poll` milliseconds, never holding the process open, and answers its stop.
|
|
28
|
+
*
|
|
29
|
+
* 7.22 FOLLOWS 6.4's FEATURE TEST: Bun's `Timer` carries `unref`, a browser's number does not and
|
|
30
|
+
* has no process to hold. `Infinity` arms nothing; a finite interval past what a timer holds runs
|
|
31
|
+
* at the longest one it does.
|
|
32
|
+
*/
|
|
33
|
+
export function armPoll(poll: number, tick: () => void): Disposer {
|
|
34
|
+
if (poll === Number.POSITIVE_INFINITY) return () => {}
|
|
35
|
+
const timer = setInterval(tick, Math.min(poll, LONGEST_DELAY))
|
|
36
|
+
const unref = (timer as { unref?: () => void }).unref
|
|
37
|
+
if (typeof unref === 'function') unref.call(timer)
|
|
38
|
+
return () => clearInterval(timer)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Refreshes `value` unless it is busy (7.21).
|
|
43
|
+
*
|
|
44
|
+
* A refresh over a load in flight supersedes it, so a producer slower than its interval would
|
|
45
|
+
* otherwise never land.
|
|
46
|
+
*/
|
|
47
|
+
export function refreshIdle(value: { busy(): boolean; refresh(): void }): void {
|
|
48
|
+
if (!value.busy()) value.refresh()
|
|
49
|
+
}
|
|
@@ -20,6 +20,7 @@ import { brandedPrototype, MEMBER_KEYS, signalMoved } from './Reactive.ts'
|
|
|
20
20
|
import { currentScope, runInScope, SCOPING } from './scoping.ts'
|
|
21
21
|
import { SELECTION_MEMBERS } from './SELECTION_MEMBERS.ts'
|
|
22
22
|
import { newTracked, type Tracked, TRACKING, trackProbe } from './tracking.ts'
|
|
23
|
+
import { owning, unowned } from './watch.ts'
|
|
23
24
|
|
|
24
25
|
const UNBUILT: unique symbol = Symbol('abide.unbuilt')
|
|
25
26
|
|
|
@@ -38,6 +39,11 @@ class Filed {
|
|
|
38
39
|
home: unknown
|
|
39
40
|
make: () => unknown
|
|
40
41
|
global: boolean
|
|
42
|
+
/**
|
|
43
|
+
* 12.17 — whether the declaration had an owner. A home declared with none — a module's, which is
|
|
44
|
+
* every handler's — is the process's, and is built as one wherever it is first reached.
|
|
45
|
+
*/
|
|
46
|
+
declaredOwned: boolean
|
|
41
47
|
/**
|
|
42
48
|
* 3.3, 13.19 — what a probe asked before the value was built here subscribes to, moved by the
|
|
43
49
|
* build: the home's. A scope files its own in `instances` under this one as its key, and drops
|
|
@@ -49,6 +55,7 @@ class Filed {
|
|
|
49
55
|
this.make = make
|
|
50
56
|
this.home = lazy ? UNBUILT : make()
|
|
51
57
|
this.global = global
|
|
58
|
+
this.declaredOwned = owning()
|
|
52
59
|
this.unbuilt = newTracked()
|
|
53
60
|
}
|
|
54
61
|
}
|
|
@@ -66,7 +73,9 @@ function instanceOf(filed: Filed): Record<PropertyKey, unknown> {
|
|
|
66
73
|
const scope = filed.global ? undefined : currentScope()
|
|
67
74
|
if (scope === undefined) {
|
|
68
75
|
if (filed.home === UNBUILT) {
|
|
69
|
-
filed.home =
|
|
76
|
+
filed.home = filed.declaredOwned
|
|
77
|
+
? runInScope(undefined, filed.make, undefined)
|
|
78
|
+
: unowned(() => runInScope(undefined, filed.make, undefined))
|
|
70
79
|
signalMoved(filed.unbuilt)
|
|
71
80
|
}
|
|
72
81
|
return filed.home as Record<PropertyKey, unknown>
|
|
@@ -25,6 +25,14 @@ export type Scope = {
|
|
|
25
25
|
|
|
26
26
|
export type Scoping = {
|
|
27
27
|
current(): Scope | undefined
|
|
28
|
+
/**
|
|
29
|
+
* The stops of the server work running — a request's scope, or a `render` outside every request —
|
|
30
|
+
* or `undefined` where none is.
|
|
31
|
+
*
|
|
32
|
+
* 12.5 — what a `watch` opened in that work is stopped with. 22.11 — and whether the work running
|
|
33
|
+
* is a server's at all.
|
|
34
|
+
*/
|
|
35
|
+
serverStops(): (() => void)[] | undefined
|
|
28
36
|
/**
|
|
29
37
|
* Runs `body` with `scope` as the current one, and `undefined` as no scope at all.
|
|
30
38
|
*
|
|
@@ -46,6 +54,12 @@ export function currentScope(): Scope | undefined {
|
|
|
46
54
|
return provider === null ? undefined : provider.current()
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/** What a `watch` opened now is stopped with on a server — 12.5 — or `undefined` in a browser. */
|
|
58
|
+
export function serverStops(): (() => void)[] | undefined {
|
|
59
|
+
const provider = SCOPING.provider
|
|
60
|
+
return provider === null ? undefined : provider.serverStops()
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
/**
|
|
50
64
|
* Runs `body` under `scope` and returns what it returns.
|
|
51
65
|
*
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// imports the narrowed form from here. A second argument is the author saying which form
|
|
12
12
|
// they meant, and it needs nothing from the other module but a type.
|
|
13
13
|
|
|
14
|
+
import { isServer } from '../isServer.ts'
|
|
14
15
|
import { namedLogger } from '../log.ts'
|
|
15
16
|
import { UNEXPECTED } from '../UNEXPECTED.ts'
|
|
16
17
|
import { WORK } from '../WORK.ts'
|
|
@@ -18,7 +19,7 @@ import { CELL } from './CELL.ts'
|
|
|
18
19
|
import type { Callable } from './cellShape.ts'
|
|
19
20
|
import { NotLanded } from './NotLanded.ts'
|
|
20
21
|
import type { Reactive } from './Reactive.ts'
|
|
21
|
-
import { currentScope, runInScope } from './scoping.ts'
|
|
22
|
+
import { currentScope, runInScope, serverStops } from './scoping.ts'
|
|
22
23
|
import {
|
|
23
24
|
detach,
|
|
24
25
|
newSubscriber,
|
|
@@ -49,9 +50,10 @@ function reportThrow(thrown: unknown, message: string): void {
|
|
|
49
50
|
/**
|
|
50
51
|
* Reports an effect's throw, then re-throws it in a browser.
|
|
51
52
|
*
|
|
52
|
-
* 12.7 — A BROWSER
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* 12.7 — A BROWSER'S THROW GOES ON OUT to surface as an uncaught error: to the caller of `watch` on
|
|
54
|
+
* a first run, and on a rerun to the drain, which reports it and runs the rest of its queue
|
|
55
|
+
* (`drainPending`) — and its `onError`, handed it already, is not handed it again (`onError.ts`).
|
|
56
|
+
* A server's has been handed it, and re-thrown there it would
|
|
55
57
|
* fail the handler or the render whose write woke this effect — a second report of one failure,
|
|
56
58
|
* against code that did not fail.
|
|
57
59
|
*/
|
|
@@ -60,7 +62,8 @@ function effectThrew(thrown: unknown): void {
|
|
|
60
62
|
thrown,
|
|
61
63
|
'an effect threw, and its watch is stopped. RULEBOOK 12.6, 12.8.',
|
|
62
64
|
)
|
|
63
|
-
|
|
65
|
+
// 22.11, NOT `typeof document`: a test process's in-process render has one, and re-threw.
|
|
66
|
+
if (!isServer()) throw thrown
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -104,16 +107,24 @@ export function collectWatches(
|
|
|
104
107
|
* Hands `dispose` to the setup being collected, if one is, and to the scope it was opened in, if
|
|
105
108
|
* there is one; answers it.
|
|
106
109
|
*
|
|
107
|
-
* 12.5 — the scope is a server's request (`
|
|
110
|
+
* 12.5 — the scope is a server's request, or its render outside every request (`serverStops`),
|
|
111
|
+
* which a browser never has.
|
|
108
112
|
*/
|
|
109
113
|
export function owned(dispose: Disposer): Disposer {
|
|
110
114
|
if (OWNING.free) return dispose
|
|
111
115
|
const stops = OWNING.stops
|
|
112
116
|
if (stops !== undefined) stops.push(dispose)
|
|
113
|
-
|
|
117
|
+
serverStops()?.push(dispose)
|
|
114
118
|
return dispose
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Whether a component's setup is being collected now: a browser's, since a server's never is.
|
|
123
|
+
*/
|
|
124
|
+
export function settingUp(): boolean {
|
|
125
|
+
return OWNING.stops !== undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
117
128
|
/**
|
|
118
129
|
* Whether something opened now has an owner to be handed to — a setup being collected, or a scope.
|
|
119
130
|
*
|
|
@@ -123,7 +134,7 @@ export function owned(dispose: Disposer): Disposer {
|
|
|
123
134
|
export function owning(): boolean {
|
|
124
135
|
return (
|
|
125
136
|
!OWNING.free &&
|
|
126
|
-
(OWNING.stops !== undefined ||
|
|
137
|
+
(OWNING.stops !== undefined || serverStops() !== undefined)
|
|
127
138
|
)
|
|
128
139
|
}
|
|
129
140
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AssistantEvent } from './AssistantEvent.ts'
|
|
2
|
+
import type { Rpc } from './rpc.ts'
|
|
3
|
+
|
|
4
|
+
/** What every call to `handler` in `events` answered, in order, a failed call's left out. 44.51. */
|
|
5
|
+
export function resultsOf<Value>(
|
|
6
|
+
events: readonly AssistantEvent[],
|
|
7
|
+
// Any handler at all: its args, failures and production are no part of what it answered.
|
|
8
|
+
handler: Rpc<Value, never, unknown, unknown>,
|
|
9
|
+
): Value[] {
|
|
10
|
+
const values: Value[] = []
|
|
11
|
+
for (const event of events)
|
|
12
|
+
if (
|
|
13
|
+
event.type === 'result' &&
|
|
14
|
+
!event.failed &&
|
|
15
|
+
event.handler !== undefined &&
|
|
16
|
+
event.handler.method === handler.method &&
|
|
17
|
+
event.handler.address === handler.address
|
|
18
|
+
)
|
|
19
|
+
values.push(event.value as Value)
|
|
20
|
+
return values
|
|
21
|
+
}
|