@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/src/abideResolverPlugin.ts +6 -1
  4. package/src/devEntry.ts +43 -9
  5. package/src/lib/cli/parseArgvForRpc.ts +21 -24
  6. package/src/lib/cli/runCli.ts +9 -24
  7. package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
  8. package/src/lib/server/rpc/defineRpc.ts +9 -1
  9. package/src/lib/server/runtime/createServer.ts +29 -12
  10. package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
  11. package/src/lib/server/runtime/gzipResponse.ts +14 -16
  12. package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
  13. package/src/lib/server/runtime/types/RequestStore.ts +16 -0
  14. package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
  15. package/src/lib/server/sockets/defineSocket.ts +5 -1
  16. package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
  17. package/src/lib/shared/activeCacheStore.ts +5 -14
  18. package/src/lib/shared/activePage.ts +5 -19
  19. package/src/lib/shared/augmentModule.ts +18 -0
  20. package/src/lib/shared/basePath.ts +6 -5
  21. package/src/lib/shared/baseResolver.ts +10 -0
  22. package/src/lib/shared/baseSlot.ts +6 -12
  23. package/src/lib/shared/cache.ts +21 -1
  24. package/src/lib/shared/cacheStoreResolver.ts +12 -0
  25. package/src/lib/shared/cacheStoreSlot.ts +5 -13
  26. package/src/lib/shared/changeAffectsClient.ts +35 -0
  27. package/src/lib/shared/createResolverSlot.ts +37 -0
  28. package/src/lib/shared/debugGate.ts +90 -0
  29. package/src/lib/shared/emitLogRecord.ts +18 -4
  30. package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
  31. package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
  32. package/src/lib/shared/isDebugEnabled.ts +3 -10
  33. package/src/lib/shared/isDebugNegated.ts +4 -6
  34. package/src/lib/shared/pageResolver.ts +16 -0
  35. package/src/lib/shared/pageSlot.ts +5 -14
  36. package/src/lib/shared/requestScopeResolver.ts +12 -0
  37. package/src/lib/shared/requestScopeSlot.ts +7 -12
  38. package/src/lib/shared/responseBodyKind.ts +35 -0
  39. package/src/lib/shared/setBaseResolver.ts +2 -4
  40. package/src/lib/shared/setCacheStoreResolver.ts +3 -5
  41. package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
  42. package/src/lib/shared/setPageResolver.ts +2 -5
  43. package/src/lib/shared/setRequestScopeResolver.ts +3 -5
  44. package/src/lib/shared/types/DebugGate.ts +10 -0
  45. package/src/lib/shared/types/ResolverSlot.ts +10 -0
  46. package/src/lib/shared/writeDts.ts +8 -1
  47. package/src/lib/shared/writePublicAssetsDts.ts +5 -9
  48. package/src/lib/shared/writeRoutesDts.ts +12 -16
  49. package/src/lib/shared/writeRpcDts.ts +13 -12
  50. package/src/lib/shared/writeTestRpcDts.ts +11 -11
  51. package/src/lib/shared/writeTestSocketsDts.ts +9 -9
  52. package/src/lib/ui/compile/abideUiPlugin.ts +9 -7
  53. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +24 -12
  54. package/src/lib/ui/compile/compileModule.ts +37 -16
  55. package/src/lib/ui/compile/compileShadow.ts +75 -1
  56. package/src/lib/ui/compile/createShadowLanguageService.ts +29 -3
  57. package/src/lib/ui/compile/generateBuild.ts +7 -1
  58. package/src/lib/ui/compile/parseTemplate.ts +4 -1
  59. package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
  60. package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
  61. package/src/lib/ui/dom/skeleton.ts +10 -1
  62. package/src/lib/ui/dom/switchBlock.ts +15 -63
  63. package/src/lib/ui/dom/when.ts +11 -64
  64. package/src/lib/ui/runtime/createDoc.ts +102 -12
  65. package/src/zodCjsPlugin.ts +16 -1
  66. package/src/lib/shared/matchesDebugPattern.ts +0 -16
  67. package/src/lib/shared/parseDebugPatterns.ts +0 -21
@@ -1,25 +1,11 @@
1
- import { pageSlot } from './pageSlot.ts'
1
+ import { pageResolver } from './pageResolver.ts'
2
2
  import type { PageSnapshot } from './types/PageSnapshot.ts'
3
3
 
4
4
  /*
5
- Resolves the active page snapshot. The runtime is registered via
6
- `setPageResolver` from the server entry (request-scoped via ALS) or the
7
- client entry (module-level singleton). If no resolver is registered, a single
8
- fallback snapshot is created lazily so isolated tests still work. Mirrors
9
- activeCacheStore.
5
+ Resolves the active page snapshot: the registered resolver's snapshot, or a
6
+ single lazily-created empty snapshot when none is registered (so isolated tests
7
+ work). The fallback creator guarantees a value, hence the non-null assertion.
10
8
  */
11
9
  export function activePage(): PageSnapshot {
12
- const fromResolver = pageSlot.resolver?.()
13
- if (fromResolver) {
14
- return fromResolver
15
- }
16
- if (!pageSlot.fallback) {
17
- pageSlot.fallback = {
18
- route: '',
19
- params: {},
20
- url: new URL('http://localhost/'),
21
- navigating: false,
22
- }
23
- }
24
- return pageSlot.fallback
10
+ return pageResolver.get()!
25
11
  }
@@ -0,0 +1,18 @@
1
+ /*
2
+ Emits one `declare module '<module>' { interface <iface> { … } }` block, the
3
+ module-augmentation shape every codegen writer (routes, rpc, publicAssets,
4
+ testRpc, testSockets) shares. `entries` is `[key, type]` pairs already in final
5
+ order; each becomes an 8-space-indented `"key": type` member, the indentation
6
+ the consumer src tsconfig include expects. Returns the block body only — the
7
+ writeDts envelope adds banner + footer.
8
+ */
9
+ export function augmentModule(module: string, iface: string, entries: [string, string][]): string {
10
+ const members = entries
11
+ .map(([key, type]) => ` ${JSON.stringify(key)}: ${type}`)
12
+ .join('\n')
13
+ return `declare module '${module}' {
14
+ interface ${iface} {
15
+ ${members}
16
+ }
17
+ }`
18
+ }
@@ -1,10 +1,11 @@
1
- import { baseSlot } from './baseSlot.ts'
1
+ import { baseResolver } from './baseResolver.ts'
2
2
 
3
3
  /*
4
- The current mount base path ('' at root). Resolved per side: the server
5
- installs an APP_URL-derived resolver at boot, the client one reading
6
- window.__SSR__.base. url() reads this to prefix rooted internal paths.
4
+ The current mount base path ('' at root). Resolved per side: the server installs
5
+ an APP_URL-derived resolver at boot, the client one reading window.__SSR__.base.
6
+ url() reads this to prefix rooted internal paths. Defaults to '' when no resolver
7
+ or fallback is set.
7
8
  */
8
9
  export function basePath(): string {
9
- return baseSlot.resolver?.() ?? baseSlot.fallback ?? ''
10
+ return baseResolver.get() ?? ''
10
11
  }
@@ -0,0 +1,10 @@
1
+ import { createResolverSlot } from './createResolverSlot.ts'
2
+
3
+ /*
4
+ The mount-base slot/resolver/reader bundle. The server entry installs an
5
+ APP_URL-derived resolver at boot; the client entry one reading
6
+ window.__SSR__.base. No lazy fallback creator — `fallback` is a plain string set
7
+ directly by isolated tests, and basePath() supplies the '' default. baseSlot /
8
+ basePath re-export the slot and reader; setBaseResolver the setter.
9
+ */
10
+ export const baseResolver = createResolverSlot<string>()
@@ -1,14 +1,8 @@
1
+ import { baseResolver } from './baseResolver.ts'
2
+
1
3
  /*
2
- Internal slot the runtime entries register their mount-base resolver into.
3
- The server entry installs an APP_URL-derived resolver at boot; the client
4
- entry installs one reading window.__SSR__.base. `fallback` is a single value
5
- used only when no resolver is registered — lets isolated tests set a base
6
- without spinning up the runtime. Mirrors pageSlot / cacheStoreSlot.
4
+ Internal slot the runtime entries register their mount-base resolver into (see
5
+ baseResolver). Exposed so test helpers snapshot/poke `.resolver` and `.fallback`
6
+ directly.
7
7
  */
8
- export const baseSlot: {
9
- resolver: (() => string) | undefined
10
- fallback: string | undefined
11
- } = {
12
- resolver: undefined,
13
- fallback: undefined,
14
- }
8
+ export const baseSlot = baseResolver.slot
@@ -199,7 +199,7 @@ export function cache<Args, Return>(
199
199
  fetch hands each reader a fresh object; cloning keeps warm reads the same.
200
200
  */
201
201
  if (!isRaw && existing?.value !== undefined) {
202
- return Promise.resolve(structuredClone(existing.value)) as Promise<Return>
202
+ return Promise.resolve(cloneWarmValue(existing.value)) as Promise<Return>
203
203
  }
204
204
  const responsePromise = invokeRemote(
205
205
  store,
@@ -216,6 +216,26 @@ export function cache<Args, Return>(
216
216
  return read
217
217
  }
218
218
 
219
+ /*
220
+ Deep-copies a warm value so each reader gets its own mutable object — the
221
+ no-shared-mutation invariant the warm path turns on (a live fetch hands every
222
+ reader a fresh object; a warm read must match). A warm value only ever comes
223
+ from the json or text body kinds (bodyValueForKind): json yields JSON.parse
224
+ output and text yields a string, so the whole population is JSON-round-trippable
225
+ by construction — no Date/Map/Blob/cycle a structuredClone would be needed for.
226
+ A primitive (string/number/boolean from a text or scalar-json body) is immutable,
227
+ so it is returned as-is with no copy. An object/array goes through a JSON
228
+ round-trip, ~2x faster than structuredClone for this exact shape (measured:
229
+ 773µs→516µs on a 149KB list, 3.4ms→1.7ms on 474KB) while producing the same
230
+ fresh, mutable, isomorphic-with-a-cold-read copy.
231
+ */
232
+ function cloneWarmValue(value: unknown): unknown {
233
+ if (typeof value !== 'object' || value === null) {
234
+ return value
235
+ }
236
+ return JSON.parse(JSON.stringify(value))
237
+ }
238
+
219
239
  /*
220
240
  Normalises the `swr` option to its window, or undefined when off. `true` (or
221
241
  `{}`) is stale-while-revalidate with no window — refetch immediately on every
@@ -0,0 +1,12 @@
1
+ import { createCacheStore } from './createCacheStore.ts'
2
+ import { createResolverSlot } from './createResolverSlot.ts'
3
+ import type { CacheStore } from './types/CacheStore.ts'
4
+
5
+ /*
6
+ The active-CacheStore slot/resolver/reader bundle. The server entry installs an
7
+ ALS-backed resolver (request-scoped); the client entry a module-singleton one.
8
+ With no resolver registered, a single fallback store is created lazily so
9
+ isolated tests work without booting the runtime. cacheStoreSlot / activeCacheStore
10
+ re-export the slot and reader; setCacheStoreResolver the setter.
11
+ */
12
+ export const cacheStoreResolver = createResolverSlot<CacheStore>(createCacheStore)
@@ -1,16 +1,8 @@
1
- import type { CacheStore } from './types/CacheStore.ts'
1
+ import { cacheStoreResolver } from './cacheStoreResolver.ts'
2
2
 
3
3
  /*
4
- Internal slot the runtime entries register their resolver into. The
5
- server entry installs an ALS-backed resolver (request-scoped); the
6
- client entry installs a module-singleton resolver. `fallback` is a
7
- single lazy store used only when no resolver is registered — keeps
8
- isolated tests working without forcing them to spin up the runtime.
4
+ Internal slot the runtime entries register their resolver into (see
5
+ cacheStoreResolver). Exposed so test helpers snapshot/poke `.resolver` and
6
+ `.fallback` directly.
9
7
  */
10
- export const cacheStoreSlot: {
11
- resolver: (() => CacheStore | undefined) | undefined
12
- fallback: CacheStore | undefined
13
- } = {
14
- resolver: undefined,
15
- fallback: undefined,
16
- }
8
+ export const cacheStoreSlot = cacheStoreResolver.slot
@@ -0,0 +1,35 @@
1
+ /*
2
+ Classifies a watcher change (a path relative to the project's `src/`, with
3
+ either OS separator) by whether it can alter the client bundle. The dev
4
+ orchestrator uses this to skip the full client `Bun.build` for changes that
5
+ only reach the server or MCP runtimes, while still restarting the SSR worker.
6
+
7
+ Server/MCP-only — client unaffected, skip the client rebuild:
8
+ - `server/rpc/**` — the client only ships proxy stubs derived from the file
9
+ path (rpc url) and the `<METHOD>(...)` wrapper, not the
10
+ handler body; the rpc manifest is server-only. A body
11
+ edit produces an identical stub. (Risk: changing the
12
+ method, the export name, or `outbox: true` does change
13
+ the stub — those need a manual full rebuild.)
14
+ - `server/sockets/**` — name-only `socketProxy` stubs derived from the file
15
+ path; socket opts are server-side. Body edits don't
16
+ change the stub.
17
+ - `mcp/**` — prompts/resources are MCP-only; the client bundle never
18
+ imports them (it emits empty stubs defensively).
19
+ - `server/config.ts` — server boot-time env validation (`abide:config`); never
20
+ reaches the client.
21
+
22
+ Everything else (`.abide` templates, `ui/**`, `shared/**`, `app.ts`, state
23
+ files, the rest of `server/`) is treated as client-affecting. Conservative by
24
+ default: an unrecognised path returns true so an ambiguous change still pays the
25
+ full rebuild rather than risking a stale client.
26
+ */
27
+ export function changeAffectsClient(relativePath: string): boolean {
28
+ const path = relativePath.split('\\').join('/')
29
+ const serverOnly =
30
+ path.startsWith('server/rpc/') ||
31
+ path.startsWith('server/sockets/') ||
32
+ path.startsWith('mcp/') ||
33
+ path === 'server/config.ts'
34
+ return !serverOnly
35
+ }
@@ -0,0 +1,37 @@
1
+ import type { ResolverSlot } from './types/ResolverSlot.ts'
2
+
3
+ /*
4
+ The slot/resolver/reader triple the runtime entries share for cache store,
5
+ page, and mount-base state. A runtime entry registers a side-specific resolver
6
+ via `set` (ALS-backed on the server, a module singleton on the client); `get`
7
+ returns the resolved value, falling back to a single lazily-created fallback
8
+ when no resolver is registered so isolated tests work without booting the
9
+ runtime. `slot` stays exposed so test helpers can snapshot/poke `.resolver` and
10
+ `.fallback` directly. Pass `createFallback` for a lazily-built, cached fallback
11
+ (cache store, page); omit it for a slot whose fallback is a plain value set
12
+ directly (mount base) — `get` then returns `T | undefined`.
13
+ */
14
+ export function createResolverSlot<T>(createFallback?: () => T): {
15
+ slot: ResolverSlot<T>
16
+ set: (fn: () => T | undefined) => void
17
+ get: () => T | undefined
18
+ } {
19
+ const slot: ResolverSlot<T> = { resolver: undefined, fallback: undefined }
20
+
21
+ function set(fn: () => T | undefined): void {
22
+ slot.resolver = fn
23
+ }
24
+
25
+ function get(): T | undefined {
26
+ const fromResolver = slot.resolver?.()
27
+ if (fromResolver !== undefined) {
28
+ return fromResolver
29
+ }
30
+ if (createFallback && slot.fallback === undefined) {
31
+ slot.fallback = createFallback()
32
+ }
33
+ return slot.fallback
34
+ }
35
+
36
+ return { slot, set, get }
37
+ }
@@ -0,0 +1,90 @@
1
+ import type { DebugGate } from './types/DebugGate.ts'
2
+
3
+ /*
4
+ The DEBUG-gate core: one memoized decision-maker per distinct DEBUG string,
5
+ npm-debug conventions. `enabled(name)` answers "is gated channel X on?",
6
+ `negated(name)` answers "did a `-X` pattern explicitly shut it off?" — the two
7
+ questions isDebugEnabled / isDebugNegated expose. Both were re-deriving the same
8
+ parse + filter work on every emission; here the include/exclude partition is
9
+ computed once per env value (a single pass, not two throwaway-array filters) and
10
+ each per-name boolean is cached, since the server's DEBUG is stable and channel
11
+ names are a tiny fixed set.
12
+
13
+ A new env string (the browser's abide-debug localStorage toggle, or tests
14
+ mutating process.env.DEBUG) misses the single-slot memo and rebuilds — so a
15
+ runtime DEBUG change takes effect on its next read.
16
+ */
17
+
18
+ // One DEBUG pattern (already `-`-stripped) against one channel name: `*` matches
19
+ // everything, `abide:*` matches 'abide' and every 'abide:…' sub-channel, else exact.
20
+ function matches(name: string, pattern: string): boolean {
21
+ if (pattern === '*') {
22
+ return true
23
+ }
24
+ if (pattern.endsWith(':*')) {
25
+ const prefix = pattern.slice(0, -2)
26
+ return name === prefix || name.startsWith(`${prefix}:`)
27
+ }
28
+ return pattern === name
29
+ }
30
+
31
+ // Builds a gate for one env value: partitions the comma list into include/exclude
32
+ // patterns in a single pass, then memoizes each per-name decision in a map.
33
+ function buildGate(env: string | undefined): DebugGate {
34
+ const includes: string[] = []
35
+ const excludes: string[] = []
36
+ if (env) {
37
+ for (const raw of env.split(',')) {
38
+ const pattern = raw.trim()
39
+ if (pattern === '') {
40
+ continue
41
+ }
42
+ if (pattern.startsWith('-')) {
43
+ excludes.push(pattern.slice(1))
44
+ } else {
45
+ includes.push(pattern)
46
+ }
47
+ }
48
+ }
49
+ /* Cache-hit fast path: a Map keyed on channel name, since names are a tiny
50
+ fixed set and the gate runs on every log emission. Negated and enabled
51
+ cache separately — a channel can be queried through either question. */
52
+ const negatedCache = new Map<string, boolean>()
53
+ const enabledCache = new Map<string, boolean>()
54
+ return {
55
+ negated(name: string): boolean {
56
+ const cached = negatedCache.get(name)
57
+ if (cached !== undefined) {
58
+ return cached
59
+ }
60
+ const result = excludes.some((pattern) => matches(name, pattern))
61
+ negatedCache.set(name, result)
62
+ return result
63
+ },
64
+ enabled(name: string): boolean {
65
+ const cached = enabledCache.get(name)
66
+ if (cached !== undefined) {
67
+ return cached
68
+ }
69
+ // Exclusions win over inclusions (npm-debug negation precedence).
70
+ const result =
71
+ !excludes.some((pattern) => matches(name, pattern)) &&
72
+ includes.some((pattern) => matches(name, pattern))
73
+ enabledCache.set(name, result)
74
+ return result
75
+ },
76
+ }
77
+ }
78
+
79
+ /* Single-slot memo on the env string: the gate (partition + decision caches) is
80
+ rebuilt only when DEBUG changes. */
81
+ let lastEnv: string | undefined
82
+ let lastGate: DebugGate = buildGate(undefined)
83
+
84
+ export function debugGate(env: string | undefined): DebugGate {
85
+ if (env !== lastEnv) {
86
+ lastEnv = env
87
+ lastGate = buildGate(env)
88
+ }
89
+ return lastGate
90
+ }
@@ -8,12 +8,21 @@ const useColor = hasBun && Bun.enableANSIColors
8
8
  const RESET = '\x1b[0m'
9
9
  const DIM = '\x1b[2m'
10
10
 
11
- // Wraps `text` in a Bun-resolved ANSI color escape; no-op when colors are disabled or unavailable (browser).
11
+ /* The finite color set the log fragments paint with. Resolve each ANSI-256
12
+ escape once at module load — `Bun.color` was otherwise called per colored
13
+ fragment per line. Empty when colors are off so `paint` short-circuits. */
14
+ const COLOR_NAMES = ['red', 'yellow', 'cyan', 'green', 'blue', 'white'] as const
15
+ const COLOR_ESCAPES: Record<string, string> = useColor
16
+ ? Object.fromEntries(COLOR_NAMES.map((name) => [name, Bun.color(name, 'ansi-256') ?? '']))
17
+ : {}
18
+
19
+ // Wraps `text` in a precomputed ANSI color escape; no-op when colors are disabled or unavailable (browser).
12
20
  function paint(color: string, text: string): string {
13
- if (!useColor) {
21
+ const ansiEscape = COLOR_ESCAPES[color]
22
+ if (ansiEscape === undefined) {
14
23
  return text
15
24
  }
16
- return `${Bun.color(color, 'ansi-256')}${text}${RESET}`
25
+ return `${ansiEscape}${text}${RESET}`
17
26
  }
18
27
 
19
28
  // Applies the ANSI dim attribute; no-op when colors are disabled.
@@ -66,12 +75,17 @@ function formatElapsed(ms: number): string {
66
75
  return `+${ms.toFixed(2)}ms`
67
76
  }
68
77
 
78
+ /* Left-pads a clock component to a fixed width. Module-scoped so it isn't
79
+ re-allocated per log line. */
80
+ function pad(value: number, width = 2): string {
81
+ return String(value).padStart(width, '0')
82
+ }
83
+
69
84
  /* `14:23:01.072` — local wall-clock to the ms. Every record carries `ts`, so the
70
85
  tsv line always leads with one; `+elapsedMs` still trails as the request-relative
71
86
  timing — the two are different axes (when it happened vs how long it took). */
72
87
  function formatClock(ts: number): string {
73
88
  const date = new Date(ts)
74
- const pad = (value: number, width = 2): string => String(value).padStart(width, '0')
75
89
  return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`
76
90
  }
77
91
 
@@ -0,0 +1,12 @@
1
+ import { createResolverSlot } from './createResolverSlot.ts'
2
+ import type { CacheStore } from './types/CacheStore.ts'
3
+
4
+ /*
5
+ Slot + setter for the process-level cache store resolver used by cache() entries
6
+ opting into `global: true`. The server entry registers a module-singleton store
7
+ outliving any one request; the client entry points it at its single tab store so
8
+ `global` is a no-op there. No fallback creator — when unset, globalCacheStore()
9
+ falls through to the active store rather than minting an isolated one.
10
+ globalCacheStoreSlot / setGlobalCacheStoreResolver re-export the slot and setter.
11
+ */
12
+ export const globalCacheStoreResolver = createResolverSlot<CacheStore>()
@@ -1,14 +1,9 @@
1
- import type { CacheStore } from './types/CacheStore.ts'
1
+ import { globalCacheStoreResolver } from './globalCacheStoreResolver.ts'
2
2
 
3
3
  /*
4
- Slot for the process-level cache store resolver used by cache() entries opting
5
- into `global: true`. The server entry registers a module-singleton store that
6
- outlives any one request; the client entry points it at its single tab store so
7
- `global` is a no-op there. Unset means no global store is registered, in which
8
- case globalCacheStore() falls back to the active (request/tab) store.
4
+ Slot for the process-level cache store resolver (see globalCacheStoreResolver).
5
+ Exposed so test helpers snapshot/poke `.resolver` directly. Unset means no global
6
+ store is registered, in which case globalCacheStore() falls back to the active
7
+ (request/tab) store.
9
8
  */
10
- export const globalCacheStoreSlot: {
11
- resolver: (() => CacheStore | undefined) | undefined
12
- } = {
13
- resolver: undefined,
14
- }
9
+ export const globalCacheStoreSlot = globalCacheStoreResolver.slot
@@ -1,6 +1,4 @@
1
- import { isDebugNegated } from './isDebugNegated.ts'
2
- import { matchesDebugPattern } from './matchesDebugPattern.ts'
3
- import { parseDebugPatterns } from './parseDebugPatterns.ts'
1
+ import { debugGate } from './debugGate.ts'
4
2
 
5
3
  /*
6
4
  Whether a DEBUG-gated channel is enabled, npm-debug conventions:
@@ -11,16 +9,11 @@ DEBUG="a,abide" → comma-separated list
11
9
  DEBUG="abide:*,-abide:cache" → negation: exclusions win over inclusions
12
10
  Always-on channels don't consult this — they check isDebugNegated only.
13
11
  The default is guarded: this runs in the browser bundle, where `process`
14
- doesn't exist.
12
+ doesn't exist. Decision routed through debugGate, memoized per DEBUG value.
15
13
  */
16
14
  export function isDebugEnabled(
17
15
  name: string,
18
16
  env: string | undefined = typeof process === 'undefined' ? undefined : process.env.DEBUG,
19
17
  ): boolean {
20
- if (isDebugNegated(name, env)) {
21
- return false
22
- }
23
- return parseDebugPatterns(env)
24
- .filter((pattern) => !pattern.startsWith('-'))
25
- .some((pattern) => matchesDebugPattern(name, pattern))
18
+ return debugGate(env).enabled(name)
26
19
  }
@@ -1,5 +1,4 @@
1
- import { matchesDebugPattern } from './matchesDebugPattern.ts'
2
- import { parseDebugPatterns } from './parseDebugPatterns.ts'
1
+ import { debugGate } from './debugGate.ts'
3
2
 
4
3
  /*
5
4
  Whether a `-` pattern in DEBUG explicitly shuts a channel off — the off
@@ -7,13 +6,12 @@ switch for the always-on channels (the app's name, 'abide'): DEBUG="-abide"
7
6
  silences framework lines including the per-request closing records,
8
7
  DEBUG="-myapp" the app's own. Silencing a channel silences all its levels —
9
8
  levels never gate, in either direction. The default is guarded: this runs in
10
- the browser bundle, where `process` doesn't exist.
9
+ the browser bundle, where `process` doesn't exist. Decision routed through
10
+ debugGate, memoized per DEBUG value.
11
11
  */
12
12
  export function isDebugNegated(
13
13
  name: string,
14
14
  env: string | undefined = typeof process === 'undefined' ? undefined : process.env.DEBUG,
15
15
  ): boolean {
16
- return parseDebugPatterns(env)
17
- .filter((pattern) => pattern.startsWith('-'))
18
- .some((pattern) => matchesDebugPattern(name, pattern.slice(1)))
16
+ return debugGate(env).negated(name)
19
17
  }
@@ -0,0 +1,16 @@
1
+ import { createResolverSlot } from './createResolverSlot.ts'
2
+ import type { PageSnapshot } from './types/PageSnapshot.ts'
3
+
4
+ /*
5
+ The active-page slot/resolver/reader bundle. The server entry installs an
6
+ ALS-backed resolver (request-scoped, so concurrent and streaming renders never
7
+ share state); the client entry a module-singleton one. With no resolver
8
+ registered, a single empty snapshot is created lazily so isolated tests work.
9
+ pageSlot / activePage re-export the slot and reader; setPageResolver the setter.
10
+ */
11
+ export const pageResolver = createResolverSlot<PageSnapshot>(() => ({
12
+ route: '',
13
+ params: {},
14
+ url: new URL('http://localhost/'),
15
+ navigating: false,
16
+ }))
@@ -1,17 +1,8 @@
1
- import type { PageSnapshot } from './types/PageSnapshot.ts'
1
+ import { pageResolver } from './pageResolver.ts'
2
2
 
3
3
  /*
4
- Internal slot the runtime entries register their page resolver into. The
5
- server entry installs an ALS-backed resolver (request-scoped, so concurrent
6
- and streaming renders never share state); the client entry installs a
7
- module-singleton resolver. `fallback` is a single lazy snapshot used only
8
- when no resolver is registered — keeps isolated tests working without forcing
9
- them to spin up the runtime. Mirrors cacheStoreSlot.
4
+ Internal slot the runtime entries register their page resolver into (see
5
+ pageResolver). Exposed so test helpers snapshot/poke `.resolver` and
6
+ `.fallback` directly.
10
7
  */
11
- export const pageSlot: {
12
- resolver: (() => PageSnapshot | undefined) | undefined
13
- fallback: PageSnapshot | undefined
14
- } = {
15
- resolver: undefined,
16
- fallback: undefined,
17
- }
8
+ export const pageSlot = pageResolver.slot
@@ -0,0 +1,12 @@
1
+ import { createResolverSlot } from './createResolverSlot.ts'
2
+ import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
3
+
4
+ /*
5
+ Slot + setter for the request-scope resolver. The server installs an ALS-backed
6
+ resolver (createServer, reading the RequestStore); the client a module-singleton
7
+ seeded from __SSR__ (startClient). No fallback creator — an unset resolver (or one
8
+ returning undefined outside any request) means "no scope": callers read
9
+ `.resolver?.()` and treat undefined as absent. requestScopeSlot /
10
+ setRequestScopeResolver re-export the slot and setter.
11
+ */
12
+ export const requestScopeResolver = createResolverSlot<RequestScopeInfo>()
@@ -1,15 +1,10 @@
1
- import type { RequestScopeInfo } from './types/RequestScopeInfo.ts'
1
+ import { requestScopeResolver } from './requestScopeResolver.ts'
2
2
 
3
3
  /*
4
- Internal slot the runtime entries register their request-scope resolver into.
5
- The server installs an ALS-backed resolver (createServer, reading the
6
- RequestStore); the client installs a module-singleton resolver seeded from
7
- __SSR__ (startClient). Undefined resolveror a resolver returning undefined
8
- outside any request means "no scope": trace() returns undefined and log
9
- lines print without the context prefix. Mirrors pageSlot / cacheStoreSlot.
4
+ Internal slot the runtime entries register their request-scope resolver into (see
5
+ requestScopeResolver). Exposed so callers read `.resolver?.()` and test helpers
6
+ snapshot/poke `.resolver` directly. Undefined resolver or one returning
7
+ undefined outside any requestmeans "no scope": trace() returns undefined and
8
+ log lines print without the context prefix.
10
9
  */
11
- export const requestScopeSlot: {
12
- resolver: (() => RequestScopeInfo | undefined) | undefined
13
- } = {
14
- resolver: undefined,
15
- }
10
+ export const requestScopeSlot = requestScopeResolver.slot
@@ -0,0 +1,35 @@
1
+ import { contentBodyKind } from './contentBodyKind.ts'
2
+ import { contentTypeOf } from './contentTypeOf.ts'
3
+
4
+ /*
5
+ Compressible Content-Types — text and structured-text payloads. Binary or
6
+ already-compressed bodies (images, fonts, archives, zstd/gzip blobs) gain
7
+ nothing from a second pass and only burn CPU.
8
+ */
9
+ const COMPRESSIBLE_TYPE =
10
+ /^(?:text\/|application\/(?:json|javascript|xml|[\w.-]+\+(?:json|xml))|image\/svg)/
11
+
12
+ /*
13
+ The post-handler body class that decides a dynamic response's wire handling:
14
+ `streaming` (SSE / JSONL — drained frame-by-frame, never gzip-buffered, opted
15
+ out of the idle timeout), `compressible` (text/structured-text — gzipped when
16
+ the client accepts it), or `opaque` (binary/already-encoded — passed through).
17
+
18
+ One header read + classify per response, threaded through the dispatch pipeline
19
+ so `gzipResponse` and the idle-timeout opt-out don't each re-derive it from the
20
+ Content-Type (the S2 finding: the same body was classified 3-4×). Mirrors the
21
+ shared `contentBodyKind` streaming bucket so the wire path and the decode path
22
+ can't disagree on what's a stream.
23
+ */
24
+ export type ResponseBodyKind = 'streaming' | 'compressible' | 'opaque'
25
+
26
+ export function responseBodyKind(response: Response): ResponseBodyKind {
27
+ const contentType = contentTypeOf(response.headers)
28
+ if (contentBodyKind(contentType) === 'streaming') {
29
+ return 'streaming'
30
+ }
31
+ if (COMPRESSIBLE_TYPE.test(contentType)) {
32
+ return 'compressible'
33
+ }
34
+ return 'opaque'
35
+ }
@@ -1,6 +1,4 @@
1
- import { baseSlot } from './baseSlot.ts'
1
+ import { baseResolver } from './baseResolver.ts'
2
2
 
3
3
  // Registers the runtime's mount-base resolver. Called once per side at boot.
4
- export function setBaseResolver(fn: () => string): void {
5
- baseSlot.resolver = fn
6
- }
4
+ export const setBaseResolver = baseResolver.set
@@ -1,6 +1,4 @@
1
- import { cacheStoreSlot } from './cacheStoreSlot.ts'
2
- import type { CacheStore } from './types/CacheStore.ts'
1
+ import { cacheStoreResolver } from './cacheStoreResolver.ts'
3
2
 
4
- export function setCacheStoreResolver(fn: () => CacheStore | undefined): void {
5
- cacheStoreSlot.resolver = fn
6
- }
3
+ // Registers the runtime's active-CacheStore resolver. Called once per side at boot.
4
+ export const setCacheStoreResolver = cacheStoreResolver.set
@@ -1,6 +1,4 @@
1
- import { globalCacheStoreSlot } from './globalCacheStoreSlot.ts'
2
- import type { CacheStore } from './types/CacheStore.ts'
1
+ import { globalCacheStoreResolver } from './globalCacheStoreResolver.ts'
3
2
 
4
- export function setGlobalCacheStoreResolver(fn: () => CacheStore | undefined): void {
5
- globalCacheStoreSlot.resolver = fn
6
- }
3
+ // Registers the process-level cache store resolver. Called once per side at boot.
4
+ export const setGlobalCacheStoreResolver = globalCacheStoreResolver.set
@@ -1,7 +1,4 @@
1
- import { pageSlot } from './pageSlot.ts'
2
- import type { PageSnapshot } from './types/PageSnapshot.ts'
1
+ import { pageResolver } from './pageResolver.ts'
3
2
 
4
3
  // Registers the runtime's page resolver. Called once per side at boot.
5
- export function setPageResolver(fn: () => PageSnapshot | undefined): void {
6
- pageSlot.resolver = fn
7
- }
4
+ export const setPageResolver = pageResolver.set