@abide/abide 0.44.0 → 0.45.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 +8 -6
- package/CHANGELOG.md +64 -0
- package/README.md +171 -181
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +6 -1
- package/src/devEntry.ts +43 -9
- package/src/lib/cli/parseArgvForRpc.ts +21 -24
- package/src/lib/cli/runCli.ts +9 -24
- package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
- package/src/lib/server/error.ts +48 -53
- package/src/lib/server/json.ts +4 -3
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/rpc/defineRpc.ts +11 -9
- package/src/lib/server/rpc/types/RemoteHandler.ts +5 -7
- package/src/lib/server/rpc/types/RpcHelper.ts +125 -35
- package/src/lib/server/rpc/types/TypedError.ts +18 -0
- package/src/lib/server/rpc/validationError.ts +3 -3
- package/src/lib/server/runtime/STATUS_TEXT.ts +22 -0
- package/src/lib/server/runtime/createServer.ts +36 -12
- package/src/lib/server/runtime/devHotModuleResponse.ts +1 -1
- package/src/lib/server/runtime/gzipResponse.ts +14 -16
- package/src/lib/server/runtime/installAmbientScopeStore.ts +33 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -0
- package/src/lib/server/runtime/typedErrorResponse.ts +26 -0
- package/src/lib/server/runtime/types/RequestStore.ts +25 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
- package/src/lib/server/sockets/defineSocket.ts +5 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
- package/src/lib/shared/activeCacheStore.ts +5 -14
- package/src/lib/shared/activePage.ts +5 -19
- package/src/lib/shared/augmentModule.ts +18 -0
- package/src/lib/shared/basePath.ts +6 -5
- package/src/lib/shared/baseResolver.ts +10 -0
- package/src/lib/shared/baseSlot.ts +6 -12
- package/src/lib/shared/cache.ts +46 -4
- package/src/lib/shared/cacheEntryFromSnapshot.ts +21 -1
- package/src/lib/shared/cacheKeyOf.ts +7 -0
- package/src/lib/shared/cacheKeyStore.ts +8 -0
- package/src/lib/shared/cacheStoreResolver.ts +12 -0
- package/src/lib/shared/cacheStoreSlot.ts +5 -13
- package/src/lib/shared/changeAffectsClient.ts +35 -0
- package/src/lib/shared/createResolverSlot.ts +37 -0
- package/src/lib/shared/debugGate.ts +90 -0
- package/src/lib/shared/emitLogRecord.ts +18 -4
- package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
- package/src/lib/shared/httpErrorFor.ts +1 -1
- package/src/lib/shared/isDebugEnabled.ts +3 -10
- package/src/lib/shared/isDebugNegated.ts +4 -6
- package/src/lib/shared/pageResolver.ts +16 -0
- package/src/lib/shared/pageSlot.ts +5 -14
- package/src/lib/shared/recordCacheKey.ts +6 -0
- package/src/lib/shared/requestScopeResolver.ts +12 -0
- package/src/lib/shared/requestScopeSlot.ts +7 -12
- package/src/lib/shared/responseBodyKind.ts +35 -0
- package/src/lib/shared/setBaseResolver.ts +2 -4
- package/src/lib/shared/setCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
- package/src/lib/shared/setPageResolver.ts +2 -5
- package/src/lib/shared/setRequestScopeResolver.ts +3 -5
- package/src/lib/shared/toTagSet.ts +3 -3
- package/src/lib/shared/types/CacheEntry.ts +15 -0
- package/src/lib/shared/types/CacheOptions.ts +5 -5
- package/src/lib/shared/types/CacheSnapshotEntry.ts +5 -0
- package/src/lib/shared/types/DebugGate.ts +10 -0
- package/src/lib/shared/types/ErrorSpec.ts +6 -6
- package/src/lib/shared/types/RemoteFunction.ts +8 -4
- package/src/lib/shared/types/ResolverSlot.ts +10 -0
- package/src/lib/shared/writeDts.ts +8 -1
- package/src/lib/shared/writePublicAssetsDts.ts +5 -9
- package/src/lib/shared/writeRoutesDts.ts +12 -16
- package/src/lib/shared/writeRpcDts.ts +13 -12
- package/src/lib/shared/writeTestRpcDts.ts +11 -11
- package/src/lib/shared/writeTestSocketsDts.ts +9 -9
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +9 -7
- package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +24 -12
- package/src/lib/ui/compile/compileModule.ts +37 -16
- package/src/lib/ui/compile/compileShadow.ts +75 -1
- package/src/lib/ui/compile/createShadowLanguageService.ts +29 -3
- package/src/lib/ui/compile/generateBuild.ts +7 -1
- package/src/lib/ui/compile/generateSSR.ts +10 -3
- package/src/lib/ui/compile/parseTemplate.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +1 -1
- package/src/lib/ui/createScope.ts +11 -4
- package/src/lib/ui/deferResume.ts +29 -0
- package/src/lib/ui/dom/appendText.ts +19 -4
- package/src/lib/ui/dom/awaitBlock.ts +74 -26
- package/src/lib/ui/dom/eachAsync.ts +11 -18
- package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
- package/src/lib/ui/dom/skeleton.ts +10 -1
- package/src/lib/ui/dom/switchBlock.ts +15 -63
- package/src/lib/ui/dom/when.ts +11 -64
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/matchRoute.ts +18 -1
- package/src/lib/ui/remoteProxy.ts +11 -1
- package/src/lib/ui/renderToStream.ts +13 -8
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +14 -1
- package/src/lib/ui/runtime/RESUME.ts +6 -0
- package/src/lib/ui/runtime/ambientScopeBacking.ts +26 -0
- package/src/lib/ui/runtime/createDoc.ts +102 -12
- package/src/lib/ui/runtime/createEffectNode.ts +7 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -0
- package/src/lib/ui/runtime/generationGuard.ts +40 -0
- package/src/lib/ui/runtime/runNode.ts +9 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +4 -3
- package/src/lib/ui/scope.ts +6 -8
- package/src/lib/ui/tryEncodeResume.ts +5 -2
- package/src/lib/ui/types/Scope.ts +3 -4
- package/src/zodCjsPlugin.ts +16 -1
- package/src/lib/server/rpc/buildErrorConstructors.ts +0 -19
- package/src/lib/shared/matchesDebugPattern.ts +0 -16
- package/src/lib/shared/parseDebugPatterns.ts +0 -21
- package/src/lib/shared/types/ErrorConstructors.ts +0 -17
- package/src/lib/shared/types/ErrorDescriptor.ts +0 -10
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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 {
|
|
1
|
+
import { baseResolver } from './baseResolver.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
The current mount base path ('' at root). Resolved per side: the server
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
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
|
package/src/lib/shared/cache.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { keyForRemoteCall } from './keyForRemoteCall.ts'
|
|
|
12
12
|
import { producerKey } from './producerKey.ts'
|
|
13
13
|
import { REMOTE_FUNCTION } from './REMOTE_FUNCTION.ts'
|
|
14
14
|
import { REPLAYABLE_METHODS } from './REPLAYABLE_METHODS.ts'
|
|
15
|
+
import { recordCacheKey } from './recordCacheKey.ts'
|
|
15
16
|
import { SocketDisconnectedError } from './SocketDisconnectedError.ts'
|
|
16
17
|
import { selectorMatcher } from './selectorMatcher.ts'
|
|
17
18
|
import { selectorPrefix } from './selectorPrefix.ts'
|
|
@@ -198,8 +199,22 @@ export function cache<Args, Return>(
|
|
|
198
199
|
reader of the key, so one mutating it would corrupt the others. A live
|
|
199
200
|
fetch hands each reader a fresh object; cloning keeps warm reads the same.
|
|
200
201
|
*/
|
|
201
|
-
if (
|
|
202
|
-
|
|
202
|
+
if (
|
|
203
|
+
!isRaw &&
|
|
204
|
+
existing !== undefined &&
|
|
205
|
+
(existing.value !== undefined || existing.warm !== undefined)
|
|
206
|
+
) {
|
|
207
|
+
/* A deferred seed carries a lazy materializer instead of a decoded value: decode
|
|
208
|
+
it here, on the first read that needs it (off the hydration path), and cache the
|
|
209
|
+
result onto the entry so later reads skip the decode. Drop the materializer once
|
|
210
|
+
used — its closure pins the raw body string, so keeping it alongside the decoded
|
|
211
|
+
value would double memory for exactly the large payloads deferral targets. */
|
|
212
|
+
const warmValue = existing.value !== undefined ? existing.value : existing.warm?.()
|
|
213
|
+
existing.value = warmValue
|
|
214
|
+
existing.warm = undefined
|
|
215
|
+
const warmed = Promise.resolve(cloneWarmValue(warmValue)) as Promise<Return>
|
|
216
|
+
recordCacheKey(warmed, key)
|
|
217
|
+
return warmed
|
|
203
218
|
}
|
|
204
219
|
const responsePromise = invokeRemote(
|
|
205
220
|
store,
|
|
@@ -209,13 +224,40 @@ export function cache<Args, Return>(
|
|
|
209
224
|
args,
|
|
210
225
|
options,
|
|
211
226
|
)
|
|
212
|
-
|
|
227
|
+
if (isRaw) {
|
|
228
|
+
return responsePromise
|
|
229
|
+
}
|
|
230
|
+
/* Tag the decoded value promise with its key so the SSR resume path can recognise a
|
|
231
|
+
cache-backed await value and defer a large one to a `{ defer, key }` marker. */
|
|
232
|
+
const decoded = responsePromise.then(decodeResponse) as Promise<Return>
|
|
233
|
+
recordCacheKey(decoded, key)
|
|
234
|
+
return decoded
|
|
213
235
|
}
|
|
214
236
|
/* Non-enumerable brand; selectorMatcher and the re-wrap guard read it. */
|
|
215
237
|
Object.defineProperty(read, CACHE_WRAPPED, { value: fn })
|
|
216
238
|
return read
|
|
217
239
|
}
|
|
218
240
|
|
|
241
|
+
/*
|
|
242
|
+
Deep-copies a warm value so each reader gets its own mutable object — the
|
|
243
|
+
no-shared-mutation invariant the warm path turns on (a live fetch hands every
|
|
244
|
+
reader a fresh object; a warm read must match). A warm value only ever comes
|
|
245
|
+
from the json or text body kinds (bodyValueForKind): json yields JSON.parse
|
|
246
|
+
output and text yields a string, so the whole population is JSON-round-trippable
|
|
247
|
+
by construction — no Date/Map/Blob/cycle a structuredClone would be needed for.
|
|
248
|
+
A primitive (string/number/boolean from a text or scalar-json body) is immutable,
|
|
249
|
+
so it is returned as-is with no copy. An object/array goes through a JSON
|
|
250
|
+
round-trip, ~2x faster than structuredClone for this exact shape (measured:
|
|
251
|
+
773µs→516µs on a 149KB list, 3.4ms→1.7ms on 474KB) while producing the same
|
|
252
|
+
fresh, mutable, isomorphic-with-a-cold-read copy.
|
|
253
|
+
*/
|
|
254
|
+
function cloneWarmValue(value: unknown): unknown {
|
|
255
|
+
if (typeof value !== 'object' || value === null) {
|
|
256
|
+
return value
|
|
257
|
+
}
|
|
258
|
+
return JSON.parse(JSON.stringify(value))
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
/*
|
|
220
262
|
Normalises the `swr` option to its window, or undefined when off. `true` (or
|
|
221
263
|
`{}`) is stale-while-revalidate with no window — refetch immediately on every
|
|
@@ -792,7 +834,7 @@ function settleRefetchFailure(store: CacheStore, entry: CacheEntry, status?: num
|
|
|
792
834
|
}
|
|
793
835
|
|
|
794
836
|
/* Folds new tags into an entry's existing set without duplicating them. */
|
|
795
|
-
function mergeTags(existing: Set<string> | undefined, incoming: string
|
|
837
|
+
function mergeTags(existing: Set<string> | undefined, incoming: string[]): Set<string> {
|
|
796
838
|
return new Set([...(existing ?? []), ...toTagSet(incoming)])
|
|
797
839
|
}
|
|
798
840
|
|
|
@@ -19,18 +19,38 @@ export function cacheEntryFromSnapshot(entry: CacheSnapshotEntry): CacheEntry {
|
|
|
19
19
|
statusText: entry.statusText,
|
|
20
20
|
headers,
|
|
21
21
|
})
|
|
22
|
+
/* Deferred seed: decode nothing now — hand back a memoized materializer the first read
|
|
23
|
+
invokes, so hydration pays no payload decode. Eager seed: decode up front as before. */
|
|
24
|
+
const warm = entry.lazy
|
|
25
|
+
? memoizeWarm(() => warmValueFromSnapshot(entry.status, headers, entry.body))
|
|
26
|
+
: undefined
|
|
22
27
|
return {
|
|
23
28
|
key: entry.key,
|
|
24
29
|
promise: Promise.resolve(response),
|
|
25
30
|
request: new Request(entry.url, { method: entry.method }),
|
|
26
31
|
ttl: undefined,
|
|
27
32
|
expiresAt: undefined,
|
|
28
|
-
value: warmValueFromSnapshot(entry.status, headers, entry.body),
|
|
33
|
+
value: entry.lazy ? undefined : warmValueFromSnapshot(entry.status, headers, entry.body),
|
|
34
|
+
warm,
|
|
29
35
|
settled: true,
|
|
30
36
|
hydrated: true,
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/* Wraps a warm decode so it runs at most once — the materialized value (including a
|
|
41
|
+
legitimate undefined for a non-warmable status) is cached after the first call. */
|
|
42
|
+
function memoizeWarm(decode: () => unknown): () => unknown {
|
|
43
|
+
let materialized = false
|
|
44
|
+
let value: unknown
|
|
45
|
+
return () => {
|
|
46
|
+
if (!materialized) {
|
|
47
|
+
value = decode()
|
|
48
|
+
materialized = true
|
|
49
|
+
}
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
/*
|
|
35
55
|
Synchronously decodes a snapshot body so the warm entry reads without a
|
|
36
56
|
microtask hop on first render. The json/text branches go through the shared
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { cacheKeyStore } from './cacheKeyStore.ts'
|
|
2
|
+
|
|
3
|
+
/* The store key behind a cache() read's promise, or undefined if it carries none
|
|
4
|
+
(a producer read, a raw Response read, or a non-cache promise). */
|
|
5
|
+
export function cacheKeyOf(promise: Promise<unknown>): string | undefined {
|
|
6
|
+
return cacheKeyStore.get(promise)
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/*
|
|
2
|
+
WeakMap recording the cache key behind a `cache()` read's returned promise, so a
|
|
3
|
+
consumer holding only the promise can recover its key. Mirrors remoteMetaStore (which
|
|
4
|
+
records the synthesized Request the same way). The SSR resume path uses it to decide
|
|
5
|
+
whether a `{#await cache()}` value is a large cache-backed read it can defer — shipping a
|
|
6
|
+
`{ defer, key }` marker instead of the value. Collected with the promise.
|
|
7
|
+
*/
|
|
8
|
+
export const cacheKeyStore = new WeakMap<Promise<unknown>, string>()
|
|
@@ -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
|
|
1
|
+
import { cacheStoreResolver } from './cacheStoreResolver.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
Internal slot the runtime entries register their resolver into
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
const ansiEscape = COLOR_ESCAPES[color]
|
|
22
|
+
if (ansiEscape === undefined) {
|
|
14
23
|
return text
|
|
15
24
|
}
|
|
16
|
-
return `${
|
|
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
|
|
1
|
+
import { globalCacheStoreResolver } from './globalCacheStoreResolver.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
Slot for the process-level cache store resolver
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
@@ -4,7 +4,7 @@ import { HttpError } from './HttpError.ts'
|
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
Builds the HttpError for a non-2xx response, parsing a typed-error body
|
|
7
|
-
(`{ $abideError, data }`, emitted by `error
|
|
7
|
+
(`{ $abideError, data }`, emitted by an `error.typed(...)` constructor and validation 422)
|
|
8
8
|
onto `.kind` / `.data`. Reads a clone so the original `response.body` stays
|
|
9
9
|
unread for callers that inspect it. A non-JSON or malformed body leaves
|
|
10
10
|
`.kind` / `.data` undefined (a plain `error(status, text)`). Shared by the
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
1
|
+
import { pageResolver } from './pageResolver.ts'
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
Internal slot the runtime entries register their page resolver into
|
|
5
|
-
|
|
6
|
-
|
|
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
|