@abide/abide 0.44.0 → 0.45.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 +64 -0
- package/README.md +171 -181
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +6 -1
- package/src/devEntry.ts +43 -9
- package/src/lib/cli/parseArgvForRpc.ts +21 -24
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- 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 +11 -9
- 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 +36 -12
- package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/installAmbientScopeStore.ts +33 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- 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 +25 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/activeCacheStore.ts +5 -14
- package/src/lib/shared/activePage.ts +5 -19
- package/src/lib/shared/augmentModule.ts +18 -0
- package/src/lib/shared/basePath.ts +6 -5
- package/src/lib/shared/baseResolver.ts +10 -0
- package/src/lib/shared/baseSlot.ts +6 -12
- package/src/lib/shared/cache.ts +46 -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/cacheStoreResolver.ts +12 -0
- package/src/lib/shared/cacheStoreSlot.ts +5 -13
- package/src/lib/shared/changeAffectsClient.ts +35 -0
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/emitLogRecord.ts +18 -4
- package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
- package/src/lib/shared/httpErrorFor.ts +1 -1
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- package/src/lib/shared/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/recordCacheKey.ts +6 -0
- package/src/lib/shared/requestScopeResolver.ts +12 -0
- package/src/lib/shared/requestScopeSlot.ts +7 -12
- package/src/lib/shared/responseBodyKind.ts +35 -0
- package/src/lib/shared/setBaseResolver.ts +2 -4
- package/src/lib/shared/setCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setPageResolver.ts +2 -5
- package/src/lib/shared/setRequestScopeResolver.ts +3 -5
- 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/DebugGate.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +6 -6
- package/src/lib/shared/types/RemoteFunction.ts +8 -4
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/writeDts.ts +8 -1
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +12 -16
- package/src/lib/shared/writeRpcDts.ts +13 -12
- package/src/lib/shared/writeTestRpcDts.ts +11 -11
- package/src/lib/shared/writeTestSocketsDts.ts +9 -9
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +9 -7
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +24 -12
- package/src/lib/ui/compile/compileModule.ts +37 -16
- package/src/lib/ui/compile/compileShadow.ts +75 -1
- package/src/lib/ui/compile/createShadowLanguageService.ts +29 -3
- package/src/lib/ui/compile/generateBuild.ts +7 -1
- package/src/lib/ui/compile/generateSSR.ts +10 -3
- package/src/lib/ui/compile/parseTemplate.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
- package/src/lib/ui/createScope.ts +11 -4
- package/src/lib/ui/deferResume.ts +29 -0
- package/src/lib/ui/dom/appendText.ts +19 -4
- package/src/lib/ui/dom/awaitBlock.ts +74 -26
- package/src/lib/ui/dom/eachAsync.ts +11 -18
- package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +15 -63
- package/src/lib/ui/dom/when.ts +11 -64
- 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/createDoc.ts +102 -12
- 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/types/SsrRender.ts +4 -3
- 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/zodCjsPlugin.ts +16 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +0 -19
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
- package/src/lib/shared/types/ErrorConstructors.ts +0 -17
- package/src/lib/shared/types/ErrorDescriptor.ts +0 -10
|
@@ -43,6 +43,18 @@ the change.
|
|
|
43
43
|
export function createDoc(initial: unknown): Doc {
|
|
44
44
|
let tree = initial
|
|
45
45
|
const nodes = new Map<string, ReactiveNode>()
|
|
46
|
+
/* Prefix index: every path → the set of its DIRECT children that lie on a route
|
|
47
|
+
to some minted node (a trie over the minted paths' ancestor chains). A
|
|
48
|
+
structural descend walks this from the change root through actual descendants
|
|
49
|
+
only, replacing the old O(all live cells) `startsWith` scan over the whole
|
|
50
|
+
`nodes` map. The links span INTERMEDIATE paths that have no node of their own
|
|
51
|
+
(e.g. `byId` and `byId/<key>` exist as links even when only `byId/<key>/n` was
|
|
52
|
+
read), so a descend from any ancestor reaches the leaf. Maintained in lockstep
|
|
53
|
+
with `nodes`: a node's whole ancestor chain is linked when `nodeFor` mints it,
|
|
54
|
+
unlinked from the leaf up when `wakeSubtree` evicts it. Membership of a path in
|
|
55
|
+
`nodes` (does it carry a signal?) is independent of its membership here (is it
|
|
56
|
+
on a route to one?). */
|
|
57
|
+
const childKeys = new Map<string, Set<string>>()
|
|
46
58
|
/* Computed slots: a path whose value is a function of other paths, not stored
|
|
47
59
|
truth. Held apart from `nodes` so the structural wake/eviction never touches
|
|
48
60
|
them; their dirtiness is driven entirely by the deps they read (the signal
|
|
@@ -54,15 +66,65 @@ export function createDoc(initial: unknown): Doc {
|
|
|
54
66
|
name the document it came from (reference identity, the undo/persistence key). */
|
|
55
67
|
let self: Doc
|
|
56
68
|
|
|
69
|
+
/* Links `path` into the trie under its parent and continues up the ancestor chain.
|
|
70
|
+
Stops at the first ancestor whose parent set already lists it — that link, and
|
|
71
|
+
everything above it, is already present (some sibling minted the upper chain). */
|
|
72
|
+
function linkPath(path: string): void {
|
|
73
|
+
let child = path
|
|
74
|
+
for (;;) {
|
|
75
|
+
const parentPath = parentPathOf(child)
|
|
76
|
+
const siblings = childKeys.get(parentPath)
|
|
77
|
+
if (siblings === undefined) {
|
|
78
|
+
childKeys.set(parentPath, new Set([child]))
|
|
79
|
+
} else if (siblings.has(child)) {
|
|
80
|
+
return
|
|
81
|
+
} else {
|
|
82
|
+
siblings.add(child)
|
|
83
|
+
}
|
|
84
|
+
if (parentPath === '') {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
child = parentPath
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
57
91
|
function nodeFor(path: string): ReactiveNode {
|
|
58
92
|
let node = nodes.get(path)
|
|
59
93
|
if (node === undefined) {
|
|
60
94
|
node = createSignalNode(walkPath(tree, path).value)
|
|
61
95
|
nodes.set(path, node)
|
|
96
|
+
linkPath(path)
|
|
62
97
|
}
|
|
63
98
|
return node
|
|
64
99
|
}
|
|
65
100
|
|
|
101
|
+
/* Drops a node and unlinks it from the trie, pruning each emptied ancestor link up
|
|
102
|
+
the chain — the eviction half of the lockstep with `nodeFor`. A link is removed
|
|
103
|
+
only once its child set empties, so an intermediate path with other live
|
|
104
|
+
descendants keeps its place. */
|
|
105
|
+
function evict(path: string): void {
|
|
106
|
+
nodes.delete(path)
|
|
107
|
+
let child = path
|
|
108
|
+
for (;;) {
|
|
109
|
+
const parentPath = parentPathOf(child)
|
|
110
|
+
const siblings = childKeys.get(parentPath)
|
|
111
|
+
if (siblings === undefined) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
siblings.delete(child)
|
|
115
|
+
/* A parent still listing other children, or still carrying its own node,
|
|
116
|
+
stays — only a fully empty, node-less link is pruned and walked past. */
|
|
117
|
+
if (siblings.size > 0) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
childKeys.delete(parentPath)
|
|
121
|
+
if (parentPath === '' || nodes.has(parentPath)) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
child = parentPath
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
66
128
|
function read<T>(path: string): T {
|
|
67
129
|
/* Size-gated so a doc with no computed slots pays nothing on the stored hot
|
|
68
130
|
path — only one `.size` check, not a `.get` per read. */
|
|
@@ -109,25 +171,53 @@ export function createDoc(initial: unknown): Doc {
|
|
|
109
171
|
return
|
|
110
172
|
}
|
|
111
173
|
const prefix = rootPath === '' ? '' : `${rootPath}/`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
174
|
+
/* Walk the prefix index from `rootPath` over its real descendants only — a
|
|
175
|
+
BFS over the parent→children adjacency map — instead of scanning every live
|
|
176
|
+
node. `pending` holds parent-paths whose direct children are yet to visit;
|
|
177
|
+
each visited child that is itself a container is enqueued. The total work is
|
|
178
|
+
the size of the changed subtree, not the whole doc. */
|
|
179
|
+
const pending: string[] = [rootPath]
|
|
180
|
+
while (pending.length > 0) {
|
|
181
|
+
const parentPath = pending.pop() as string
|
|
182
|
+
const siblings = childKeys.get(parentPath)
|
|
183
|
+
if (siblings === undefined) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
/* Snapshot the set before iterating: eviction below mutates it, and a
|
|
187
|
+
deleted child must not skip its successor (Set iteration is order-
|
|
188
|
+
sensitive to in-place deletes). */
|
|
189
|
+
for (const candidate of [...siblings]) {
|
|
121
190
|
/* Walk from the already-resolved container (`rootValue`) using only the
|
|
122
191
|
path SUFFIX past the shared prefix, instead of re-walking every
|
|
123
192
|
candidate's full path from the tree root — the prefix is walked once
|
|
124
193
|
per wake, not once per descendant. */
|
|
125
194
|
const walk = walkPath(rootValue, candidate.slice(prefix.length))
|
|
195
|
+
/* An INTERMEDIATE link (a path on a route to a node but carrying none of
|
|
196
|
+
its own — e.g. `byId/<key>` when only `byId/<key>/n` was read) has no
|
|
197
|
+
signal to wake; it exists only to be descended through. So the
|
|
198
|
+
wake/evict touches a node only when one is present, while the descend
|
|
199
|
+
decision below is independent of node existence. */
|
|
200
|
+
const node = nodes.get(candidate)
|
|
126
201
|
if (walk.exists) {
|
|
127
|
-
|
|
202
|
+
if (node !== undefined) {
|
|
203
|
+
writeNode(node, walk.value)
|
|
204
|
+
}
|
|
205
|
+
/* Descend into a container child to reach its own descendants. */
|
|
206
|
+
if (walk.value !== null && typeof walk.value === 'object') {
|
|
207
|
+
pending.push(candidate)
|
|
208
|
+
}
|
|
128
209
|
} else {
|
|
129
|
-
|
|
130
|
-
|
|
210
|
+
/* A descendant whose path the mutation removed — a deleted key, an
|
|
211
|
+
out-of-range index after a shrink — is woken to undefined, then
|
|
212
|
+
dropped from the registry. The woken reader re-mints a fresh node
|
|
213
|
+
on its flush if the path ever returns. A removed container's own
|
|
214
|
+
descendants are still enqueued (whether or not THIS path held a
|
|
215
|
+
node) so they too are woken/evicted. */
|
|
216
|
+
if (node !== undefined) {
|
|
217
|
+
writeNode(node, undefined)
|
|
218
|
+
evict(candidate)
|
|
219
|
+
}
|
|
220
|
+
pending.push(candidate)
|
|
131
221
|
}
|
|
132
222
|
}
|
|
133
223
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CURRENT_SCOPE } from './CURRENT_SCOPE.ts'
|
|
2
|
+
import { inScope } from './inScope.ts'
|
|
1
3
|
import { NODE_STATE } from './NODE_STATE.ts'
|
|
2
4
|
import { OWNER } from './OWNER.ts'
|
|
3
5
|
import { runNode } from './runNode.ts'
|
|
@@ -23,13 +25,17 @@ export function createEffectNode(fn: () => EffectResult): () => void {
|
|
|
23
25
|
ReactiveNode so signals and computeds — which share the node shape and the
|
|
24
26
|
read/write hot path — pay nothing for a feature only effects use. */
|
|
25
27
|
let cleanup: Teardown | undefined
|
|
28
|
+
/* The teardown fires deferred (before a re-run, on dispose) when the ambient scope
|
|
29
|
+
has moved on; pin the one current at creation so an ambient `scope()` inside it
|
|
30
|
+
resolves the owning component, matching how `attach` pins its teardown. */
|
|
31
|
+
const captured = CURRENT_SCOPE.current
|
|
26
32
|
/* Runs the previous run's teardown before re-running and on dispose, exactly
|
|
27
33
|
once each. */
|
|
28
34
|
const runCleanup = (): void => {
|
|
29
35
|
if (cleanup !== undefined) {
|
|
30
36
|
const teardown = cleanup
|
|
31
37
|
cleanup = undefined
|
|
32
|
-
teardown
|
|
38
|
+
inScope(captured, teardown)
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
const node: ReactiveNode = {
|
|
@@ -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
|
|
@@ -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
|
}
|
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 */
|
package/src/zodCjsPlugin.ts
CHANGED
|
@@ -20,6 +20,14 @@ the bug while keeping zod v4. Server-only: the client bundle strips zod from rpc
|
|
|
20
20
|
modules (they become remote proxies), and dev never bundles.
|
|
21
21
|
*/
|
|
22
22
|
export function zodCjsPlugin(cwd: string): BunPlugin {
|
|
23
|
+
/*
|
|
24
|
+
The (specifier, importer-dir) → cjs path mapping is deterministic per build —
|
|
25
|
+
zod's location doesn't move mid-build, and the filter fires for every zod
|
|
26
|
+
import in every module. Memoise on a `${path}\0${from}` key (mirrors the
|
|
27
|
+
resolver's resolveExtension cache) so the fs round-trips (resolveSync +
|
|
28
|
+
existsSync) run once per unique pair instead of once per import.
|
|
29
|
+
*/
|
|
30
|
+
const cjsPathCache = new Map<string, string>()
|
|
23
31
|
return {
|
|
24
32
|
name: 'abide-zod-cjs',
|
|
25
33
|
setup(build) {
|
|
@@ -27,9 +35,16 @@ export function zodCjsPlugin(cwd: string): BunPlugin {
|
|
|
27
35
|
/* Resolve from the importer (or cwd for the entry) to zod's ESM
|
|
28
36
|
target, then swap to its `.cjs` sibling when one exists. */
|
|
29
37
|
const from = args.importer ? dirname(args.importer) : cwd
|
|
38
|
+
const key = `${args.path}\0${from}`
|
|
39
|
+
const cached = cjsPathCache.get(key)
|
|
40
|
+
if (cached !== undefined) {
|
|
41
|
+
return { path: cached }
|
|
42
|
+
}
|
|
30
43
|
const resolved = Bun.resolveSync(args.path, from)
|
|
31
44
|
const cjs = resolved.replace(/\.js$/, '.cjs')
|
|
32
|
-
|
|
45
|
+
const path = existsSync(cjs) ? cjs : resolved
|
|
46
|
+
cjsPathCache.set(key, path)
|
|
47
|
+
return { path }
|
|
33
48
|
})
|
|
34
49
|
},
|
|
35
50
|
}
|
|
@@ -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,16 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
One DEBUG pattern against one channel name, npm-debug conventions:
|
|
3
|
-
`*` matches everything, `abide:*` matches 'abide' and every 'abide:…'
|
|
4
|
-
sub-channel, anything else matches exactly. Negation (`-` prefix) is the
|
|
5
|
-
caller's concern — patterns arrive here already stripped.
|
|
6
|
-
*/
|
|
7
|
-
export function matchesDebugPattern(name: string, pattern: string): boolean {
|
|
8
|
-
if (pattern === '*') {
|
|
9
|
-
return true
|
|
10
|
-
}
|
|
11
|
-
if (pattern.endsWith(':*')) {
|
|
12
|
-
const prefix = pattern.slice(0, -2)
|
|
13
|
-
return name === prefix || name.startsWith(`${prefix}:`)
|
|
14
|
-
}
|
|
15
|
-
return pattern === name
|
|
16
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
DEBUG's comma list, parsed once per distinct string — the gate runs on every
|
|
3
|
-
log emission, and the env never changes server-side. A single-slot memo
|
|
4
|
-
suffices: the browser's abide-debug localStorage toggle produces a new string
|
|
5
|
-
that misses the memo and reparses.
|
|
6
|
-
*/
|
|
7
|
-
let lastEnv: string | undefined
|
|
8
|
-
let lastPatterns: string[] = []
|
|
9
|
-
|
|
10
|
-
export function parseDebugPatterns(env: string | undefined): string[] {
|
|
11
|
-
if (env !== lastEnv) {
|
|
12
|
-
lastEnv = env
|
|
13
|
-
lastPatterns = env
|
|
14
|
-
? env
|
|
15
|
-
.split(',')
|
|
16
|
-
.map((raw) => raw.trim())
|
|
17
|
-
.filter((pattern) => pattern !== '')
|
|
18
|
-
: []
|
|
19
|
-
}
|
|
20
|
-
return lastPatterns
|
|
21
|
-
}
|
|
@@ -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
|
-
}
|