@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.
Files changed (163) hide show
  1. package/AGENTS.md +39 -34
  2. package/CHANGELOG.md +30 -0
  3. package/README.md +21 -21
  4. package/bin/abide.ts +6 -2
  5. package/package.json +5 -5
  6. package/src/abideResolverPlugin.ts +1 -1
  7. package/src/build.ts +1 -1
  8. package/src/compile.ts +1 -1
  9. package/src/devEntry.ts +12 -1
  10. package/src/lib/bundle/BundleMenu.ts +1 -1
  11. package/src/lib/bundle/BundleMenuItem.ts +1 -1
  12. package/src/lib/bundle/BundleWindow.ts +1 -1
  13. package/src/lib/bundle/bundled.ts +1 -1
  14. package/src/lib/bundle/disconnected.abide +105 -75
  15. package/src/lib/bundle/exitWithParent.ts +7 -7
  16. package/src/lib/bundle/onMenu.ts +1 -1
  17. package/src/lib/mcp/createMcpServer.ts +1 -1
  18. package/src/lib/server/AppModule.ts +1 -1
  19. package/src/lib/server/DELETE.ts +1 -1
  20. package/src/lib/server/GET.ts +1 -1
  21. package/src/lib/server/HEAD.ts +1 -1
  22. package/src/lib/server/PATCH.ts +1 -1
  23. package/src/lib/server/POST.ts +1 -1
  24. package/src/lib/server/PUT.ts +1 -1
  25. package/src/lib/server/agent.ts +1 -1
  26. package/src/lib/server/appDataDir.ts +1 -1
  27. package/src/lib/server/cookies.ts +1 -1
  28. package/src/lib/server/env.ts +1 -1
  29. package/src/lib/server/error.ts +1 -1
  30. package/src/lib/server/json.ts +1 -1
  31. package/src/lib/server/jsonl.ts +1 -1
  32. package/src/lib/server/prompts/definePrompt.ts +1 -1
  33. package/src/lib/server/prompts/renderPromptTemplate.ts +1 -1
  34. package/src/lib/server/reachable.ts +1 -1
  35. package/src/lib/server/redirect.ts +1 -1
  36. package/src/lib/server/request.ts +1 -1
  37. package/src/lib/server/rpc/defineVerb.ts +1 -1
  38. package/src/lib/server/runtime/buildCacheSnapshot.ts +1 -1
  39. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +2 -2
  40. package/src/lib/server/runtime/types/InspectorContext.ts +2 -2
  41. package/src/lib/server/server.ts +1 -1
  42. package/src/lib/server/socket.ts +1 -1
  43. package/src/lib/server/sockets/defineSocket.ts +1 -1
  44. package/src/lib/server/sse.ts +1 -1
  45. package/src/lib/shared/HttpError.ts +1 -1
  46. package/src/lib/shared/cache.ts +18 -18
  47. package/src/lib/shared/createSubscriber.ts +1 -1
  48. package/src/lib/shared/health.ts +1 -1
  49. package/src/lib/shared/html.ts +1 -1
  50. package/src/lib/shared/isSubscribable.ts +1 -1
  51. package/src/lib/shared/log.ts +1 -1
  52. package/src/lib/shared/online.ts +1 -1
  53. package/src/lib/shared/page.ts +1 -1
  54. package/src/lib/shared/pending.ts +2 -2
  55. package/src/lib/shared/probeRegistries.ts +2 -2
  56. package/src/lib/shared/refreshing.ts +2 -2
  57. package/src/lib/shared/selectorMatcher.ts +8 -8
  58. package/src/lib/shared/selectorPrefix.ts +1 -1
  59. package/src/lib/shared/snippet.ts +1 -1
  60. package/src/lib/shared/toTagSet.ts +4 -0
  61. package/src/lib/shared/trace.ts +1 -1
  62. package/src/lib/shared/types/CacheEntry.ts +3 -3
  63. package/src/lib/shared/types/CacheOptions.ts +3 -3
  64. package/src/lib/shared/types/CacheSelector.ts +2 -2
  65. package/src/lib/shared/url.ts +1 -1
  66. package/src/lib/shared/withJsonSchema.ts +1 -1
  67. package/src/lib/test/assertAgentFrameConformance.ts +1 -1
  68. package/src/lib/test/createScriptedSurface.ts +1 -1
  69. package/src/lib/test/createTestApp.ts +1 -1
  70. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -2
  71. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +3 -4
  72. package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
  73. package/src/lib/ui/compile/analyzeComponent.ts +2 -1
  74. package/src/lib/ui/compile/compileComponent.ts +3 -3
  75. package/src/lib/ui/compile/compileModule.ts +30 -2
  76. package/src/lib/ui/compile/compileSSR.ts +5 -5
  77. package/src/lib/ui/compile/compileShadow.ts +54 -36
  78. package/src/lib/ui/compile/desugarSignals.ts +147 -18
  79. package/src/lib/ui/compile/generateBuild.ts +15 -28
  80. package/src/lib/ui/compile/generateSSR.ts +20 -13
  81. package/src/lib/ui/compile/isControlFlow.ts +18 -0
  82. package/src/lib/ui/compile/isTextLeaf.ts +15 -0
  83. package/src/lib/ui/compile/lowerContext.ts +45 -5
  84. package/src/lib/ui/compile/prepareNestedScript.ts +12 -8
  85. package/src/lib/ui/compile/renameSignalRefs.ts +21 -4
  86. package/src/lib/ui/compile/skeletonable.ts +1 -14
  87. package/src/lib/ui/compile/types/AnalyzedComponent.ts +1 -0
  88. package/src/lib/ui/computed.ts +32 -0
  89. package/src/lib/ui/createScope.ts +91 -0
  90. package/src/lib/ui/dom/anchorCursor.ts +1 -1
  91. package/src/lib/ui/dom/appendSnippet.ts +1 -1
  92. package/src/lib/ui/dom/appendStatic.ts +1 -1
  93. package/src/lib/ui/dom/appendText.ts +1 -1
  94. package/src/lib/ui/dom/appendTextAt.ts +1 -1
  95. package/src/lib/ui/dom/applyResolved.ts +1 -1
  96. package/src/lib/ui/dom/attach.ts +7 -2
  97. package/src/lib/ui/dom/attr.ts +1 -1
  98. package/src/lib/ui/dom/awaitBlock.ts +1 -1
  99. package/src/lib/ui/dom/cloneStatic.ts +1 -1
  100. package/src/lib/ui/dom/each.ts +4 -3
  101. package/src/lib/ui/dom/eachAsync.ts +1 -1
  102. package/src/lib/ui/dom/hydrate.ts +20 -6
  103. package/src/lib/ui/dom/mount.ts +12 -1
  104. package/src/lib/ui/dom/mountChild.ts +6 -2
  105. package/src/lib/ui/dom/mountSlot.ts +1 -1
  106. package/src/lib/ui/dom/on.ts +10 -4
  107. package/src/lib/ui/dom/openMarker.ts +3 -3
  108. package/src/lib/ui/dom/skeleton.ts +13 -5
  109. package/src/lib/ui/dom/switchBlock.ts +1 -1
  110. package/src/lib/ui/dom/text.ts +1 -1
  111. package/src/lib/ui/dom/tryBlock.ts +1 -1
  112. package/src/lib/ui/dom/when.ts +1 -1
  113. package/src/lib/ui/effect.ts +7 -2
  114. package/src/lib/ui/enterScope.ts +18 -0
  115. package/src/lib/ui/exitScope.ts +8 -0
  116. package/src/lib/ui/history.ts +102 -0
  117. package/src/lib/ui/installHotBridge.ts +6 -8
  118. package/src/lib/ui/linked.ts +0 -1
  119. package/src/lib/ui/navigate.ts +1 -1
  120. package/src/lib/ui/outbox.ts +115 -0
  121. package/src/lib/ui/persist.ts +112 -0
  122. package/src/lib/ui/probeNavigation.ts +1 -1
  123. package/src/lib/ui/remoteProxy.ts +1 -1
  124. package/src/lib/ui/renderToStream.ts +1 -1
  125. package/src/lib/ui/router.ts +1 -1
  126. package/src/lib/ui/runtime/CURRENT_SCOPE.ts +10 -0
  127. package/src/lib/ui/runtime/OUTLET_TAG.ts +1 -1
  128. package/src/lib/ui/runtime/PATCH_BUS.ts +28 -0
  129. package/src/lib/ui/runtime/RESUME.ts +1 -1
  130. package/src/lib/ui/runtime/captureModelDoc.ts +29 -0
  131. package/src/lib/ui/runtime/claimExpected.ts +26 -0
  132. package/src/lib/ui/runtime/createComputedNode.ts +1 -1
  133. package/src/lib/ui/runtime/createDoc.ts +73 -3
  134. package/src/lib/ui/runtime/enterRenderPass.ts +1 -1
  135. package/src/lib/ui/runtime/escapeKey.ts +13 -0
  136. package/src/lib/ui/runtime/exitRenderPass.ts +1 -1
  137. package/src/lib/ui/runtime/hotReplace.ts +17 -8
  138. package/src/lib/ui/runtime/inScope.ts +19 -0
  139. package/src/lib/ui/runtime/localStoragePersistence.ts +34 -0
  140. package/src/lib/ui/runtime/nextBlockId.ts +1 -1
  141. package/src/lib/ui/runtime/seedModelDoc.ts +26 -0
  142. package/src/lib/ui/runtime/types/{Derived.ts → Computed.ts} +1 -1
  143. package/src/lib/ui/runtime/types/Doc.ts +3 -0
  144. package/src/lib/ui/runtime/types/HotInstance.ts +6 -2
  145. package/src/lib/ui/runtime/types/PatchEvent.ts +16 -0
  146. package/src/lib/ui/runtime/unescapeKey.ts +12 -0
  147. package/src/lib/ui/runtime/walkPath.ts +3 -1
  148. package/src/lib/ui/scope.ts +19 -0
  149. package/src/lib/ui/socketProxy.ts +1 -1
  150. package/src/lib/ui/startClient.ts +1 -1
  151. package/src/lib/ui/state.ts +1 -2
  152. package/src/lib/ui/sync.ts +42 -0
  153. package/src/lib/ui/tail.ts +1 -1
  154. package/src/lib/ui/types/History.ts +14 -0
  155. package/src/lib/ui/types/Outbox.ts +14 -0
  156. package/src/lib/ui/types/PersistHandle.ts +11 -0
  157. package/src/lib/ui/types/PersistenceStore.ts +12 -0
  158. package/src/lib/ui/types/Scope.ts +57 -0
  159. package/src/lib/ui/types/SyncTransport.ts +13 -0
  160. package/src/preload.ts +1 -1
  161. package/src/lib/shared/toScopeSet.ts +0 -4
  162. package/src/lib/ui/derived.ts +0 -49
  163. package/src/lib/ui/doc.ts +0 -15
@@ -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 { toScopeSet } from './toScopeSet.ts'
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
- // @readme cache
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
- tagScope(existing, options?.scope)
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 scope tag to probe it from elsewhere.',
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
- tagScope(existing, options?.scope)
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
- scope: options?.scope === undefined ? undefined : toScopeSet(options.scope),
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 tagScope/attachPolicy for retention: a hydrated snapshot entry ships
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
- scopes, `*` for the bare form.
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 `scope: ${[...toScopeSet(arg.scope ?? [])].join(', ')}`
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 mergeScopes(existing: Set<string> | undefined, incoming: string | string[]): Set<string> {
924
- return new Set([...(existing ?? []), ...toScopeSet(incoming)])
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 scope so a later cache.invalidate({ scope })
929
- reaches entries hydrated from the SSR snapshot (which carry a value but no scope)
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 scope.
931
+ drop tags another read site already added; a no-op when the read passes no tags.
932
932
  */
933
- function tagScope(entry: CacheEntry, scope: CacheOptions['scope']): void {
934
- if (scope !== undefined) {
935
- entry.scope = mergeScopes(entry.scope, scope)
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 tagScope for invalidate policies: a read declaring a policy arms an
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
- // @readme plumbing
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
@@ -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
- // @readme observability
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 }
@@ -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
- // @readme plumbing
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 `{ scope }` objects, neither of
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> {
@@ -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
- // @readme observability
16
+ // @documentation observability
17
17
  export const log: Log = Object.assign(
18
18
  createChannelLog(() => appNameSlot.name ?? 'app', true),
19
19
  {
@@ -40,7 +40,7 @@ const subscribeOnline =
40
40
  }
41
41
  })
42
42
 
43
- // @readme probes
43
+ // @documentation probes
44
44
  export function online(): boolean {
45
45
  /* window, not navigator: Bun defines a partial navigator with no onLine. */
46
46
  if (typeof window === 'undefined') {
@@ -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
- // @readme page
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({ scope }) → a tagged group
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
- // @readme probes
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 scope selectors scan many entries and tap the store-wide channel. A
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/scope selectors are cache identities only. Outside a tracking
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({ scope }) → a tagged group
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
- // @readme probes
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 { toScopeSet } from './toScopeSet.ts'
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
- { scope } → any entry sharing one of the requested scope tags. An
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.scope === undefined) {
49
+ if (arg.tags === undefined) {
50
50
  return () => false
51
51
  }
52
- const requestedScopes = toScopeSet(arg.scope)
53
- return (entry) => entry.scope !== undefined && intersects(entry.scope, requestedScopes)
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(entryScopes: Set<string>, requestedScopes: Set<string>): boolean {
62
- for (const scope of requestedScopes) {
63
- if (entryScopes.has(scope)) {
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 scope selectors (they
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
- // @readme plumbing
13
+ // @documentation templating
14
14
  export function snippet<Payload>(payload: Payload): Snippet<Payload> {
15
15
  return { [SNIPPET]: payload }
16
16
  }
@@ -0,0 +1,4 @@
1
+ /* Normalizes a tags option (one tag or many) to a Set for O(1) membership. */
2
+ export function toTagSet(tags: string | string[]): Set<string> {
3
+ return new Set(typeof tags === 'string' ? [tags] : tags)
4
+ }
@@ -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
- // @readme observability
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
- `scope` holds the cache() call's scope tags as a Set so
32
- `cache.invalidate({ scope })` can drop every entry sharing any tag with O(1)
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
- scope?: Set<string>
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
- `scope` is one or more free-form tags grouping unrelated calls so one
10
- `cache.invalidate({ scope })` drops every entry sharing any of them — pass an
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
- scope?: string | string[]
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
- { scope } → any entry sharing one of the requested scope tags
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, 'scope'>
17
+ | Pick<CacheOptions, 'tags'>
@@ -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
- // @readme url
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
- // @readme rpc
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
- // @readme plumbing
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
- // @readme plumbing
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
- // @readme testing
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(...)`, `derived(...)`, `prop(...)`): the shared
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(['state', 'linked', 'derived', 'prop'])
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: 'doc', specifier: 'ui/doc' },
12
- { name: 'state', specifier: 'ui/state' },
13
- { name: 'linked', specifier: 'ui/linked' },
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
- // @readme plumbing
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`/`derived`/`effect` and the dom bindings in
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) => (line === '' ? line : ` ${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`/`derived`/`effect`/`nextBlockId`/`enterRenderPass`/
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
  }