@abide/abide 0.124.1 → 0.125.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ABIDE.md +4 -0
- package/CHANGELOG.md +50 -0
- package/bin/abide.ts +3 -2
- package/docs/REGISTRY.md +67 -40
- package/docs/RULEBOOK.md +206 -18
- package/docs/abide.md +393 -67
- package/package.json +1 -1
- package/src/compiler/appMap.ts +14 -1
- package/src/compiler/compileApp.ts +1 -0
- 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 +52 -2
- package/src/compiler/readsIn.ts +39 -34
- package/src/server/PageRender.ts +7 -8
- package/src/server/abideModules.ts +22 -9
- package/src/server/addressed.ts +14 -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/faceOf.ts +3 -1
- 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 +21 -4
- package/src/server/invokeHandler.ts +1 -1
- package/src/server/mountHandlers.ts +9 -7
- 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 +14 -7
- 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 +53 -25
- 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 +23 -0
- package/src/shared/route.ts +18 -4
- package/src/shared/rpc.ts +125 -30
- 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
package/src/shared/index.ts
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
// value (15.1), so it is in this seam rather than the server's: the same object crosses a
|
|
11
11
|
// wire, fills `s.error` and narrows through `s.isError`.
|
|
12
12
|
|
|
13
|
+
// 44 — a conversation as a page sends it and is handed it back: the server's `assistant` yields it.
|
|
14
|
+
export type {
|
|
15
|
+
AssistantContent,
|
|
16
|
+
AssistantEvent,
|
|
17
|
+
AssistantMessage,
|
|
18
|
+
} from './AssistantEvent.ts'
|
|
13
19
|
// 33.23 — an element's attributes, typed as a component's props.
|
|
14
20
|
export type { Attributes } from './Attributes.ts'
|
|
15
21
|
// 32.37 — what a value writes into an attribute, on both sides.
|
|
@@ -27,8 +33,11 @@ export { escapeAttribute, escapeStyleValue, escapeText } from './escapeText.ts'
|
|
|
27
33
|
export { type Failed, isFailed } from './Failed.ts'
|
|
28
34
|
// 25 — the account of the app.
|
|
29
35
|
export { type Health, health } from './health.ts'
|
|
36
|
+
// 44.52-44.55 — a pasted, dropped or picked image as a turn's piece.
|
|
37
|
+
export { imageFiles, imagePiece } from './imagePiece.ts'
|
|
30
38
|
export type { Issues, Paths } from './Issues.ts'
|
|
31
39
|
export type { JsonValue } from './JsonValue.ts'
|
|
40
|
+
export { isServer } from './isServer.ts'
|
|
32
41
|
export { jsonSchema } from './jsonSchema.ts'
|
|
33
42
|
// 32.15's run-time half. In this seam because BOTH sides ask it what a `{#for}` subject iterates
|
|
34
43
|
// as — the server's emitted block, the client's `updateList`, and the markup a client builds for a
|
|
@@ -52,7 +61,11 @@ export {
|
|
|
52
61
|
export { navigate } from './navigate.ts'
|
|
53
62
|
export { online } from './online.ts'
|
|
54
63
|
export { outbox, type QueuedCall } from './outbox.ts'
|
|
64
|
+
// 37.16 — the browser's `onError`. `@abide/abide/server` exports the server's over it.
|
|
65
|
+
export { onError } from './onError.ts'
|
|
55
66
|
export { onMenu } from './onMenu.ts'
|
|
67
|
+
// 37.22 — the browser's `onRefusal`, which has no server's.
|
|
68
|
+
export { onRefusal } from './onRefusal.ts'
|
|
56
69
|
export { type Principal, principal } from './principal.ts'
|
|
57
70
|
// EMITTED CODE'S: a prop the child writes (33.6, 33.7), and one it hands where a `Reactive` is due (33.27).
|
|
58
71
|
export {
|
|
@@ -96,6 +109,8 @@ export {
|
|
|
96
109
|
watch,
|
|
97
110
|
} from './reactive/index.ts'
|
|
98
111
|
export { aborted, notFound, refuse, validationError } from './refuse.ts'
|
|
112
|
+
// 44.51 — what a conversation's calls to one handler answered.
|
|
113
|
+
export { resultsOf } from './resultsOf.ts'
|
|
99
114
|
export { route } from './route.ts'
|
|
100
115
|
export type { Schema, StandardSchemaV1 } from './Schema.ts'
|
|
101
116
|
export { safeUrl } from './safeUrl.ts'
|
|
@@ -119,4 +134,6 @@ export {
|
|
|
119
134
|
url,
|
|
120
135
|
} from './url.ts'
|
|
121
136
|
export { validateJson } from './validateJson.ts'
|
|
137
|
+
// 8.22 — a `Store` in the browser's Web Storage, inert where there is none (8.23).
|
|
138
|
+
export { webStore } from './webStore.ts'
|
|
122
139
|
export { mutatePath, writePath } from './writePath.ts'
|
|
@@ -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
|
*
|
|
@@ -1012,8 +1019,8 @@ function serve(cell: Cell, value: unknown): void {
|
|
|
1012
1019
|
// servable — `armNext` reads the deadline off a production that has landed, and from
|
|
1013
1020
|
// `produce` it would be reading one that had not.
|
|
1014
1021
|
if (cell.expiry !== null) armNext(cell, cell.expiry)
|
|
1015
|
-
// 8.8: every production that reaches a reader is written
|
|
1016
|
-
// 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.
|
|
1017
1024
|
if (cell.store !== null) persist(cell, cell.store, value)
|
|
1018
1025
|
}
|
|
1019
1026
|
|
|
@@ -1677,7 +1684,8 @@ function run(cell: Cell): void {
|
|
|
1677
1684
|
// where the cascade would try: `a` reads `b` and `b` reads `a`. Without this the
|
|
1678
1685
|
// recursion is unbounded and the failure is a stack overflow rather than a value.
|
|
1679
1686
|
if (cell.running) return
|
|
1680
|
-
// 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).
|
|
1681
1689
|
if (!cell.alive) revive(cell as unknown as Subscriber)
|
|
1682
1690
|
cell.generation += 1
|
|
1683
1691
|
cell.state = CLEAN
|
|
@@ -1847,11 +1855,13 @@ function landOpening(cell: Cell): void {
|
|
|
1847
1855
|
* WHAT STARTS A PRODUCER, for the three callers that start one: construction, `refresh`
|
|
1848
1856
|
* and `invalidate`. A stream is dispatched HERE rather than inside `run`, which a recompute
|
|
1849
1857
|
* walks per node per write — and a stream is never recomputed, having no tracked source to
|
|
1850
|
-
* 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.
|
|
1851
1861
|
*/
|
|
1852
1862
|
function startProducer(cell: Cell): void {
|
|
1853
1863
|
const stream = ownStream(cell)
|
|
1854
|
-
if (stream === null) run(cell)
|
|
1864
|
+
if (stream === null || !cell.alive) run(cell)
|
|
1855
1865
|
else startStream(cell, stream)
|
|
1856
1866
|
}
|
|
1857
1867
|
|
|
@@ -3203,11 +3213,19 @@ function settleRestore(
|
|
|
3203
3213
|
restore(cell, persisted, value)
|
|
3204
3214
|
return
|
|
3205
3215
|
}
|
|
3206
|
-
// 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).
|
|
3207
3219
|
if (cell.body !== null) startProducer(cell)
|
|
3208
3220
|
else if (initial === undefined) landOpening(cell)
|
|
3209
|
-
else if (initial !== NO_INITIAL)
|
|
3210
|
-
|
|
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)
|
|
3211
3229
|
}
|
|
3212
3230
|
|
|
3213
3231
|
/**
|
|
@@ -3218,13 +3236,13 @@ function settleRestore(
|
|
|
3218
3236
|
* it back.
|
|
3219
3237
|
*/
|
|
3220
3238
|
function restore(cell: Cell, persisted: Persisted, value: unknown): void {
|
|
3221
|
-
persisted.
|
|
3239
|
+
persisted.withheld = true
|
|
3222
3240
|
try {
|
|
3223
3241
|
const stream = cell.stream
|
|
3224
3242
|
if (stream === null) produce(cell, value)
|
|
3225
3243
|
else restoreStream(cell, stream, value as unknown[])
|
|
3226
3244
|
} finally {
|
|
3227
|
-
persisted.
|
|
3245
|
+
persisted.withheld = false
|
|
3228
3246
|
}
|
|
3229
3247
|
}
|
|
3230
3248
|
|
|
@@ -3263,7 +3281,7 @@ function restoreStream(cell: Cell, stream: Stream, chunks: unknown[]): void {
|
|
|
3263
3281
|
* asked at all and the written value stands in its place. Asking took the value BACKWARDS and left
|
|
3264
3282
|
* it there: `s.set(99)` over a store still holding 1 leaves a `set` in flight, a `refresh` in the
|
|
3265
3283
|
* same turn read 1 and produced it, and the queued write then landed 99 in the store — the cell
|
|
3266
|
-
* 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
|
|
3267
3285
|
* reverted value from being written back and no probe moving to say so.
|
|
3268
3286
|
*/
|
|
3269
3287
|
function rereadStore(cell: Cell, persisted: Persisted): void {
|
|
@@ -3353,13 +3371,12 @@ function settleMiss(cell: Cell): void {
|
|
|
3353
3371
|
}
|
|
3354
3372
|
|
|
3355
3373
|
/**
|
|
3356
|
-
* 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.
|
|
3357
3375
|
*
|
|
3358
|
-
* A
|
|
3359
|
-
* persisted from here.
|
|
3376
|
+
* A stream is written at its close (8.9), not from here.
|
|
3360
3377
|
*/
|
|
3361
3378
|
function persist(cell: Cell, persisted: Persisted, value: unknown): void {
|
|
3362
|
-
if (persisted.
|
|
3379
|
+
if (persisted.withheld || cell.stream !== null) return
|
|
3363
3380
|
writeStore(cell, persisted, value)
|
|
3364
3381
|
}
|
|
3365
3382
|
|
|
@@ -3874,22 +3891,22 @@ function orphan(cell: Cell): void {
|
|
|
3874
3891
|
* Left subscribed, a value a dropped row built stayed on its sources' reader lists for the life of
|
|
3875
3892
|
* the page, and an adopting one's mirror ran on every write to what it adopted. Released, it holds
|
|
3876
3893
|
* what it last served; a read that reaches it later — it escaped its owner — runs the body again
|
|
3877
|
-
* and subscribes afresh, so it is never stale, and from then on it is nobody's to release.
|
|
3878
|
-
* 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.
|
|
3879
3898
|
*/
|
|
3880
3899
|
function releaseOrphan(cell: Cell): void {
|
|
3881
3900
|
cell.orphaned = false
|
|
3882
|
-
const stream = cell.stream
|
|
3883
3901
|
const own = ownStream(cell)
|
|
3884
3902
|
if (own !== null) stopStream(own)
|
|
3885
3903
|
dropAdoption(cell)
|
|
3886
3904
|
cell.aborted = false
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
} 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
|
|
3893
3910
|
}
|
|
3894
3911
|
|
|
3895
3912
|
RELEASING.unread = (source: Tracked): void => releaseOrphan(source as Cell)
|
|
@@ -3987,5 +4004,16 @@ export function createReactive<
|
|
|
3987
4004
|
else if (initial !== NO_INITIAL) open(cell, initial)
|
|
3988
4005
|
} else if (deferred) cell.loading = true
|
|
3989
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
|
+
}
|
|
3990
4018
|
return self as unknown as Reactive<Stored, Accepted, Failures, Produced>
|
|
3991
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
|
|