@abide/abide 0.33.0 → 0.34.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 +39 -34
- package/CHANGELOG.md +30 -0
- package/README.md +21 -21
- package/bin/abide.ts +6 -2
- package/package.json +5 -5
- package/src/abideResolverPlugin.ts +1 -1
- package/src/build.ts +1 -1
- package/src/compile.ts +1 -1
- package/src/devEntry.ts +12 -1
- package/src/lib/bundle/BundleMenu.ts +1 -1
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/bundle/BundleWindow.ts +1 -1
- package/src/lib/bundle/bundled.ts +1 -1
- package/src/lib/bundle/disconnected.abide +105 -75
- package/src/lib/bundle/exitWithParent.ts +7 -7
- package/src/lib/bundle/onMenu.ts +1 -1
- package/src/lib/mcp/createMcpServer.ts +1 -1
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/DELETE.ts +1 -1
- package/src/lib/server/GET.ts +1 -1
- package/src/lib/server/HEAD.ts +1 -1
- package/src/lib/server/PATCH.ts +1 -1
- package/src/lib/server/POST.ts +1 -1
- package/src/lib/server/PUT.ts +1 -1
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/appDataDir.ts +1 -1
- package/src/lib/server/cookies.ts +1 -1
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +1 -1
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/prompts/definePrompt.ts +1 -1
- package/src/lib/server/prompts/renderPromptTemplate.ts +1 -1
- package/src/lib/server/reachable.ts +1 -1
- package/src/lib/server/redirect.ts +1 -1
- package/src/lib/server/request.ts +1 -1
- package/src/lib/server/rpc/defineVerb.ts +1 -1
- package/src/lib/server/runtime/buildCacheSnapshot.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +2 -2
- package/src/lib/server/runtime/types/InspectorContext.ts +2 -2
- package/src/lib/server/server.ts +1 -1
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/defineSocket.ts +1 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/cache.ts +18 -18
- package/src/lib/shared/createSubscriber.ts +1 -1
- package/src/lib/shared/health.ts +1 -1
- package/src/lib/shared/html.ts +1 -1
- package/src/lib/shared/isSubscribable.ts +1 -1
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/page.ts +1 -1
- package/src/lib/shared/pending.ts +2 -2
- package/src/lib/shared/probeRegistries.ts +2 -2
- package/src/lib/shared/refreshing.ts +2 -2
- package/src/lib/shared/selectorMatcher.ts +8 -8
- package/src/lib/shared/selectorPrefix.ts +1 -1
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/toTagSet.ts +4 -0
- package/src/lib/shared/trace.ts +1 -1
- package/src/lib/shared/types/CacheEntry.ts +3 -3
- package/src/lib/shared/types/CacheOptions.ts +3 -3
- package/src/lib/shared/types/CacheSelector.ts +2 -2
- package/src/lib/shared/url.ts +1 -1
- package/src/lib/shared/withJsonSchema.ts +1 -1
- package/src/lib/test/assertAgentFrameConformance.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +1 -1
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -2
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +3 -4
- package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
- package/src/lib/ui/compile/analyzeComponent.ts +2 -1
- package/src/lib/ui/compile/compileComponent.ts +3 -3
- package/src/lib/ui/compile/compileModule.ts +30 -2
- package/src/lib/ui/compile/compileSSR.ts +5 -5
- package/src/lib/ui/compile/compileShadow.ts +54 -36
- package/src/lib/ui/compile/desugarSignals.ts +147 -18
- package/src/lib/ui/compile/generateBuild.ts +15 -28
- package/src/lib/ui/compile/generateSSR.ts +20 -13
- package/src/lib/ui/compile/isControlFlow.ts +18 -0
- package/src/lib/ui/compile/isTextLeaf.ts +15 -0
- package/src/lib/ui/compile/lowerContext.ts +45 -5
- package/src/lib/ui/compile/prepareNestedScript.ts +12 -8
- package/src/lib/ui/compile/renameSignalRefs.ts +21 -4
- package/src/lib/ui/compile/skeletonable.ts +1 -14
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +1 -0
- package/src/lib/ui/computed.ts +32 -0
- package/src/lib/ui/createScope.ts +91 -0
- package/src/lib/ui/dom/anchorCursor.ts +1 -1
- package/src/lib/ui/dom/appendSnippet.ts +1 -1
- package/src/lib/ui/dom/appendStatic.ts +1 -1
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/appendTextAt.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/attach.ts +7 -2
- package/src/lib/ui/dom/attr.ts +1 -1
- package/src/lib/ui/dom/awaitBlock.ts +1 -1
- package/src/lib/ui/dom/cloneStatic.ts +1 -1
- package/src/lib/ui/dom/each.ts +4 -3
- package/src/lib/ui/dom/eachAsync.ts +1 -1
- package/src/lib/ui/dom/hydrate.ts +20 -6
- package/src/lib/ui/dom/mount.ts +12 -1
- package/src/lib/ui/dom/mountChild.ts +6 -2
- package/src/lib/ui/dom/mountSlot.ts +1 -1
- package/src/lib/ui/dom/on.ts +10 -4
- package/src/lib/ui/dom/openMarker.ts +3 -3
- package/src/lib/ui/dom/skeleton.ts +13 -5
- package/src/lib/ui/dom/switchBlock.ts +1 -1
- package/src/lib/ui/dom/text.ts +1 -1
- package/src/lib/ui/dom/tryBlock.ts +1 -1
- package/src/lib/ui/dom/when.ts +1 -1
- package/src/lib/ui/effect.ts +7 -2
- package/src/lib/ui/enterScope.ts +18 -0
- package/src/lib/ui/exitScope.ts +8 -0
- package/src/lib/ui/history.ts +102 -0
- package/src/lib/ui/installHotBridge.ts +6 -8
- package/src/lib/ui/linked.ts +0 -1
- package/src/lib/ui/navigate.ts +1 -1
- package/src/lib/ui/outbox.ts +115 -0
- package/src/lib/ui/persist.ts +112 -0
- package/src/lib/ui/probeNavigation.ts +1 -1
- package/src/lib/ui/remoteProxy.ts +1 -1
- package/src/lib/ui/renderToStream.ts +1 -1
- package/src/lib/ui/router.ts +1 -1
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +10 -0
- package/src/lib/ui/runtime/OUTLET_TAG.ts +1 -1
- package/src/lib/ui/runtime/PATCH_BUS.ts +28 -0
- package/src/lib/ui/runtime/RESUME.ts +1 -1
- package/src/lib/ui/runtime/captureModelDoc.ts +29 -0
- package/src/lib/ui/runtime/claimExpected.ts +26 -0
- package/src/lib/ui/runtime/createComputedNode.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +73 -3
- package/src/lib/ui/runtime/enterRenderPass.ts +1 -1
- package/src/lib/ui/runtime/escapeKey.ts +13 -0
- package/src/lib/ui/runtime/exitRenderPass.ts +1 -1
- package/src/lib/ui/runtime/hotReplace.ts +17 -8
- package/src/lib/ui/runtime/inScope.ts +19 -0
- package/src/lib/ui/runtime/localStoragePersistence.ts +34 -0
- package/src/lib/ui/runtime/nextBlockId.ts +1 -1
- package/src/lib/ui/runtime/seedModelDoc.ts +26 -0
- package/src/lib/ui/runtime/types/{Derived.ts → Computed.ts} +1 -1
- package/src/lib/ui/runtime/types/Doc.ts +3 -0
- package/src/lib/ui/runtime/types/HotInstance.ts +6 -2
- package/src/lib/ui/runtime/types/PatchEvent.ts +16 -0
- package/src/lib/ui/runtime/unescapeKey.ts +12 -0
- package/src/lib/ui/runtime/walkPath.ts +3 -1
- package/src/lib/ui/scope.ts +19 -0
- package/src/lib/ui/socketProxy.ts +1 -1
- package/src/lib/ui/startClient.ts +1 -1
- package/src/lib/ui/state.ts +1 -2
- package/src/lib/ui/sync.ts +42 -0
- package/src/lib/ui/tail.ts +1 -1
- package/src/lib/ui/types/History.ts +14 -0
- package/src/lib/ui/types/Outbox.ts +14 -0
- package/src/lib/ui/types/PersistHandle.ts +11 -0
- package/src/lib/ui/types/PersistenceStore.ts +12 -0
- package/src/lib/ui/types/Scope.ts +57 -0
- package/src/lib/ui/types/SyncTransport.ts +13 -0
- package/src/preload.ts +1 -1
- package/src/lib/shared/toScopeSet.ts +0 -4
- package/src/lib/ui/derived.ts +0 -49
- package/src/lib/ui/doc.ts +0 -15
package/src/lib/shared/cache.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { REPLAYABLE_METHODS } from './REPLAYABLE_METHODS.ts'
|
|
|
15
15
|
import { SocketDisconnectedError } from './SocketDisconnectedError.ts'
|
|
16
16
|
import { selectorMatcher } from './selectorMatcher.ts'
|
|
17
17
|
import { selectorPrefix } from './selectorPrefix.ts'
|
|
18
|
-
import {
|
|
18
|
+
import { toTagSet } from './toTagSet.ts'
|
|
19
19
|
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
20
20
|
import type { CacheOnContext } from './types/CacheOnContext.ts'
|
|
21
21
|
import type { CacheOptions } from './types/CacheOptions.ts'
|
|
@@ -116,7 +116,7 @@ keep streaming reads in a parent that never top-level awaits — the
|
|
|
116
116
|
await-everything mode is per component instance, so a child's await blocks only
|
|
117
117
|
the child.
|
|
118
118
|
*/
|
|
119
|
-
// @
|
|
119
|
+
// @documentation cache
|
|
120
120
|
export function cache<Args, Return>(
|
|
121
121
|
fn: RemoteFunction<Args, Return>,
|
|
122
122
|
options?: CacheOptions,
|
|
@@ -173,7 +173,7 @@ export function cache<Args, Return>(
|
|
|
173
173
|
const existing = store.entries.get(key)
|
|
174
174
|
recordRead(options?.global ? activeCacheStore() : store, key, existing)
|
|
175
175
|
if (existing) {
|
|
176
|
-
|
|
176
|
+
tagEntry(existing, options?.tags)
|
|
177
177
|
attachPolicy(existing, options, () => remote(args as Args))
|
|
178
178
|
adoptTtl(store, existing, options)
|
|
179
179
|
}
|
|
@@ -278,7 +278,7 @@ function warnAnonymousProducer(producer: (args?: never) => unknown): void {
|
|
|
278
278
|
}
|
|
279
279
|
warnedAnonymousProducers.add(source)
|
|
280
280
|
abideLog.warn(
|
|
281
|
-
'cache() received an anonymous function — each call mints a fresh identity, so it never coalesces and pending()/refreshing() never match it. Hoist it to a named binding, or add a
|
|
281
|
+
'cache() received an anonymous function — each call mints a fresh identity, so it never coalesces and pending()/refreshing() never match it. Hoist it to a named binding, or add a tag to probe it from elsewhere.',
|
|
282
282
|
)
|
|
283
283
|
}
|
|
284
284
|
|
|
@@ -298,7 +298,7 @@ function invokeProducer<Args, Return>(
|
|
|
298
298
|
const existing = store.entries.get(key)
|
|
299
299
|
recordRead(options?.global ? activeCacheStore() : store, key, existing)
|
|
300
300
|
if (existing) {
|
|
301
|
-
|
|
301
|
+
tagEntry(existing, options?.tags)
|
|
302
302
|
attachPolicy(existing, options, () => producer(args))
|
|
303
303
|
const shared = existing.promise as Promise<Return>
|
|
304
304
|
/* A coalesced join waits on the in-flight producer — time the block so the
|
|
@@ -368,7 +368,7 @@ function registerEntry(
|
|
|
368
368
|
request,
|
|
369
369
|
ttl,
|
|
370
370
|
expiresAt: undefined,
|
|
371
|
-
|
|
371
|
+
tags: options?.tags === undefined ? undefined : toTagSet(options.tags),
|
|
372
372
|
refreshing,
|
|
373
373
|
invalidation,
|
|
374
374
|
}
|
|
@@ -451,7 +451,7 @@ function armTtlExpiry(store: CacheStore, entry: CacheEntry, ttl: number): void {
|
|
|
451
451
|
}
|
|
452
452
|
|
|
453
453
|
/*
|
|
454
|
-
Mirrors
|
|
454
|
+
Mirrors tagEntry/attachPolicy for retention: a hydrated snapshot entry ships
|
|
455
455
|
without its wrap options (they live at call sites, not on the wire), so the
|
|
456
456
|
first read adopts its call site's ttl declaration. Omitted = forever, exactly
|
|
457
457
|
as shipped; ttl > 0 = the expiry clock starts at this read; ttl = 0 = the warm
|
|
@@ -530,7 +530,7 @@ function invalidate<Args, Return>(arg?: CacheSelector<Args, Return>, args?: Args
|
|
|
530
530
|
Human-readable selector identity for the tripwire and cache.on coverage: the
|
|
531
531
|
key prefix for fn selectors — the exact key when args narrow it — falling
|
|
532
532
|
back to the function's name for a producer never cached, the tag list for
|
|
533
|
-
|
|
533
|
+
a `{ tags }` selector, `*` for the bare form.
|
|
534
534
|
*/
|
|
535
535
|
function selectorLabel<Args, Return>(
|
|
536
536
|
arg?: CacheSelector<Args, Return>,
|
|
@@ -543,7 +543,7 @@ function selectorLabel<Args, Return>(
|
|
|
543
543
|
if (typeof arg === 'function') {
|
|
544
544
|
return prefix ?? selectorPrefix(arg, args) ?? (arg.name || 'anonymous producer')
|
|
545
545
|
}
|
|
546
|
-
return `
|
|
546
|
+
return `tags: ${[...toTagSet(arg.tags ?? [])].join(', ')}`
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
cache.invalidate = invalidate
|
|
@@ -920,24 +920,24 @@ function settleRefetchFailure(store: CacheStore, entry: CacheEntry, status?: num
|
|
|
920
920
|
}
|
|
921
921
|
|
|
922
922
|
/* Folds new tags into an entry's existing set without duplicating them. */
|
|
923
|
-
function
|
|
924
|
-
return new Set([...(existing ?? []), ...
|
|
923
|
+
function mergeTags(existing: Set<string> | undefined, incoming: string | string[]): Set<string> {
|
|
924
|
+
return new Set([...(existing ?? []), ...toTagSet(incoming)])
|
|
925
925
|
}
|
|
926
926
|
|
|
927
927
|
/*
|
|
928
|
-
Tags an existing entry with a read's
|
|
929
|
-
reaches entries hydrated from the SSR snapshot (which carry a value but no
|
|
928
|
+
Tags an existing entry with a read's tags so a later cache.invalidate({ tags })
|
|
929
|
+
reaches entries hydrated from the SSR snapshot (which carry a value but no tags)
|
|
930
930
|
without a refetch. Merges rather than replaces so a read tagging one group can't
|
|
931
|
-
drop tags another read site already added; a no-op when the read passes no
|
|
931
|
+
drop tags another read site already added; a no-op when the read passes no tags.
|
|
932
932
|
*/
|
|
933
|
-
function
|
|
934
|
-
if (
|
|
935
|
-
entry.
|
|
933
|
+
function tagEntry(entry: CacheEntry, tags: CacheOptions['tags']): void {
|
|
934
|
+
if (tags !== undefined) {
|
|
935
|
+
entry.tags = mergeTags(entry.tags, tags)
|
|
936
936
|
}
|
|
937
937
|
}
|
|
938
938
|
|
|
939
939
|
/*
|
|
940
|
-
Mirrors
|
|
940
|
+
Mirrors tagEntry for invalidate policies: a read declaring a policy arms an
|
|
941
941
|
existing entry that lacks one. Hydrated snapshot entries carry a value but no
|
|
942
942
|
refetch thunk — without this, the first invalidate after hydration would hard-
|
|
943
943
|
drop the entry (a pending flash) instead of revalidating stale-in-place, and a
|
|
@@ -24,7 +24,7 @@ resource down and reopening it across a re-run. Called outside any tracking scop
|
|
|
24
24
|
`track` is a no-op, observers stay empty, and the resource never opens — matching
|
|
25
25
|
the open-on-first-read contract.
|
|
26
26
|
*/
|
|
27
|
-
// @
|
|
27
|
+
// @documentation plumbing
|
|
28
28
|
export function createSubscriber(start: (update: () => void) => () => void): () => void {
|
|
29
29
|
const node = createSignalNode(undefined)
|
|
30
30
|
let cleanup: (() => void) | undefined
|
package/src/lib/shared/health.ts
CHANGED
|
@@ -13,7 +13,7 @@ Augmented by the generated `src/.abide/health.d.ts` with
|
|
|
13
13
|
`health()` reads type against its own hook. Unaugmented (no app.ts, or no
|
|
14
14
|
hook), AppHealth resolves to no fields.
|
|
15
15
|
*/
|
|
16
|
-
// @
|
|
16
|
+
// @documentation observability
|
|
17
17
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated health.d.ts
|
|
18
18
|
export interface AppHealthMap {}
|
|
19
19
|
export type AppHealth = AppHealthMap extends { fields: infer Fields }
|
package/src/lib/shared/html.ts
CHANGED
|
@@ -17,7 +17,7 @@ Calling `html` is the explicit opt-in to raw insertion; plain `{value}` always
|
|
|
17
17
|
escapes. The tag does NOT auto-escape interpolations (it's raw by intent), so only
|
|
18
18
|
build markup from values you trust, or escape them yourself.
|
|
19
19
|
*/
|
|
20
|
-
// @
|
|
20
|
+
// @documentation templating
|
|
21
21
|
export function html(strings: TemplateStringsArray | string, ...values: unknown[]): RawHtml {
|
|
22
22
|
if (typeof strings === 'string') {
|
|
23
23
|
return { [RAW_HTML]: strings }
|
|
@@ -2,7 +2,7 @@ import type { Subscribable } from './types/Subscribable.ts'
|
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
A Subscribable is a named AsyncIterable — distinguishes a stream argument from
|
|
5
|
-
the other probe selector shapes (callables and `{
|
|
5
|
+
the other probe selector shapes (callables and `{ tags }` objects, neither of
|
|
6
6
|
which carries Symbol.asyncIterator).
|
|
7
7
|
*/
|
|
8
8
|
export function isSubscribable(value: unknown): value is Subscribable<unknown> {
|
package/src/lib/shared/log.ts
CHANGED
|
@@ -13,7 +13,7 @@ diagnostic channel (browser: the `abide-debug` localStorage key). Renders as
|
|
|
13
13
|
the tab-separated tsv format (default) or one JSON object per line under
|
|
14
14
|
ABIDE_LOG_FORMAT=json.
|
|
15
15
|
*/
|
|
16
|
-
// @
|
|
16
|
+
// @documentation observability
|
|
17
17
|
export const log: Log = Object.assign(
|
|
18
18
|
createChannelLog(() => appNameSlot.name ?? 'app', true),
|
|
19
19
|
{
|
package/src/lib/shared/online.ts
CHANGED
package/src/lib/shared/page.ts
CHANGED
|
@@ -13,7 +13,7 @@ navigation updates the snapshot (the client resolver reads a abide-ui signal).
|
|
|
13
13
|
prefix (the server re-applies it to the proxy-stripped request URL), so compare
|
|
14
14
|
active state against url() output, e.g. `page.url.pathname.startsWith(url('/x'))`.
|
|
15
15
|
*/
|
|
16
|
-
// @
|
|
16
|
+
// @documentation page
|
|
17
17
|
export const page: PageSnapshot = {
|
|
18
18
|
get route(): string {
|
|
19
19
|
return activePage().route
|
|
@@ -11,7 +11,7 @@ tail registry (streams). Pending means "no value yet":
|
|
|
11
11
|
pending(fn) → that function's calls (per-route spinner; remote or
|
|
12
12
|
producer, same selector grammar as cache.invalidate)
|
|
13
13
|
pending(fn, args) → exactly that call (per-row spinner)
|
|
14
|
-
pending({
|
|
14
|
+
pending({ tags }) → a tagged group
|
|
15
15
|
pending(subscribable) → that stream awaiting its first frame
|
|
16
16
|
(tail.status === 'pending'; true when nothing
|
|
17
17
|
is reading yet — there is no value either way)
|
|
@@ -19,7 +19,7 @@ Probes report, never act: reading one opens no fetch and no stream. SSR
|
|
|
19
19
|
loading state is driven by {#await}, not this. Scan semantics (tap order,
|
|
20
20
|
selector grammar, registry spans) live in probeRegistries.
|
|
21
21
|
*/
|
|
22
|
-
// @
|
|
22
|
+
// @documentation probes
|
|
23
23
|
export function pending<Args, Return>(
|
|
24
24
|
arg?: CacheSelector<Args, Return> | Subscribable<unknown>,
|
|
25
25
|
args?: Args,
|
|
@@ -16,14 +16,14 @@ re-runs whenever a matching call or stream changes state. The tap is scoped
|
|
|
16
16
|
to the selector: a fn selector taps only its own prefix channel, so reading
|
|
17
17
|
`pending(fn)` in an effect can't be re-woken by unrelated cache events (an
|
|
18
18
|
effect that probes one fn and invalidates another no longer feeds itself);
|
|
19
|
-
bare and
|
|
19
|
+
bare and tag selectors scan many entries and tap the store-wide channel. A
|
|
20
20
|
producer never cached resolves no prefix yet and taps store-wide — its first
|
|
21
21
|
cache() marks that channel, the re-derive resolves the prefix, and the scope
|
|
22
22
|
re-tracks narrowed. `matchesEntry` is the probe's cache-side question;
|
|
23
23
|
`field` selects the stream-side answer; `unprobed` is the answer for a
|
|
24
24
|
stream no prober can see (server, or tail never imported) — matching what
|
|
25
25
|
tail() itself reports there. The bare form (no selector) spans both
|
|
26
|
-
registries; fn/
|
|
26
|
+
registries; fn/tag selectors are cache identities only. Outside a tracking
|
|
27
27
|
scope the taps are no-ops and the current value returns.
|
|
28
28
|
*/
|
|
29
29
|
export function probeRegistries<Args, Return>(
|
|
@@ -9,7 +9,7 @@ value it already had while a fresher source is in flight":
|
|
|
9
9
|
refreshing() → anything reloading data it already had
|
|
10
10
|
refreshing(fn) → that function's calls (per-route "updating…" badge)
|
|
11
11
|
refreshing(fn, args) → exactly that call (per-row badge)
|
|
12
|
-
refreshing({
|
|
12
|
+
refreshing({ tags }) → a tagged group
|
|
13
13
|
refreshing(subscribable) → that stream reconnecting with its last value
|
|
14
14
|
retained — never merely `open`; a live stream's
|
|
15
15
|
arriving frames are its normal mode, not a reload
|
|
@@ -20,7 +20,7 @@ no value yet?", refreshing answers "is a held value being superseded?".
|
|
|
20
20
|
Probes report, never act. Scan semantics (tap order, selector grammar,
|
|
21
21
|
registry spans) live in probeRegistries.
|
|
22
22
|
*/
|
|
23
|
-
// @
|
|
23
|
+
// @documentation probes
|
|
24
24
|
export function refreshing<Args, Return>(
|
|
25
25
|
arg?: CacheSelector<Args, Return> | Subscribable<unknown>,
|
|
26
26
|
args?: Args,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { keyMatchesPrefix } from './keyMatchesPrefix.ts'
|
|
2
2
|
import { selectorPrefix } from './selectorPrefix.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { toTagSet } from './toTagSet.ts'
|
|
4
4
|
import type { CacheEntry } from './types/CacheEntry.ts'
|
|
5
5
|
import type { CacheSelector } from './types/CacheSelector.ts'
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ pending(), and refreshing() so all three interpret the call shapes identically:
|
|
|
16
16
|
producer fn → that producer's calls (reference id prefix). Matches
|
|
17
17
|
only if the producer was cached at least once (else it
|
|
18
18
|
has no id and nothing matches).
|
|
19
|
-
{
|
|
19
|
+
{ tags } → any entry sharing one of the requested tags. An
|
|
20
20
|
empty selector matches nothing.
|
|
21
21
|
fn + args → exactly that call's entry (the key derived from the
|
|
22
22
|
same encoders the read path uses); other args
|
|
@@ -46,11 +46,11 @@ export function selectorMatcher<Args, Return>(
|
|
|
46
46
|
}
|
|
47
47
|
return (entry) => keyMatchesPrefix(entry.key, prefix)
|
|
48
48
|
}
|
|
49
|
-
if (arg.
|
|
49
|
+
if (arg.tags === undefined) {
|
|
50
50
|
return () => false
|
|
51
51
|
}
|
|
52
|
-
const
|
|
53
|
-
return (entry) => entry.
|
|
52
|
+
const requestedTags = toTagSet(arg.tags)
|
|
53
|
+
return (entry) => entry.tags !== undefined && intersects(entry.tags, requestedTags)
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/*
|
|
@@ -58,9 +58,9 @@ True when an entry's tags and the requested tags overlap on any tag. A plain
|
|
|
58
58
|
for-of over the Set rather than .values().some() — Iterator Helpers are too new
|
|
59
59
|
for this module's browser baseline (see producerKey).
|
|
60
60
|
*/
|
|
61
|
-
function intersects(
|
|
62
|
-
for (const
|
|
63
|
-
if (
|
|
61
|
+
function intersects(entryTags: Set<string>, requestedTags: Set<string>): boolean {
|
|
62
|
+
for (const tag of requestedTags) {
|
|
63
|
+
if (entryTags.has(tag)) {
|
|
64
64
|
return true
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -11,7 +11,7 @@ method+url (fn and fn.raw agree — same wire identity), a producer on the
|
|
|
11
11
|
reference id minted at first cache. With `args`, the exact entry key for that
|
|
12
12
|
one call instead — derived through the same encoders the read path uses
|
|
13
13
|
(keyForRemoteCall / producerKey format), so selector and entry can't
|
|
14
|
-
disagree. Undefined when no prefix exists — bare and
|
|
14
|
+
disagree. Undefined when no prefix exists — bare and tag selectors (they
|
|
15
15
|
scan, not prefix-match) and producers never cached (no id was minted;
|
|
16
16
|
minting one here would leak identities for probe-only reads). The
|
|
17
17
|
cache()-wrapper throw lives here so both consumers — the matcher and the
|
|
@@ -10,7 +10,7 @@ export type Snippet<Payload> = { readonly [SNIPPET]: Payload }
|
|
|
10
10
|
inserting escaped text. The compiler wraps a snippet's body in this — the client
|
|
11
11
|
builder closes over the defining component's scope, the server string is its SSR
|
|
12
12
|
render — so a snippet value passes through props like any other value. */
|
|
13
|
-
// @
|
|
13
|
+
// @documentation templating
|
|
14
14
|
export function snippet<Payload>(payload: Payload): Snippet<Payload> {
|
|
15
15
|
return { [SNIPPET]: payload }
|
|
16
16
|
}
|
package/src/lib/shared/trace.ts
CHANGED
|
@@ -9,7 +9,7 @@ __SSR__ — so a client-side read returns the trace of the request that
|
|
|
9
9
|
rendered the page. The string is propagation-ready: hand it to your own logs,
|
|
10
10
|
error reports, or `propagation.extract` when attaching OpenTelemetry spans.
|
|
11
11
|
*/
|
|
12
|
-
// @
|
|
12
|
+
// @documentation observability
|
|
13
13
|
export function trace(): string | undefined {
|
|
14
14
|
const scope = requestScopeSlot.resolver?.()
|
|
15
15
|
return scope ? formatTraceparent(scope.trace) : undefined
|
|
@@ -28,8 +28,8 @@ ttl = 0 = the warm value exists only to complete the hydration render and is
|
|
|
28
28
|
evicted a macrotask later). Live entries never carry it; their ttl was fixed
|
|
29
29
|
at registration.
|
|
30
30
|
|
|
31
|
-
`
|
|
32
|
-
`cache.invalidate({
|
|
31
|
+
`tags` holds the cache() call's tags as a Set so
|
|
32
|
+
`cache.invalidate({ tags })` can drop every entry sharing any tag with O(1)
|
|
33
33
|
membership; a re-read merges new tags in rather than replacing them.
|
|
34
34
|
|
|
35
35
|
`settled` flips true once the stored promise resolves. SSR snapshot
|
|
@@ -61,7 +61,7 @@ export type CacheEntry = {
|
|
|
61
61
|
ttl: number | undefined
|
|
62
62
|
expiresAt: number | undefined
|
|
63
63
|
value?: unknown
|
|
64
|
-
|
|
64
|
+
tags?: Set<string>
|
|
65
65
|
settled?: boolean
|
|
66
66
|
hydrated?: boolean
|
|
67
67
|
refreshing?: boolean
|
|
@@ -6,8 +6,8 @@ milliseconds-past-resolve that the entry stays live: omitted = forever, 0 =
|
|
|
6
6
|
dedupe only (entry dropped once the promise settles — the mutation idiom:
|
|
7
7
|
in-flight coalescing and pending() visibility, nothing retained), any other
|
|
8
8
|
number = TTL.
|
|
9
|
-
`
|
|
10
|
-
`cache.invalidate({
|
|
9
|
+
`tags` is one or more free-form labels grouping unrelated calls so one
|
|
10
|
+
`cache.invalidate({ tags })` drops every entry sharing any of them — pass an
|
|
11
11
|
array when a call belongs to multiple invalidation groups. A unique tag (e.g. a
|
|
12
12
|
uuid) shared by a set of calls gives them their own private invalidation group.
|
|
13
13
|
|
|
@@ -37,7 +37,7 @@ uncheckable — set `swr` only on a producer that is a pure read.
|
|
|
37
37
|
*/
|
|
38
38
|
export type CacheOptions = {
|
|
39
39
|
ttl?: number
|
|
40
|
-
|
|
40
|
+
tags?: string | string[]
|
|
41
41
|
global?: boolean
|
|
42
42
|
swr?: boolean | { throttle?: number; debounce?: number }
|
|
43
43
|
}
|
|
@@ -8,10 +8,10 @@ Selector grammar shared by cache.invalidate(), pending(), and refreshing():
|
|
|
8
8
|
remote fn → that function's calls (method+url prefix); fn and fn.raw match
|
|
9
9
|
the same set since they share wire identity
|
|
10
10
|
producer fn → that producer's calls (reference-id prefix)
|
|
11
|
-
{
|
|
11
|
+
{ tags } → any entry sharing one of the requested tags
|
|
12
12
|
*/
|
|
13
13
|
export type CacheSelector<Args, Return> =
|
|
14
14
|
| RemoteFunction<Args, Return>
|
|
15
15
|
| RawRemoteFunction<Args>
|
|
16
16
|
| ((args?: Args) => Promise<Return>)
|
|
17
|
-
| Pick<CacheOptions, '
|
|
17
|
+
| Pick<CacheOptions, 'tags'>
|
package/src/lib/shared/url.ts
CHANGED
|
@@ -15,7 +15,7 @@ verb, so url('/rpc/search', { q }) types its args against the verb's own
|
|
|
15
15
|
signature. Empty by default — an absent entry falls through to the page/asset
|
|
16
16
|
branch, so the helper works before the generated d.ts lands.
|
|
17
17
|
*/
|
|
18
|
-
// @
|
|
18
|
+
// @documentation url
|
|
19
19
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated rpc.d.ts
|
|
20
20
|
export interface RpcRoutes {}
|
|
21
21
|
|
|
@@ -12,7 +12,7 @@ the schema is declared:
|
|
|
12
12
|
Mutates and returns the same schema with the method attached, so the wrapped
|
|
13
13
|
value stays usable everywhere the bare schema was.
|
|
14
14
|
*/
|
|
15
|
-
// @
|
|
15
|
+
// @documentation rpc
|
|
16
16
|
export function withJsonSchema<Schema extends StandardSchemaV1>(
|
|
17
17
|
schema: Schema,
|
|
18
18
|
toJsonSchema: (schema: Schema) => Record<string, unknown>,
|
|
@@ -15,7 +15,7 @@ Returns the collected frames plus the `done` frame so a scenario can layer
|
|
|
15
15
|
its provider-specific assertions on top. This is the conformance seam: one
|
|
16
16
|
suite of invariants, N engine adapters run against it.
|
|
17
17
|
*/
|
|
18
|
-
// @
|
|
18
|
+
// @documentation plumbing
|
|
19
19
|
export async function assertAgentFrameConformance(
|
|
20
20
|
stream: AsyncIterable<AgentFrame>,
|
|
21
21
|
): Promise<{ frames: AgentFrame[]; done: Extract<AgentFrame, { type: 'done' }> }> {
|
|
@@ -17,7 +17,7 @@ surface out, with every `call` recorded so a test can assert exactly which
|
|
|
17
17
|
tools an engine dispatched and with what arguments. Prompts and resources are
|
|
18
18
|
empty — engines must tolerate an app exposing none.
|
|
19
19
|
*/
|
|
20
|
-
// @
|
|
20
|
+
// @documentation plumbing
|
|
21
21
|
export function createScriptedSurface(
|
|
22
22
|
tools: ScriptedTool[] = [],
|
|
23
23
|
): AgentSurface & { calls: RecordedCall[] } {
|
|
@@ -52,7 +52,7 @@ build's writeTestRpcDts / writeTestSocketsDts emit one entry per verb/socket
|
|
|
52
52
|
with no imports. Empty here; types arrive once the app has been built. Mirrors
|
|
53
53
|
url's RpcRoutes / health's AppHealthMap.
|
|
54
54
|
*/
|
|
55
|
-
// @
|
|
55
|
+
// @documentation testing
|
|
56
56
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated testRpc.d.ts
|
|
57
57
|
export interface RpcClient {}
|
|
58
58
|
// biome-ignore lint/suspicious/noEmptyInterface: augmented by the generated testSockets.d.ts
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/* The callee names the `.abide` compiler recognises as reactive declarations
|
|
2
|
-
(`let x = state(...)`, `linked(...)`, `
|
|
2
|
+
(`let x = state(...)`, `linked(...)`, `computed(...)`, `prop(...)`): the shared
|
|
3
3
|
"is this a reactive binding" allowlist read by the desugarer, the nested-script
|
|
4
4
|
scoper, and the type-checking shadow. How each lowers — a serializable doc slot
|
|
5
5
|
vs a `.value` cell — is decided per-site; this is only the membership set, so a
|
|
6
6
|
new primitive is a single edit here. */
|
|
7
|
-
export const REACTIVE_CALLEES: ReadonlySet<string> = new Set([
|
|
7
|
+
export const REACTIVE_CALLEES: ReadonlySet<string> = new Set([
|
|
8
|
+
'state',
|
|
9
|
+
'linked',
|
|
10
|
+
'computed',
|
|
11
|
+
'prop',
|
|
12
|
+
])
|
|
@@ -8,10 +8,9 @@ that populates `window.__abide`. Order is the emit order.
|
|
|
8
8
|
export const UI_RUNTIME_IMPORTS: { name: string; specifier: string }[] = [
|
|
9
9
|
{ name: 'html', specifier: 'shared/html' },
|
|
10
10
|
{ name: 'snippet', specifier: 'shared/snippet' },
|
|
11
|
-
{ name: '
|
|
12
|
-
{ name: '
|
|
13
|
-
{ name: '
|
|
14
|
-
{ name: 'derived', specifier: 'ui/derived' },
|
|
11
|
+
{ name: 'scope', specifier: 'ui/scope' },
|
|
12
|
+
{ name: 'enterScope', specifier: 'ui/enterScope' },
|
|
13
|
+
{ name: 'exitScope', specifier: 'ui/exitScope' },
|
|
15
14
|
{ name: 'effect', specifier: 'ui/effect' },
|
|
16
15
|
{ name: 'mount', specifier: 'ui/dom/mount' },
|
|
17
16
|
{ name: 'appendText', specifier: 'ui/dom/appendText' },
|
|
@@ -29,7 +29,7 @@ that same linked sheet, so a page renders styled before the client bundle loads,
|
|
|
29
29
|
no inlined `<style>`. The elements still carry their `data-a-…` scope attributes
|
|
30
30
|
either way (one per `<style>` covering them — see `analyzeComponent`).
|
|
31
31
|
*/
|
|
32
|
-
// @
|
|
32
|
+
// @documentation plumbing
|
|
33
33
|
export const abideUiPlugin: BunPlugin = {
|
|
34
34
|
name: 'abide-ui',
|
|
35
35
|
setup(build) {
|
|
@@ -30,7 +30,7 @@ export function analyzeComponent(source: string, scopeSeed?: string): AnalyzedCo
|
|
|
30
30
|
const scriptBody = (scriptMatch?.[1] ?? '').trim()
|
|
31
31
|
const template = source.replace(/^\s*<script[^>]*>[\s\S]*?<\/script>/, '').trim()
|
|
32
32
|
|
|
33
|
-
const { code: desugared, stateNames, derivedNames } = desugarSignals(scriptBody)
|
|
33
|
+
const { code: desugared, stateNames, derivedNames, computedNames } = desugarSignals(scriptBody)
|
|
34
34
|
const lowered = desugared.trim() === '' ? '' : lowerDocAccess(desugared, 'model')
|
|
35
35
|
/* Hoist top-level import statements (e.g. child components) out of the script
|
|
36
36
|
so the module wrapper can place them at module scope — they can't live
|
|
@@ -54,6 +54,7 @@ export function analyzeComponent(source: string, scopeSeed?: string): AnalyzedCo
|
|
|
54
54
|
imports: imports.join('\n'),
|
|
55
55
|
stateNames,
|
|
56
56
|
derivedNames,
|
|
57
|
+
computedNames,
|
|
57
58
|
nodes,
|
|
58
59
|
styles,
|
|
59
60
|
/* Hydration adopts every block in place — including `await`, which resumes
|
|
@@ -7,7 +7,7 @@ import type { AnalyzedComponent } from './types/AnalyzedComponent.ts'
|
|
|
7
7
|
Compiles a single-file abide component into the body of a client build function.
|
|
8
8
|
Runs the shared front-end (`analyzeComponent`), generates the DOM build from the
|
|
9
9
|
template, and hoists static paths to cells. The returned body runs against a
|
|
10
|
-
`host` element with `doc`/`state`/`
|
|
10
|
+
`host` element with `doc`/`state`/`computed`/`effect` and the dom bindings in
|
|
11
11
|
scope and defines `model` itself. `compileModule` wraps it (and the SSR body) into
|
|
12
12
|
a real module; tests wrap it with `new Function`.
|
|
13
13
|
|
|
@@ -21,8 +21,8 @@ export function compileComponent(
|
|
|
21
21
|
scopeSeed?: string,
|
|
22
22
|
analyzed: AnalyzedComponent = analyzeComponent(source, scopeSeed),
|
|
23
23
|
): string {
|
|
24
|
-
const { script, stateNames, derivedNames, nodes } = analyzed
|
|
25
|
-
const build = generateBuild(nodes, 'host', stateNames, derivedNames, isLayout)
|
|
24
|
+
const { script, stateNames, derivedNames, computedNames, nodes } = analyzed
|
|
25
|
+
const build = generateBuild(nodes, 'host', stateNames, derivedNames, computedNames, isLayout)
|
|
26
26
|
/* The scoped CSS is bundled into the entry stylesheet (see `abideUiPlugin`), not
|
|
27
27
|
injected at runtime; the build only needs the `data-a-…` scope attributes on
|
|
28
28
|
elements, which `generateBuild` reads from each node's annotated `scopes`. */
|
|
@@ -93,6 +93,11 @@ ${options.moduleId === undefined ? '' : `component.__abideId = ${JSON.stringify(
|
|
|
93
93
|
if (entry.name === 'mount' || entry.name === 'hydrate') {
|
|
94
94
|
return true
|
|
95
95
|
}
|
|
96
|
+
/* The SSR render always brackets itself with a per-render scope (compileSSR),
|
|
97
|
+
so these are referenced whenever the `render` export is — i.e. always. */
|
|
98
|
+
if (entry.name === 'enterScope' || entry.name === 'exitScope') {
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
96
101
|
/* Render-pass helpers are emitted by both back-ends (e.g. client and server
|
|
97
102
|
await/try blocks both call nextBlockId); their names are distinctive enough
|
|
98
103
|
that scanning both surfaces adds no false match. */
|
|
@@ -110,10 +115,33 @@ ${userImports}
|
|
|
110
115
|
${moduleBody}`
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
/* Indents a body block for embedding inside a wrapper function.
|
|
118
|
+
/* Indents a body block for embedding inside a wrapper function. Lines whose start
|
|
119
|
+
sits inside a multi-line template literal are left untouched — their leading
|
|
120
|
+
whitespace is significant string content (e.g. a CodeBlock's `code={`…`}`
|
|
121
|
+
snippet), and indenting it would corrupt the rendered sample. An odd count of
|
|
122
|
+
unescaped backticks on a line flips in/out of a literal. */
|
|
114
123
|
function indent(body: string): string {
|
|
124
|
+
let insideTemplateLiteral = false
|
|
115
125
|
return body
|
|
116
126
|
.split('\n')
|
|
117
|
-
.map((line) =>
|
|
127
|
+
.map((line) => {
|
|
128
|
+
const indented = insideTemplateLiteral || line === '' ? line : ` ${line}`
|
|
129
|
+
if (unescapedBacktickCount(line) % 2 === 1) {
|
|
130
|
+
insideTemplateLiteral = !insideTemplateLiteral
|
|
131
|
+
}
|
|
132
|
+
return indented
|
|
133
|
+
})
|
|
118
134
|
.join('\n')
|
|
119
135
|
}
|
|
136
|
+
|
|
137
|
+
/* Counts backticks not preceded by a backslash — the template-literal delimiters
|
|
138
|
+
on a line, ignoring escaped `\`` inside one. */
|
|
139
|
+
function unescapedBacktickCount(line: string): number {
|
|
140
|
+
let count = 0
|
|
141
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
142
|
+
if (line[index] === '`' && line[index - 1] !== '\\') {
|
|
143
|
+
count += 1
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return count
|
|
147
|
+
}
|
|
@@ -16,7 +16,7 @@ front-end, then the SSR back-end, and returns `{ html, state, awaits }`:
|
|
|
16
16
|
Effects are stripped — they are client lifecycle and emit no HTML, so the server
|
|
17
17
|
render is a snapshot of the markup before any effect runs.
|
|
18
18
|
|
|
19
|
-
Runs with `doc`/`state`/`
|
|
19
|
+
Runs with `doc`/`state`/`computed`/`effect`/`nextBlockId`/`enterRenderPass`/
|
|
20
20
|
`exitRenderPass` in scope and defines `model`. The body is bracketed by a render
|
|
21
21
|
pass so the outermost render resets the block-id counter and an inlined child
|
|
22
22
|
render continues it — keeping await/try ids unique and aligned with the client.
|
|
@@ -30,16 +30,16 @@ export function compileSSR(
|
|
|
30
30
|
scopeSeed?: string,
|
|
31
31
|
analyzed: AnalyzedComponent = analyzeComponent(source, scopeSeed),
|
|
32
32
|
): string {
|
|
33
|
-
const { script, stateNames, derivedNames, nodes } = analyzed
|
|
34
|
-
const ssr = generateSSR(nodes, stateNames, derivedNames, isLayout)
|
|
33
|
+
const { script, stateNames, derivedNames, computedNames, nodes } = analyzed
|
|
34
|
+
const ssr = generateSSR(nodes, stateNames, derivedNames, computedNames, isLayout)
|
|
35
35
|
/* No `<style>` in the markup — the scoped CSS is bundled into the entry stylesheet
|
|
36
36
|
the shell links (see `abideUiPlugin`), so SSR output is styled by that sheet. The
|
|
37
37
|
elements still carry their `data-a-…` scopes via `generateSSR`. */
|
|
38
38
|
/* `typeof model` guards a component with no reactive state (a pure-async or
|
|
39
39
|
static component declares no `model`); its snapshot is then empty. */
|
|
40
40
|
return (
|
|
41
|
-
`enterRenderPass();\ntry {\n${stripEffects(script)}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\n${ssr}` +
|
|
41
|
+
`enterRenderPass();\nconst $scope = enterScope();\ntry {\n${stripEffects(script)}\n${SSR_ESCAPE}\nconst $out = [];\nconst $awaits = [];\n${ssr}` +
|
|
42
42
|
`return { html: $out.join(''), state: (typeof model !== 'undefined' ? model.snapshot() : {}), awaits: $awaits };\n` +
|
|
43
|
-
`} finally { exitRenderPass(); }`
|
|
43
|
+
`} finally { exitScope($scope); exitRenderPass(); }`
|
|
44
44
|
)
|
|
45
45
|
}
|