@abide/abide 0.44.1 → 0.46.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/AGENTS.md +8 -6
- package/CHANGELOG.md +56 -0
- package/README.md +171 -181
- package/package.json +2 -1
- package/src/lib/server/error.ts +48 -53
- package/src/lib/server/json.ts +4 -3
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/rpc/defineRpc.ts +1 -6
- package/src/lib/server/rpc/types/RemoteHandler.ts +5 -7
- package/src/lib/server/rpc/types/RpcHelper.ts +125 -35
- package/src/lib/server/rpc/types/TypedError.ts +18 -0
- package/src/lib/server/rpc/validationError.ts +3 -3
- package/src/lib/server/runtime/STATUS_TEXT.ts +22 -0
- package/src/lib/server/runtime/createServer.ts +7 -0
- package/src/lib/server/runtime/installAmbientScopeStore.ts +33 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -0
- package/src/lib/server/runtime/typedErrorResponse.ts +26 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +14 -0
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/cache.ts +26 -4
- package/src/lib/shared/cacheEntryFromSnapshot.ts +21 -1
- package/src/lib/shared/cacheKeyOf.ts +7 -0
- package/src/lib/shared/cacheKeyStore.ts +8 -0
- package/src/lib/shared/httpErrorFor.ts +1 -1
- package/src/lib/shared/recordCacheKey.ts +6 -0
- package/src/lib/shared/toTagSet.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +15 -0
- package/src/lib/shared/types/CacheOptions.ts +5 -5
- package/src/lib/shared/types/CacheSnapshotEntry.ts +5 -0
- package/src/lib/shared/types/ErrorSpec.ts +6 -6
- package/src/lib/shared/types/RemoteFunction.ts +8 -4
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/generateBuild.ts +5 -1
- package/src/lib/ui/compile/generateSSR.ts +10 -3
- package/src/lib/ui/compile/parseTemplate.ts +14 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +5 -0
- package/src/lib/ui/createScope.ts +11 -4
- package/src/lib/ui/deferResume.ts +36 -0
- package/src/lib/ui/dom/appendText.ts +19 -4
- package/src/lib/ui/dom/awaitBlock.ts +99 -26
- package/src/lib/ui/dom/eachAsync.ts +11 -18
- package/src/lib/ui/dom/firstElementBetween.ts +14 -0
- package/src/lib/ui/dom/mountChild.ts +5 -2
- package/src/lib/ui/dom/mountRange.ts +75 -0
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/matchRoute.ts +18 -1
- package/src/lib/ui/remoteProxy.ts +11 -1
- package/src/lib/ui/renderToStream.ts +13 -8
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +14 -1
- package/src/lib/ui/runtime/RESUME.ts +6 -0
- package/src/lib/ui/runtime/ambientScopeBacking.ts +26 -0
- package/src/lib/ui/runtime/createEffectNode.ts +7 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -0
- package/src/lib/ui/runtime/generationGuard.ts +40 -0
- package/src/lib/ui/runtime/runNode.ts +9 -1
- package/src/lib/ui/runtime/scheduleWake.ts +28 -0
- package/src/lib/ui/runtime/types/SsrRender.ts +4 -3
- package/src/lib/ui/runtime/whenIdle.ts +21 -0
- package/src/lib/ui/runtime/whenVisible.ts +105 -0
- package/src/lib/ui/scope.ts +6 -8
- package/src/lib/ui/tryEncodeResume.ts +5 -2
- package/src/lib/ui/types/Scope.ts +3 -4
- package/src/lib/server/rpc/buildErrorConstructors.ts +0 -19
- package/src/lib/shared/types/ErrorConstructors.ts +0 -17
- package/src/lib/shared/types/ErrorDescriptor.ts +0 -10
|
@@ -11,6 +11,12 @@ drains the captured one — so an effect that dirties further effects re-queues
|
|
|
11
11
|
for the next pass rather than mutating the array mid-iteration; loops until the
|
|
12
12
|
graph settles. The swap reuses the drained array as the next spare, so a steady
|
|
13
13
|
flush allocates nothing.
|
|
14
|
+
|
|
15
|
+
Raises `batchDepth` for the whole drain so a write inside an effect body queues
|
|
16
|
+
rather than re-entering the flush: `trigger` gates its flush on `batchDepth === 0`,
|
|
17
|
+
and re-entry would run a just-dirtied effect nested — ahead of effects already in
|
|
18
|
+
this pass and on a JS stack that grows with the write chain. Suppressed, the
|
|
19
|
+
newly-dirtied effect falls to the `do…while` and runs in queue (creation) order.
|
|
14
20
|
*/
|
|
15
21
|
export function flushEffects(): void {
|
|
16
22
|
/* Empty-queue fast path allocates nothing — only the length check, matching the
|
|
@@ -20,6 +26,18 @@ export function flushEffects(): void {
|
|
|
20
26
|
if (REACTIVE_CONTEXT.pendingEffects.length === 0) {
|
|
21
27
|
return
|
|
22
28
|
}
|
|
29
|
+
/* Always entered at depth 0 (trigger/batch-exit gate on it); the bump makes the
|
|
30
|
+
drain non-reentrant, restored in `finally` so a throwing effect body can't strand
|
|
31
|
+
the graph batched. */
|
|
32
|
+
REACTIVE_CONTEXT.batchDepth += 1
|
|
33
|
+
try {
|
|
34
|
+
drain()
|
|
35
|
+
} finally {
|
|
36
|
+
REACTIVE_CONTEXT.batchDepth -= 1
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function drain(): void {
|
|
23
41
|
let spare: ReactiveNode[] = []
|
|
24
42
|
do {
|
|
25
43
|
const batch = REACTIVE_CONTEXT.pendingEffects
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { OWNER } from './OWNER.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A monotonic generation an async block checks before a late continuation touches the DOM. It
|
|
5
|
+
answers one question — "is the work I started still the current work?" — and is bumped on the
|
|
6
|
+
two events that both invalidate in-flight work: a reactive `renew()` (a re-run supersedes the
|
|
7
|
+
prior promise/drain) and the enclosing OWNER's teardown (a settle after disposal must be
|
|
8
|
+
DROPPED, not run `place`/`insertBefore` on a now-detached anchor → NotFoundError). `onTeardown`
|
|
9
|
+
releases extra resources at teardown (e.g. an async iterator's `return()`).
|
|
10
|
+
|
|
11
|
+
Extracted from the byte-identical guard hand-rolled in `awaitBlock` and `eachAsync`. The
|
|
12
|
+
teardown half is the load-bearing one: a re-run bump alone leaves a promise/drain that settles
|
|
13
|
+
AFTER disposal to still pass the liveness check and crash on the dead anchor — the bug both
|
|
14
|
+
sites independently had to grow.
|
|
15
|
+
*/
|
|
16
|
+
export function generationGuard(onTeardown?: () => void): {
|
|
17
|
+
renew: () => number
|
|
18
|
+
token: () => number
|
|
19
|
+
live: (captured: number) => boolean
|
|
20
|
+
} {
|
|
21
|
+
let generation = 0
|
|
22
|
+
/* The teardown bump is load-bearing: without it a promise/drain settling after the owner
|
|
23
|
+
disposes still passes `live(captured)` and touches a detached anchor. */
|
|
24
|
+
if (OWNER.current !== undefined) {
|
|
25
|
+
OWNER.current.push(() => {
|
|
26
|
+
generation += 1
|
|
27
|
+
onTeardown?.()
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
/* Supersede any in-flight generation; call at the start of each (re-)run. Returns the
|
|
32
|
+
new token so a runner can capture it in the same statement. */
|
|
33
|
+
renew: () => (generation += 1),
|
|
34
|
+
/* The live generation to capture before an await and compare after — for when the
|
|
35
|
+
renew and the capture happen in different functions (as in awaitBlock's render). */
|
|
36
|
+
token: () => generation,
|
|
37
|
+
/* Whether `captured` is still the live generation — safe to touch the DOM. */
|
|
38
|
+
live: (captured: number) => captured === generation,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -34,7 +34,15 @@ export function runNode(node: ReactiveNode): unknown {
|
|
|
34
34
|
on read without re-running, never waking downstream. An effect has no value
|
|
35
35
|
worth comparing and no subscribers, so this is a no-op for it (its body ran
|
|
36
36
|
inside compute above). The subscriber list isn't re-linked here — only its
|
|
37
|
-
members' `status` is bumped — so walking it live is safe.
|
|
37
|
+
members' `status` is bumped — so walking it live is safe.
|
|
38
|
+
|
|
39
|
+
This bumps status directly, bypassing `mark` — it neither enqueues effects nor
|
|
40
|
+
propagates CHECK onward, yet nothing is missed: this recompute only happens
|
|
41
|
+
while settling a subscriber that was itself reached from the originating
|
|
42
|
+
write's `mark` propagation, which already CHECK-marked (and so enqueued) every
|
|
43
|
+
effect in this cone on their CLEAN→CHECK edge. So every subscriber here is
|
|
44
|
+
already CHECK/queued; the direct write only UPGRADES it CHECK→DIRTY so its own
|
|
45
|
+
settle recomputes instead of memoising back to CLEAN. */
|
|
38
46
|
if (!Object.is(node.value, next)) {
|
|
39
47
|
node.value = next
|
|
40
48
|
let link = node.subsHead
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { whenIdle } from './whenIdle.ts'
|
|
2
|
+
import { whenVisible } from './whenVisible.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Schedule a one-shot wake for a deferred (inert-hydrated) region by trigger — the single
|
|
6
|
+
trigger-selection both the await block and component islands share. `'visible'` (and `'auto'`)
|
|
7
|
+
wake on scroll-in when the DOM can be measured (a real IntersectionObserver) and the region has
|
|
8
|
+
an element to observe: a below-the-fold region decodes only when reached, one never scrolled to
|
|
9
|
+
costs nothing. Otherwise — `'idle'`, no observer, or an empty branch — wake on idle: off the
|
|
10
|
+
critical boot path but soon, so the region never lingers inert. Returns a cancel to drop a
|
|
11
|
+
pending wake on teardown.
|
|
12
|
+
|
|
13
|
+
`'auto'` is the await block's policy (defer decides, position decides trigger); `'idle'` /
|
|
14
|
+
`'visible'` are an island's explicit `client:` choice.
|
|
15
|
+
*/
|
|
16
|
+
export function scheduleWake(
|
|
17
|
+
trigger: 'idle' | 'visible' | 'auto',
|
|
18
|
+
element: Element | undefined,
|
|
19
|
+
wake: () => void,
|
|
20
|
+
): () => void {
|
|
21
|
+
const hasObserver =
|
|
22
|
+
typeof (globalThis as { IntersectionObserver?: unknown }).IntersectionObserver ===
|
|
23
|
+
'function'
|
|
24
|
+
if (trigger !== 'idle' && hasObserver && element !== undefined) {
|
|
25
|
+
return whenVisible(element, wake)
|
|
26
|
+
}
|
|
27
|
+
return whenIdle(wake)
|
|
28
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResumeEntry } from '../RESUME.ts'
|
|
1
|
+
import type { DeferMarker, ResumeEntry } from '../RESUME.ts'
|
|
2
2
|
|
|
3
3
|
/* One STREAMING await block captured during SSR (no `then` on the `await` tag): its
|
|
4
4
|
boundary id, the promise to await, and the async string-renderers for the resolved
|
|
@@ -18,10 +18,11 @@ export type SsrAwait = {
|
|
|
18
18
|
/* The result of a component's server `render()`: the pending-shell HTML, the
|
|
19
19
|
serializable document snapshot for client resume, the STREAMING await blocks to
|
|
20
20
|
flush out of order, and `resume` — the inline-rendered BLOCKING await values keyed
|
|
21
|
-
by boundary id, seeded into the manifest so hydration adopts them without a refetch.
|
|
21
|
+
by boundary id, seeded into the manifest so hydration adopts them without a refetch. A
|
|
22
|
+
deferred (cache-backed) blocking value is a `DeferMarker` in place of the value. */
|
|
22
23
|
export type SsrRender = {
|
|
23
24
|
html: string
|
|
24
25
|
state: unknown
|
|
25
26
|
awaits: SsrAwait[]
|
|
26
|
-
resume: Record<number, ResumeEntry>
|
|
27
|
+
resume: Record<number, ResumeEntry | DeferMarker>
|
|
27
28
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Run `callback` once, in the first idle gap after the current frame — the client-side
|
|
3
|
+
trigger a deferred (inert-hydrated) region uses to wake itself: boot stays cheap (no
|
|
4
|
+
decode, no effects), then the region belatedly comes alive off the critical path,
|
|
5
|
+
before a human can act on it. `requestIdleCallback` when the runtime has it (a `timeout`
|
|
6
|
+
guarantees it fires even under sustained load); a `setTimeout(0)` macrotask otherwise
|
|
7
|
+
(Safari < 16.4, test DOMs), which still clears the boot frame. Returns a cancel to drop
|
|
8
|
+
the pending wake on teardown, so a region torn down before idle does no late work.
|
|
9
|
+
*/
|
|
10
|
+
export function whenIdle(callback: () => void, timeoutMs = 200): () => void {
|
|
11
|
+
const globalWithIdle = globalThis as {
|
|
12
|
+
requestIdleCallback?: (cb: () => void, options?: { timeout: number }) => number
|
|
13
|
+
cancelIdleCallback?: (handle: number) => void
|
|
14
|
+
}
|
|
15
|
+
if (typeof globalWithIdle.requestIdleCallback === 'function') {
|
|
16
|
+
const handle = globalWithIdle.requestIdleCallback(callback, { timeout: timeoutMs })
|
|
17
|
+
return () => globalWithIdle.cancelIdleCallback?.(handle)
|
|
18
|
+
}
|
|
19
|
+
const handle = setTimeout(callback, 0)
|
|
20
|
+
return () => clearTimeout(handle)
|
|
21
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Run `callback` once, the first time `element` enters (or is already within) the viewport —
|
|
3
|
+
the trigger a below-the-fold deferred region uses to wake only when scrolled into view, so a
|
|
4
|
+
grid the user never reaches costs nothing. A synchronous fire where the observer is absent
|
|
5
|
+
(SSR/test DOMs), which keeps the region from staying inert where visibility can't be measured.
|
|
6
|
+
Returns a cancel to drop a pending watch on teardown. Fires at most once.
|
|
7
|
+
|
|
8
|
+
ONE shared IntersectionObserver backs every watcher (a grid of N islands registers N elements
|
|
9
|
+
on one observer, not N observers), and wakes are BATCHED off the observer callback into a
|
|
10
|
+
frame-flush capped at `WAKE_BUDGET_PER_FRAME`: a wake rebuilds DOM, so running it inside the
|
|
11
|
+
observer callback would force a synchronous layout that shifts other elements across the
|
|
12
|
+
margin and re-fires the observer — a cascade that locks the main thread at grid scale. Queuing
|
|
13
|
+
to a rAF and spending a bounded number per frame breaks that loop and keeps each frame cheap;
|
|
14
|
+
overflow spills to the next frame, so even a whole grid scrolled into view at once stays
|
|
15
|
+
responsive.
|
|
16
|
+
*/
|
|
17
|
+
// A screenful of margin so a region is live by the time it's actually read.
|
|
18
|
+
const ROOT_MARGIN = '256px'
|
|
19
|
+
// Max wakes (DOM rebuilds) per frame — the ceiling that keeps a flush off the critical path.
|
|
20
|
+
const WAKE_BUDGET_PER_FRAME = 40
|
|
21
|
+
|
|
22
|
+
let sharedObserver: IntersectionObserver | undefined
|
|
23
|
+
/* The IntersectionObserver constructor `sharedObserver` was built from. Identity only changes
|
|
24
|
+
when a test swaps `globalThis.IntersectionObserver`; on a change the observer + its pending
|
|
25
|
+
registrations are rebuilt, so module state doesn't leak across tests. */
|
|
26
|
+
let sharedObserverCtor: unknown
|
|
27
|
+
const wakeByElement = new Map<Element, () => void>()
|
|
28
|
+
const readyWakes = new Set<() => void>()
|
|
29
|
+
let flushScheduled = false
|
|
30
|
+
|
|
31
|
+
/* Next animation frame (browser) or macrotask (test/SSR) — the flush cadence. */
|
|
32
|
+
function scheduleFrame(run: () => void): void {
|
|
33
|
+
const globalWithRaf = globalThis as { requestAnimationFrame?: (cb: () => void) => number }
|
|
34
|
+
if (typeof globalWithRaf.requestAnimationFrame === 'function') {
|
|
35
|
+
globalWithRaf.requestAnimationFrame(run)
|
|
36
|
+
} else {
|
|
37
|
+
setTimeout(run, 0)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Spend up to the per-frame budget of queued wakes; reschedule if any remain. */
|
|
42
|
+
function flushWakes(): void {
|
|
43
|
+
flushScheduled = false
|
|
44
|
+
let processed = 0
|
|
45
|
+
for (const wake of readyWakes) {
|
|
46
|
+
readyWakes.delete(wake)
|
|
47
|
+
wake()
|
|
48
|
+
processed += 1
|
|
49
|
+
if (processed >= WAKE_BUDGET_PER_FRAME) {
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
scheduleFlush()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scheduleFlush(): void {
|
|
57
|
+
if (flushScheduled || readyWakes.size === 0) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
flushScheduled = true
|
|
61
|
+
scheduleFrame(flushWakes)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Queue every newly-intersecting element's wake — never run it here (see the cascade note). */
|
|
65
|
+
function onIntersections(entries: IntersectionObserverEntry[]): void {
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
if (entry.isIntersecting) {
|
|
68
|
+
const wake = wakeByElement.get(entry.target)
|
|
69
|
+
if (wake !== undefined) {
|
|
70
|
+
wakeByElement.delete(entry.target)
|
|
71
|
+
sharedObserver?.unobserve(entry.target)
|
|
72
|
+
readyWakes.add(wake)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
scheduleFlush()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function observerFor(observerConstructor: typeof IntersectionObserver): IntersectionObserver {
|
|
80
|
+
if (sharedObserver === undefined || sharedObserverCtor !== observerConstructor) {
|
|
81
|
+
wakeByElement.clear()
|
|
82
|
+
readyWakes.clear()
|
|
83
|
+
sharedObserver = new observerConstructor(onIntersections, { rootMargin: ROOT_MARGIN })
|
|
84
|
+
sharedObserverCtor = observerConstructor
|
|
85
|
+
}
|
|
86
|
+
return sharedObserver
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function whenVisible(element: Element, callback: () => void): () => void {
|
|
90
|
+
const observerConstructor = (
|
|
91
|
+
globalThis as { IntersectionObserver?: typeof IntersectionObserver }
|
|
92
|
+
).IntersectionObserver
|
|
93
|
+
if (typeof observerConstructor !== 'function') {
|
|
94
|
+
callback()
|
|
95
|
+
return () => undefined
|
|
96
|
+
}
|
|
97
|
+
const observer = observerFor(observerConstructor)
|
|
98
|
+
wakeByElement.set(element, callback)
|
|
99
|
+
observer.observe(element)
|
|
100
|
+
return () => {
|
|
101
|
+
wakeByElement.delete(element)
|
|
102
|
+
readyWakes.delete(callback)
|
|
103
|
+
observer.unobserve(element)
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/lib/ui/scope.ts
CHANGED
|
@@ -3,17 +3,15 @@ import { CURRENT_SCOPE } from './runtime/CURRENT_SCOPE.ts'
|
|
|
3
3
|
import type { Scope } from './types/Scope.ts'
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
Resolves
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
passable: hand it to a child or a helper and it can read/extend/undo that scope.
|
|
6
|
+
Resolves the current lexical scope — established per lexical level by the compiler,
|
|
7
|
+
so it reads "where you are" with no handle. Outside any scope (boot, a script) it
|
|
8
|
+
mints a detached root once and reuses it. The returned value is passable: hand it to
|
|
9
|
+
a child or a helper and it can read/extend/undo that scope (walk up via `.root()`).
|
|
11
10
|
*/
|
|
12
11
|
// @documentation reactive-state
|
|
13
|
-
export function scope(
|
|
12
|
+
export function scope(): Scope {
|
|
14
13
|
if (!CURRENT_SCOPE.current) {
|
|
15
14
|
CURRENT_SCOPE.current = createScope()
|
|
16
15
|
}
|
|
17
|
-
|
|
18
|
-
return address === '/' ? current.root() : current
|
|
16
|
+
return CURRENT_SCOPE.current
|
|
19
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { encodeRefJson } from '../shared/encodeRefJson.ts'
|
|
2
|
-
import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
2
|
+
import type { DeferMarker, ResumeEntry } from './runtime/RESUME.ts'
|
|
3
3
|
|
|
4
4
|
/* ref-json-encode an await-resume entry, or `undefined` if it can't be serialized.
|
|
5
5
|
encodeRefJson is total (cycles become back-references, functions fold to undefined),
|
|
@@ -7,7 +7,10 @@ import type { ResumeEntry } from './runtime/RESUME.ts'
|
|
|
7
7
|
entry and warn, so the client re-runs that one branch's promise while every other
|
|
8
8
|
branch stays seeded. Shared by the streaming (`renderToStream`) and buffered/seed
|
|
9
9
|
(`resumeSeedScript`) paths so the serialize-or-refetch policy lives in one place. */
|
|
10
|
-
export function tryEncodeResume(
|
|
10
|
+
export function tryEncodeResume(
|
|
11
|
+
entry: ResumeEntry | DeferMarker,
|
|
12
|
+
id: number | string,
|
|
13
|
+
): string | undefined {
|
|
11
14
|
try {
|
|
12
15
|
return encodeRefJson(entry)
|
|
13
16
|
} catch (cause) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { computed } from '../computed.ts'
|
|
2
2
|
import type { effect } from '../effect.ts'
|
|
3
3
|
import type { linked } from '../linked.ts'
|
|
4
|
-
import type { Cell } from '../runtime/types/Cell.ts'
|
|
5
4
|
import type { Patch } from '../runtime/types/Patch.ts'
|
|
6
5
|
import type { state } from '../state.ts'
|
|
7
6
|
import type { SyncTransport } from './SyncTransport.ts'
|
|
@@ -9,7 +8,7 @@ import type { SyncTransport } from './SyncTransport.ts'
|
|
|
9
8
|
/*
|
|
10
9
|
A lexical scope: the unit that owns a region's reactive data, its lifetime, and
|
|
11
10
|
the capabilities applied to it. Its data surface MIRRORS `Doc` (read/replace/add/
|
|
12
|
-
remove/
|
|
11
|
+
remove/derive/apply/snapshot) so the compiler can target a scope as a
|
|
13
12
|
component's data binding directly. It nests (`child`/`root`), passes values
|
|
14
13
|
down the tree as context (`share`/`shared`), and carries the capability surface as
|
|
15
14
|
methods so a scope is a passable value: `<Child parentScope={scope} />`.
|
|
@@ -33,7 +32,6 @@ export type Scope = {
|
|
|
33
32
|
add: (path: string, value: unknown) => void
|
|
34
33
|
remove: (path: string) => void
|
|
35
34
|
apply: (patch: Patch) => void
|
|
36
|
-
cell: <T>(path: string) => Cell<T>
|
|
37
35
|
derive: <T>(path: string, compute: () => T) => () => T
|
|
38
36
|
snapshot: () => unknown
|
|
39
37
|
/* The reactive primitives — namespaced under the scope, but AMBIENT-bound, not
|
|
@@ -64,7 +62,8 @@ export type Scope = {
|
|
|
64
62
|
/* context — values shared DOWN the tree (not in the reactive doc, which doesn't
|
|
65
63
|
inherit): `share` puts a named value on this scope; `shared` reads the closest
|
|
66
64
|
ancestor (self included) that has the key, undefined if none. The value is held
|
|
67
|
-
by reference, so reactive context = share a
|
|
65
|
+
by reference, so reactive context = share a scope (its doc is reactive), not a
|
|
66
|
+
plain object snapshot. */
|
|
68
67
|
share: (key: string, value: unknown) => void
|
|
69
68
|
shared: <T>(key: string) => T | undefined
|
|
70
69
|
/* capabilities — enable where the scope's changes go */
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ErrorConstructors } from '../../shared/types/ErrorConstructors.ts'
|
|
2
|
-
import type { ErrorSpec } from '../../shared/types/ErrorSpec.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Turns a rpc's declared `ErrorSpec` into the constructor object handed to the
|
|
6
|
-
handler (`(args, { errors })`). Each constructor stamps its name + status onto an
|
|
7
|
-
`ErrorDescriptor` carrying the call's `data`, which `error()` serializes as the
|
|
8
|
-
`{ $abideError, data }` body. Receiver-agnostic on data: a nullary error ignores
|
|
9
|
-
the (absent) argument.
|
|
10
|
-
*/
|
|
11
|
-
export function buildErrorConstructors<Spec extends ErrorSpec>(
|
|
12
|
-
spec: Spec,
|
|
13
|
-
): ErrorConstructors<Spec> {
|
|
14
|
-
const entries = Object.entries(spec).map(([name, { status }]) => [
|
|
15
|
-
name,
|
|
16
|
-
(data: unknown) => ({ $abideError: name, status, data }),
|
|
17
|
-
])
|
|
18
|
-
return Object.fromEntries(entries) as ErrorConstructors<Spec>
|
|
19
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ErrorDescriptor } from './ErrorDescriptor.ts'
|
|
2
|
-
import type { ErrorSpec } from './ErrorSpec.ts'
|
|
3
|
-
import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
The constructors handed to the handler via its second arg (`(args, { errors })`),
|
|
7
|
-
derived from the rpc's `ErrorSpec`. An entry with a `data` schema makes its
|
|
8
|
-
constructor require that schema's inferred input; an entry without one is nullary.
|
|
9
|
-
Each returns a typed `ErrorDescriptor` to pass to `error()`.
|
|
10
|
-
*/
|
|
11
|
-
export type ErrorConstructors<Spec extends ErrorSpec> = {
|
|
12
|
-
[Name in keyof Spec & string]: Spec[Name]['data'] extends StandardSchemaV1
|
|
13
|
-
? (
|
|
14
|
-
data: StandardSchemaV1.InferInput<Spec[Name]['data']>,
|
|
15
|
-
) => ErrorDescriptor<Name, StandardSchemaV1.InferInput<Spec[Name]['data']>>
|
|
16
|
-
: () => ErrorDescriptor<Name, undefined>
|
|
17
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
What a rpc error constructor returns: a plain descriptor (NOT a Response), so it
|
|
3
|
-
flows through the single `error()` funnel — `return error(errors.invalidCoupon({…}))`.
|
|
4
|
-
`error()` reads `status` off it and serializes `{ $abideError: name, data }` as the body.
|
|
5
|
-
*/
|
|
6
|
-
export type ErrorDescriptor<Name extends string = string, Data = unknown> = {
|
|
7
|
-
readonly $abideError: Name
|
|
8
|
-
readonly status: number
|
|
9
|
-
readonly data: Data
|
|
10
|
-
}
|