@abide/abide 0.51.0 → 0.52.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 +509 -525
- package/CHANGELOG.md +29 -0
- package/README.md +128 -157
- package/package.json +4 -1
- package/src/abideResolverPlugin.ts +12 -0
- package/src/checkAbide.ts +60 -7
- package/src/lib/cli/completeCli.ts +38 -0
- package/src/lib/cli/printTopLevelHelp.ts +1 -0
- package/src/lib/cli/renderCliCompletions.ts +56 -0
- package/src/lib/cli/runCli.ts +27 -0
- package/src/lib/server/render.ts +70 -0
- package/src/lib/server/runtime/cacheStalenessBroadcaster.ts +33 -0
- package/src/lib/server/runtime/createServer.ts +43 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +37 -0
- package/src/lib/server/runtime/pageRenderSlot.ts +15 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
- package/src/lib/server/sockets/registerSocket.ts +12 -0
- package/src/lib/shared/CACHE_STALENESS_SOCKET.ts +8 -0
- package/src/lib/shared/RESERVED_SOCKET_PREFIX.ts +8 -0
- package/src/lib/shared/applyCacheStalenessLocally.ts +18 -0
- package/src/lib/shared/attachRpcSelectorMethods.ts +3 -1
- package/src/lib/shared/cache.ts +32 -4
- package/src/lib/shared/cacheStalenessSlot.ts +21 -0
- package/src/lib/shared/createRpcServerProgram.ts +187 -0
- package/src/lib/shared/docSnapshotsSlot.ts +13 -0
- package/src/lib/shared/invalidate.ts +39 -0
- package/src/lib/shared/matcherFromEnvelope.ts +31 -0
- package/src/lib/shared/prepareRpcModule.ts +22 -3
- package/src/lib/shared/refresh.ts +9 -2
- package/src/lib/shared/selectorMatcher.ts +2 -15
- package/src/lib/shared/serializeSelector.ts +69 -0
- package/src/lib/shared/setsIntersect.ts +15 -0
- package/src/lib/shared/types/CacheStalenessApply.ts +13 -0
- package/src/lib/shared/types/CacheStalenessFrame.ts +24 -0
- package/src/lib/shared/types/DocSnapshots.ts +12 -0
- package/src/lib/shared/types/RemoteFunction.ts +11 -8
- package/src/lib/shared/types/RpcBuildStamps.ts +9 -0
- package/src/lib/shared/types/SsrPayload.ts +5 -0
- package/src/lib/test/createTestApp.ts +15 -0
- package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +19 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/compileShadow.ts +92 -14
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -14
- package/src/lib/ui/compile/lowerScript.ts +6 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +65 -0
- package/src/lib/ui/compile/wrapReactionCellSources.ts +32 -0
- package/src/lib/ui/createScope.ts +42 -1
- package/src/lib/ui/dom/assertClaimedText.ts +14 -4
- package/src/lib/ui/dom/attr.ts +25 -2
- package/src/lib/ui/dom/writeCell.ts +22 -0
- package/src/lib/ui/runtime/DOC_SEED.ts +11 -0
- package/src/lib/ui/runtime/claimExpected.ts +6 -1
- package/src/lib/ui/runtime/reportHydrationDivergence.ts +30 -0
- package/src/lib/ui/startClient.ts +22 -1
- package/src/lib/ui/subscribeCacheStaleness.ts +85 -0
- package/src/serverEntry.ts +12 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { applyCacheStalenessLocally } from '../shared/applyCacheStalenessLocally.ts'
|
|
2
|
+
import { cacheStalenessSlot } from '../shared/cacheStalenessSlot.ts'
|
|
1
3
|
import { cacheStoreSlot } from '../shared/cacheStoreSlot.ts'
|
|
2
4
|
import { createCacheStore } from '../shared/createCacheStore.ts'
|
|
3
5
|
import { pageSlot } from '../shared/pageSlot.ts'
|
|
@@ -8,9 +10,11 @@ import { probeNavigation } from './probeNavigation.ts'
|
|
|
8
10
|
import { router } from './router.ts'
|
|
9
11
|
import { CELL_SEED } from './runtime/CELL_SEED.ts'
|
|
10
12
|
import { clientPage } from './runtime/clientPage.ts'
|
|
13
|
+
import { DOC_SEED } from './runtime/DOC_SEED.ts'
|
|
11
14
|
import type { RouteLoader } from './runtime/types/RouteLoader.ts'
|
|
12
15
|
import { seedBootState } from './seedBootState.ts'
|
|
13
16
|
import { seedStreamedResolution } from './seedStreamedResolution.ts'
|
|
17
|
+
import { subscribeCacheStaleness } from './subscribeCacheStaleness.ts'
|
|
14
18
|
|
|
15
19
|
/*
|
|
16
20
|
The official abide-ui client entry. Reads the server's `window.__SSR__` payload,
|
|
@@ -49,6 +53,10 @@ export function startClient(
|
|
|
49
53
|
cacheStoreSlot.resolver = () => store
|
|
50
54
|
/* One tab store: cache(fn, { shared: true }) shares it, so shared is a no-op here. */
|
|
51
55
|
sharedCacheStoreSlot.resolver = () => store
|
|
56
|
+
/* invalidate()/refresh() apply to THIS tab's cache (the server entry installs a
|
|
57
|
+
broadcaster instead — the ADR-0041 side-swap). Same function as the slot fallback so
|
|
58
|
+
the local-apply path can't diverge. */
|
|
59
|
+
cacheStalenessSlot.resolver = () => applyCacheStalenessLocally
|
|
52
60
|
/* Seed both SSR cache partitions through the one streamed-resolution sink: `ssr.cache`
|
|
53
61
|
(inline — reads settled at render-return, in __SSR__) and `__abideResumeCache` (pending
|
|
54
62
|
{#await} reads whose `__abideResolve(...)` chunks the stream pushed during parse, before
|
|
@@ -66,6 +74,12 @@ export function startClient(
|
|
|
66
74
|
if (ssr.cells !== undefined) {
|
|
67
75
|
Object.assign(CELL_SEED, ssr.cells)
|
|
68
76
|
}
|
|
77
|
+
/* Seed the doc-state warm partition into DOC_SEED before mount: a hydrating `createScope` reads
|
|
78
|
+
its render-path key to adopt the SSR doc snapshot, so a plain `state(initial)` keeps the server
|
|
79
|
+
value (the consume-once swap in `scope.replace`) instead of re-running a divergent init. */
|
|
80
|
+
if (ssr.docs !== undefined) {
|
|
81
|
+
Object.assign(DOC_SEED, ssr.docs)
|
|
82
|
+
}
|
|
69
83
|
/* Keep the cache channel live past boot: replace the head's buffering collector with
|
|
70
84
|
the store-connected sink so a post-boot resolution — the inline doc-stream cache
|
|
71
85
|
script — seeds the store through `seedStreamedResolution` directly instead of pushing
|
|
@@ -74,5 +88,12 @@ export function startClient(
|
|
|
74
88
|
;(globalThis as { __abideResolve?: (resolution: StreamedResolution) => void }).__abideResolve =
|
|
75
89
|
seedStreamedResolution
|
|
76
90
|
|
|
77
|
-
|
|
91
|
+
/* Subscribe to the reserved cache-staleness pipe (ADR-0041) AFTER seeding, so a server
|
|
92
|
+
broadcast drops/refetches this tab's freshly-hydrated cache — live-only, never replay. */
|
|
93
|
+
const disposeStaleness = subscribeCacheStaleness()
|
|
94
|
+
const disposeRouter = router(target, routes, layoutRoutes, probeNavigation)
|
|
95
|
+
return () => {
|
|
96
|
+
disposeStaleness()
|
|
97
|
+
disposeRouter()
|
|
98
|
+
}
|
|
78
99
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { abideLog } from '../shared/abideLog.ts'
|
|
2
|
+
import { CACHE_STALENESS_SOCKET } from '../shared/CACHE_STALENESS_SOCKET.ts'
|
|
3
|
+
import { cache } from '../shared/cache.ts'
|
|
4
|
+
import { matcherFromEnvelope } from '../shared/matcherFromEnvelope.ts'
|
|
5
|
+
import { SocketDisconnectedError } from '../shared/SocketDisconnectedError.ts'
|
|
6
|
+
import type { CacheStalenessFrame } from '../shared/types/CacheStalenessFrame.ts'
|
|
7
|
+
import type { Socket } from '../shared/types/Socket.ts'
|
|
8
|
+
import { socketProxy } from './socketProxy.ts'
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
The client half of the cross-client staleness broadcast (ADR-0041): every tab
|
|
12
|
+
subscribes to the reserved __abide/cache socket at boot and applies each frame to
|
|
13
|
+
its own cache. A frame rebuilds the entry predicate via matcherFromEnvelope and
|
|
14
|
+
drives the SAME store loop a local invalidate()/refresh() does
|
|
15
|
+
(cache.invalidateMatching / cache.refreshMatching), so wire-driven and local applies
|
|
16
|
+
can't diverge.
|
|
17
|
+
|
|
18
|
+
LIVE-ONLY, never replay: bare iteration (replay 0) on both boot and reconnect — a
|
|
19
|
+
client applies only frames published while it is connected. The reserved topic keeps
|
|
20
|
+
no tail, so there is nothing to catch up on; a client offline when a frame was
|
|
21
|
+
published misses it and falls back to SWR staleness. Applies are idempotent, so
|
|
22
|
+
correctness has no dependency on frame ordering.
|
|
23
|
+
|
|
24
|
+
No-op on the server (no window): the broadcast is the server's job, not something it
|
|
25
|
+
consumes. Returns a disposer that stops the loop and closes the subscription.
|
|
26
|
+
*/
|
|
27
|
+
export function subscribeCacheStaleness(injectedSocket?: Socket<CacheStalenessFrame>): () => void {
|
|
28
|
+
if (typeof window === 'undefined') {
|
|
29
|
+
return () => undefined
|
|
30
|
+
}
|
|
31
|
+
/* Default to the browser channel proxy; tests inject a socket over their own channel. */
|
|
32
|
+
const socket = injectedSocket ?? socketProxy<CacheStalenessFrame>(CACHE_STALENESS_SOCKET)
|
|
33
|
+
const controller = new AbortController()
|
|
34
|
+
/* `let`: a reconnect swaps in a fresh live iterator after a transport loss. */
|
|
35
|
+
let iterator = socket[Symbol.asyncIterator]()
|
|
36
|
+
;(async () => {
|
|
37
|
+
while (!controller.signal.aborted) {
|
|
38
|
+
let next: IteratorResult<CacheStalenessFrame>
|
|
39
|
+
try {
|
|
40
|
+
next = await iterator.next()
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (controller.signal.aborted) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
/* Transport loss: re-open a fresh LIVE subscription (no replay) and keep going. */
|
|
46
|
+
if (error instanceof SocketDisconnectedError) {
|
|
47
|
+
iterator = socket[Symbol.asyncIterator]()
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
abideLog.error(error)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (controller.signal.aborted || next.done === true) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
/* One malformed frame must not tear down the live pipe: a throw from decode/apply
|
|
57
|
+
is logged and swallowed so every subsequent frame still delivers. Frames come
|
|
58
|
+
only from the trusted server (clientPublish is false), so this is defense in
|
|
59
|
+
depth, not an expected path. */
|
|
60
|
+
try {
|
|
61
|
+
applyFrame(next.value)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
abideLog.error(error)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})()
|
|
67
|
+
return () => {
|
|
68
|
+
controller.abort()
|
|
69
|
+
iterator.return?.(undefined)?.catch(() => undefined)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Applies one decoded frame to this tab's cache via the shared apply-by-matcher seam. */
|
|
74
|
+
function applyFrame(frame: CacheStalenessFrame): void {
|
|
75
|
+
const matches = matcherFromEnvelope(frame)
|
|
76
|
+
/* The tripwire label + the rpc-error-registry prefix, mirroring what a local selector
|
|
77
|
+
derives: a tag frame has no key prefix to clear. */
|
|
78
|
+
const label = frame.mode === 'tags' ? `tags: ${frame.tags.join(', ')}` : frame.match
|
|
79
|
+
const prefix = frame.mode === 'tags' ? undefined : frame.match
|
|
80
|
+
if (frame.op === 'invalidate') {
|
|
81
|
+
cache.invalidateMatching(matches, label, prefix)
|
|
82
|
+
} else {
|
|
83
|
+
cache.refreshMatching(matches, label, prefix)
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/serverEntry.ts
CHANGED
|
@@ -26,11 +26,14 @@ import { shell } from './_virtual/shell.ts'
|
|
|
26
26
|
import { sockets } from './_virtual/sockets.ts'
|
|
27
27
|
import { exitWithParent } from './lib/bundle/exitWithParent.ts'
|
|
28
28
|
import { loadEnvFromBinaryDir } from './lib/cli/loadEnvFromBinaryDir.ts'
|
|
29
|
+
import { broadcastCacheStaleness } from './lib/server/runtime/cacheStalenessBroadcaster.ts'
|
|
29
30
|
import { createServer } from './lib/server/runtime/createServer.ts'
|
|
30
31
|
import { requestContext } from './lib/server/runtime/requestContext.ts'
|
|
31
32
|
import { resolvePageSnapshot } from './lib/server/runtime/resolvePageSnapshot.ts'
|
|
33
|
+
import { cacheStalenessSlot } from './lib/shared/cacheStalenessSlot.ts'
|
|
32
34
|
import { cacheStoreSlot } from './lib/shared/cacheStoreSlot.ts'
|
|
33
35
|
import { createCacheStore } from './lib/shared/createCacheStore.ts'
|
|
36
|
+
import { docSnapshotsSlot } from './lib/shared/docSnapshotsSlot.ts'
|
|
34
37
|
import { loadEnvFromDataDir } from './lib/shared/loadEnvFromDataDir.ts'
|
|
35
38
|
import { pageSlot } from './lib/shared/pageSlot.ts'
|
|
36
39
|
import { pendingAsyncCellsSlot } from './lib/shared/pendingAsyncCellsSlot.ts'
|
|
@@ -79,6 +82,13 @@ sharedCacheStoreSlot.resolver = () => sharedCacheStore
|
|
|
79
82
|
|
|
80
83
|
cacheStoreSlot.resolver = () => requestContext.getStore()?.cache ?? sharedCacheStore
|
|
81
84
|
|
|
85
|
+
/*
|
|
86
|
+
Server-side invalidate()/refresh() broadcast to every connected client instead of
|
|
87
|
+
mutating a throwaway request store (ADR-0041). The broadcaster is imported ONLY here
|
|
88
|
+
so its server socket code never enters the client reachability graph.
|
|
89
|
+
*/
|
|
90
|
+
cacheStalenessSlot.resolver = () => broadcastCacheStaleness
|
|
91
|
+
|
|
82
92
|
/* One process-wide fallback list for reads with no request in flight (boot/cron/socket) —
|
|
83
93
|
real requests each carry their own `pendingAsyncCells`, so the SSR barrier drains a
|
|
84
94
|
per-request list and concurrent renders never cross-contaminate. */
|
|
@@ -89,6 +99,8 @@ const sharedResolvedCells = { entries: [] }
|
|
|
89
99
|
resolvedCellsSlot.resolver = () => requestContext.getStore()?.resolvedCells ?? sharedResolvedCells
|
|
90
100
|
const sharedStreamedCells = { entries: [] }
|
|
91
101
|
streamedCellsSlot.resolver = () => requestContext.getStore()?.streamedCells ?? sharedStreamedCells
|
|
102
|
+
const sharedDocSnapshots = { entries: [] }
|
|
103
|
+
docSnapshotsSlot.resolver = () => requestContext.getStore()?.docSnapshots ?? sharedDocSnapshots
|
|
92
104
|
|
|
93
105
|
pageSlot.resolver = resolvePageSnapshot
|
|
94
106
|
|