@abide/abide 0.44.0 → 0.44.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/CHANGELOG.md +24 -0
- package/package.json +1 -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/rpc/defineRpc.ts +9 -1
- package/src/lib/server/runtime/createServer.ts +29 -12
- package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/types/RequestStore.ts +16 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/sockets/defineSocket.ts +5 -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 +21 -1
- 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/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/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/types/DebugGate.ts +10 -0
- 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/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/parseTemplate.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
- 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/runtime/createDoc.ts +102 -12
- package/src/zodCjsPlugin.ts +16 -1
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { effect } from '../effect.ts'
|
|
2
|
+
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
3
|
+
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
+
import { scope } from '../runtime/scope.ts'
|
|
5
|
+
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
|
+
import { fillBefore } from './fillBefore.ts'
|
|
7
|
+
import { openMarker } from './openMarker.ts'
|
|
8
|
+
import { replaceRange } from './replaceRange.ts'
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
The shared lifecycle of every single-slot control-flow block (`when`, `switchBlock`):
|
|
12
|
+
a marker-bounded RANGE that holds at most one piece of content at a time, swapped as a
|
|
13
|
+
unit when a `key` changes. The block supplies a `key()` thunk (the identity of the
|
|
14
|
+
chosen branch — `'then'|'else'` for `when`, the case index for `switchBlock`) and a
|
|
15
|
+
`contentFor(key)` resolver returning that branch's builder, or `undefined` for an
|
|
16
|
+
empty branch. This module owns the marker setup, the hydrate-vs-create fork, and the
|
|
17
|
+
swap effect with its reentrancy/teardown dance — the structure both blocks copy-pasted.
|
|
18
|
+
|
|
19
|
+
The chosen branch builds through `scope` (directly on hydrate, via `fillBefore` on a
|
|
20
|
+
swap), which builds untracked — so a raw reactive read in the branch content doesn't
|
|
21
|
+
subscribe the swap effect; only `key()` (which reads the block's source) drives the
|
|
22
|
+
toggle. The branch's own interpolations still track, each through its own effect.
|
|
23
|
+
|
|
24
|
+
On hydrate it adopts the server-rendered range: claim the start marker, run the chosen
|
|
25
|
+
branch in place (its content claims the existing nodes), then claim the end marker. The
|
|
26
|
+
effect's first run sees the same key and is a no-op; later changes clear the range and
|
|
27
|
+
build fresh into a fragment.
|
|
28
|
+
|
|
29
|
+
`before` (a static node located by the skeleton) places the range among siblings on
|
|
30
|
+
create, so the block sits before a static suffix rather than at the parent's end.
|
|
31
|
+
Hydrate ignores it — the claim cursor (positioned past the prefix) drives placement.
|
|
32
|
+
*/
|
|
33
|
+
export function mountSwappableRange<Key>(
|
|
34
|
+
parent: Node,
|
|
35
|
+
key: () => Key,
|
|
36
|
+
contentFor: (key: Key) => ((parent: Node) => void) | undefined,
|
|
37
|
+
before: Node | null = null,
|
|
38
|
+
): void {
|
|
39
|
+
const hydration = RENDER.hydration
|
|
40
|
+
/* The live branch's scope, registered with the owner so it disposes on owner
|
|
41
|
+
teardown — not only on a branch swap via replaceRange. */
|
|
42
|
+
const group = scopeGroup()
|
|
43
|
+
let dispose: (() => void) | undefined
|
|
44
|
+
let activeKey: Key
|
|
45
|
+
let end: Comment
|
|
46
|
+
|
|
47
|
+
const start = openMarker(parent, RANGE_OPEN, before)
|
|
48
|
+
if (hydration !== undefined) {
|
|
49
|
+
activeKey = key()
|
|
50
|
+
const chosen = contentFor(activeKey)
|
|
51
|
+
if (chosen !== undefined) {
|
|
52
|
+
dispose = group.track(scope(() => chosen(parent))) // content claims the SSR nodes in place
|
|
53
|
+
}
|
|
54
|
+
end = openMarker(parent, RANGE_CLOSE)
|
|
55
|
+
} else {
|
|
56
|
+
end = openMarker(parent, RANGE_CLOSE, before)
|
|
57
|
+
activeKey = key()
|
|
58
|
+
const chosen = contentFor(activeKey)
|
|
59
|
+
if (chosen !== undefined) {
|
|
60
|
+
dispose = group.track(fillBefore(end, chosen))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
effect(() => {
|
|
65
|
+
const next = key()
|
|
66
|
+
if (next === activeKey) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
activeKey = next
|
|
70
|
+
const chosen = contentFor(next)
|
|
71
|
+
/* Null `dispose` before `replaceRange` builds the new branch: a reentrant swap
|
|
72
|
+
during that build (an effect in the new content writing the source) would
|
|
73
|
+
otherwise re-enter with the already-disposed disposer and clear it twice. */
|
|
74
|
+
const prior = dispose
|
|
75
|
+
dispose = undefined
|
|
76
|
+
const built = replaceRange(start, end, prior, chosen)
|
|
77
|
+
dispose = built !== undefined ? group.track(built) : undefined
|
|
78
|
+
})
|
|
79
|
+
}
|
|
@@ -129,10 +129,19 @@ export function skeleton(parent: Node, html: string): SkeletonHoles {
|
|
|
129
129
|
hydration.next.set(parent, node)
|
|
130
130
|
} else {
|
|
131
131
|
const children = source.childNodes
|
|
132
|
+
/* Stage clones in a fragment ONLY for a live (connected) parent, where one
|
|
133
|
+
append reflows once instead of per clone. A detached parent triggers no
|
|
134
|
+
reflow on append, so the fragment is pure overhead there — skip it and
|
|
135
|
+
append direct. `topLevel` collects each clone either way for anchor/
|
|
136
|
+
element-hole resolution. */
|
|
137
|
+
const target: Node = parent.isConnected ? document.createDocumentFragment() : parent
|
|
132
138
|
for (let index = 0; index < children.length; index += 1) {
|
|
133
139
|
const clone = (children[index] as Node).cloneNode(true)
|
|
134
140
|
topLevel.push(clone)
|
|
135
|
-
|
|
141
|
+
target.appendChild(clone)
|
|
142
|
+
}
|
|
143
|
+
if (target !== parent) {
|
|
144
|
+
parent.appendChild(target)
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
/* Anchor holes via the ONE shared ordering rule (`walkAnchorOrder`) — the same traversal
|
|
@@ -1,26 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
3
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
-
import { scope } from '../runtime/scope.ts'
|
|
5
|
-
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
|
-
import { fillBefore } from './fillBefore.ts'
|
|
7
|
-
import { openMarker } from './openMarker.ts'
|
|
8
|
-
import { replaceRange } from './replaceRange.ts'
|
|
1
|
+
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
9
2
|
import type { SwitchCase } from './types/SwitchCase.ts'
|
|
10
3
|
|
|
11
4
|
/*
|
|
12
5
|
Multi-branch binding — the runtime for `{#switch}` blocks and for `{#if}` chains that
|
|
13
6
|
include `{:elseif}` branches (compiled as a switch over `true` with Boolean-coerced
|
|
14
|
-
match thunks so the first truthy branch wins).
|
|
7
|
+
match thunks so the first truthy branch wins). A swappable range evaluates the
|
|
15
8
|
subject, picks the first case whose `match` equals it (strict `===`), falling back
|
|
16
9
|
to the default (`match` undefined); the chosen case's content lives in a RANGE
|
|
17
10
|
bounded by two comment markers, so a case holds any content. Staying on the same
|
|
18
11
|
case across a subject change leaves it mounted; switching clears the range and
|
|
19
|
-
builds the new case fresh.
|
|
20
|
-
|
|
21
|
-
On hydrate it adopts the case the server rendered: claim the start marker, run the
|
|
22
|
-
matching case in place, claim the end marker. The effect's first run picks the same
|
|
23
|
-
case and is a no-op; later changes swap the range.
|
|
12
|
+
builds the new case fresh. See `mountSwappableRange` for the shared
|
|
13
|
+
hydrate/swap/teardown semantics — the case index is the swap key.
|
|
24
14
|
*/
|
|
25
15
|
// @documentation plumbing
|
|
26
16
|
export function switchBlock(
|
|
@@ -29,59 +19,21 @@ export function switchBlock(
|
|
|
29
19
|
cases: SwitchCase[],
|
|
30
20
|
before: Node | null = null,
|
|
31
21
|
): void {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
teardown — not only when the subject switches cases via replaceRange. */
|
|
35
|
-
const group = scopeGroup()
|
|
36
|
-
let dispose: (() => void) | undefined
|
|
37
|
-
let activeIndex: number
|
|
38
|
-
let end: Comment
|
|
39
|
-
|
|
22
|
+
/* Pick the first case matching the subject (`===`), else the default (`match`
|
|
23
|
+
undefined), else -1 for no match. */
|
|
40
24
|
const select = (value: unknown): number => {
|
|
41
25
|
const matched = cases.findIndex(
|
|
42
26
|
(entry) => entry.match !== undefined && entry.match() === value,
|
|
43
27
|
)
|
|
44
28
|
return matched === -1 ? cases.findIndex((entry) => entry.match === undefined) : matched
|
|
45
29
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const start = openMarker(parent, RANGE_OPEN, before)
|
|
56
|
-
if (hydration !== undefined) {
|
|
57
|
-
activeIndex = select(subject())
|
|
58
|
-
const chosen = caseAt(activeIndex)
|
|
59
|
-
if (chosen !== undefined) {
|
|
60
|
-
dispose = group.track(scope(() => chosen.render(parent))) // claim the SSR nodes in place
|
|
61
|
-
}
|
|
62
|
-
end = openMarker(parent, RANGE_CLOSE)
|
|
63
|
-
} else {
|
|
64
|
-
end = openMarker(parent, RANGE_CLOSE, before)
|
|
65
|
-
activeIndex = select(subject())
|
|
66
|
-
const chosen = caseAt(activeIndex)
|
|
67
|
-
if (chosen !== undefined) {
|
|
68
|
-
dispose = group.track(fillBefore(end, (p) => chosen.render(p)))
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
effect(() => {
|
|
73
|
-
const index = select(subject())
|
|
74
|
-
if (index === activeIndex) {
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
activeIndex = index
|
|
78
|
-
const chosen = caseAt(index)
|
|
79
|
-
/* Null `dispose` before `replaceRange` builds the new case: a reentrant switch
|
|
80
|
-
during that build (an effect in the new content writing the subject) would
|
|
81
|
-
otherwise re-enter with the already-disposed disposer and clear it twice. */
|
|
82
|
-
const prior = dispose
|
|
83
|
-
dispose = undefined
|
|
84
|
-
const next = replaceRange(start, end, prior, chosen && ((p) => chosen.render(p)))
|
|
85
|
-
dispose = next !== undefined ? group.track(next) : undefined
|
|
86
|
-
})
|
|
30
|
+
mountSwappableRange(
|
|
31
|
+
parent,
|
|
32
|
+
() => select(subject()),
|
|
33
|
+
(index) => {
|
|
34
|
+
const chosen = index === -1 ? undefined : cases[index]
|
|
35
|
+
return chosen && ((p) => chosen.render(p))
|
|
36
|
+
},
|
|
37
|
+
before,
|
|
38
|
+
)
|
|
87
39
|
}
|
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
3
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
-
import { scope } from '../runtime/scope.ts'
|
|
5
|
-
import { scopeGroup } from '../runtime/scopeGroup.ts'
|
|
6
|
-
import { fillBefore } from './fillBefore.ts'
|
|
7
|
-
import { openMarker } from './openMarker.ts'
|
|
8
|
-
import { replaceRange } from './replaceRange.ts'
|
|
1
|
+
import { mountSwappableRange } from './mountSwappableRange.ts'
|
|
9
2
|
|
|
10
3
|
/*
|
|
11
4
|
Conditional binding — the runtime for `<template if>` (with optional `else`). The
|
|
12
5
|
branch's content lives in a RANGE bounded by two comment markers, so a branch may
|
|
13
6
|
hold anything — elements, components, text, nested control-flow, snippets — not
|
|
14
|
-
just element roots.
|
|
15
|
-
on a truthy↔falsy flip (`render` truthy, `renderElse` falsy); an
|
|
16
|
-
condition is a no-op.
|
|
17
|
-
|
|
18
|
-
On hydrate it adopts the server-rendered range: claim the start marker, run the
|
|
19
|
-
matching render in place (its content claims the existing nodes), then claim the
|
|
20
|
-
end marker. The effect's first run sees the same branch and is a no-op; later
|
|
21
|
-
toggles clear the range and build fresh into a fragment.
|
|
7
|
+
just element roots. A 2-case swappable range tracks `condition()` and swaps the
|
|
8
|
+
range's content on a truthy↔falsy flip (`render` truthy, `renderElse` falsy); an
|
|
9
|
+
unchanged condition is a no-op. See `mountSwappableRange` for the shared
|
|
10
|
+
hydrate/swap/teardown semantics.
|
|
22
11
|
*/
|
|
23
12
|
// @documentation plumbing
|
|
24
13
|
export function when(
|
|
@@ -28,52 +17,10 @@ export function when(
|
|
|
28
17
|
renderElse?: (parent: Node) => void,
|
|
29
18
|
before: Node | null = null,
|
|
30
19
|
): void {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/* The live branch's scope, registered with the owner so it disposes on owner
|
|
38
|
-
teardown — not only on a branch flip via replaceRange. */
|
|
39
|
-
const group = scopeGroup()
|
|
40
|
-
let dispose: (() => void) | undefined
|
|
41
|
-
let activeBranch: 'then' | 'else'
|
|
42
|
-
let end: Comment
|
|
43
|
-
|
|
44
|
-
/* `before` (a static node located by the skeleton) places the range among siblings on
|
|
45
|
-
create, so the block sits before a static suffix rather than at the parent's end.
|
|
46
|
-
Hydrate ignores it — the claim cursor (positioned past the prefix) drives placement. */
|
|
47
|
-
const start = openMarker(parent, RANGE_OPEN, before)
|
|
48
|
-
if (hydration !== undefined) {
|
|
49
|
-
activeBranch = condition() ? 'then' : 'else'
|
|
50
|
-
const chosen = chosenFor(activeBranch)
|
|
51
|
-
if (chosen !== undefined) {
|
|
52
|
-
dispose = group.track(scope(() => chosen(parent))) // content claims the SSR nodes in place
|
|
53
|
-
}
|
|
54
|
-
end = openMarker(parent, RANGE_CLOSE)
|
|
55
|
-
} else {
|
|
56
|
-
end = openMarker(parent, RANGE_CLOSE, before)
|
|
57
|
-
activeBranch = condition() ? 'then' : 'else'
|
|
58
|
-
const chosen = chosenFor(activeBranch)
|
|
59
|
-
if (chosen !== undefined) {
|
|
60
|
-
dispose = group.track(fillBefore(end, chosen))
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
effect(() => {
|
|
65
|
-
const branch = condition() ? 'then' : 'else'
|
|
66
|
-
if (branch === activeBranch) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
activeBranch = branch
|
|
70
|
-
const chosen = chosenFor(branch)
|
|
71
|
-
/* Null `dispose` before `replaceRange` builds the new branch: a reentrant flip
|
|
72
|
-
during that build (an effect in the new content writing the condition) would
|
|
73
|
-
otherwise re-enter with the already-disposed disposer and clear it twice. */
|
|
74
|
-
const prior = dispose
|
|
75
|
-
dispose = undefined
|
|
76
|
-
const next = replaceRange(start, end, prior, chosen)
|
|
77
|
-
dispose = next !== undefined ? group.track(next) : undefined
|
|
78
|
-
})
|
|
20
|
+
mountSwappableRange(
|
|
21
|
+
parent,
|
|
22
|
+
() => (condition() ? 'then' : 'else'),
|
|
23
|
+
(branch) => (branch === 'then' ? render : renderElse),
|
|
24
|
+
before,
|
|
25
|
+
)
|
|
79
26
|
}
|
|
@@ -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
|
}
|
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,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
|
-
}
|