@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
|
@@ -2,7 +2,61 @@ import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
|
|
|
2
2
|
import type { ErrorSpec } from '../../../shared/types/ErrorSpec.ts'
|
|
3
3
|
import type { RemoteFunction } from '../../../shared/types/RemoteFunction.ts'
|
|
4
4
|
import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
|
|
5
|
-
import type {
|
|
5
|
+
import type { TypedError } from './TypedError.ts'
|
|
6
|
+
import type { TypedResponse } from './TypedResponse.ts'
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
The success body carried by a handler's return type `R`. Error branches
|
|
10
|
+
(`TypedError`, checked first since they're also Responses) drop to `never` and
|
|
11
|
+
union away, so `Return` is the body of the success `TypedResponse` members alone
|
|
12
|
+
— an untagged `Response` falls back to `unknown`, matching hand-built responses.
|
|
13
|
+
*/
|
|
14
|
+
type SuccessBody<R> =
|
|
15
|
+
R extends TypedError<string, ErrorSpec[string]>
|
|
16
|
+
? never
|
|
17
|
+
: R extends TypedResponse<infer Body>
|
|
18
|
+
? Body
|
|
19
|
+
: unknown
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
The error spec a handler's return type `R` declares — rebuilt name→entry from the
|
|
23
|
+
`TypedError` brands among its branches (distributes over the union; no error
|
|
24
|
+
branches → `{}`). This is what gives `rpc.isError` its typed surface with no
|
|
25
|
+
`errors:` option: the errors a handler RETURNS are the errors it can raise.
|
|
26
|
+
*/
|
|
27
|
+
type ErrorBrand<R> =
|
|
28
|
+
R extends TypedError<infer Name, infer Entry> ? { name: Name; entry: Entry } : never
|
|
29
|
+
type InferredErrors<R> = { [Brand in ErrorBrand<R> as Brand['name']]: Brand['entry'] }
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
The handler every bare (schemaless) overload accepts: any args in, a Response (or a
|
|
33
|
+
Promise of one) out. Collapsing the old `<Args, R>` pair into this single inferred
|
|
34
|
+
function type is deliberate. TypeScript has no partial type-argument inference, so
|
|
35
|
+
`GET<Args>(fn)` used to drop `R` to its `Response` default and silently erase the body
|
|
36
|
+
to `unknown`. With one generic constrained to `RpcFn`, that same call is instead a loud
|
|
37
|
+
`does not satisfy the constraint 'RpcFn'`, and `Args`/`Return`/`Errors` all read
|
|
38
|
+
structurally off the handler — args from its parameter, body + errors from its return —
|
|
39
|
+
which is where a normal TS function declares them anyway (no mainstream rpc framework
|
|
40
|
+
parameterises the call with generics). `any` is load-bearing: `never`/`unknown` in the
|
|
41
|
+
parameter position break return inference by contextually widening the handler's body.
|
|
42
|
+
*/
|
|
43
|
+
type RpcFn = (args: any) => Response | Promise<Response>
|
|
44
|
+
|
|
45
|
+
/* The handler's declared args: its first parameter, or `undefined` for a nullary handler. */
|
|
46
|
+
type RpcArgs<F extends RpcFn> = Parameters<F> extends [infer Args, ...unknown[]] ? Args : undefined
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
The RemoteFunction a bare overload produces from handler `F`. `SuccessBody`/`InferredErrors`
|
|
50
|
+
run over `Awaited<ReturnType<F>>` INLINE (not via an intermediate `extends RpcFn`-constrained
|
|
51
|
+
alias) — such an alias resolves `ReturnType` against the constraint bound and degrades the
|
|
52
|
+
body to `any`. `Durable` threads the outbox bit through for the durable overloads.
|
|
53
|
+
*/
|
|
54
|
+
type RpcOf<F extends RpcFn, Durable extends boolean = false> = RemoteFunction<
|
|
55
|
+
RpcArgs<F>,
|
|
56
|
+
SuccessBody<Awaited<ReturnType<F>>>,
|
|
57
|
+
InferredErrors<Awaited<ReturnType<F>>>,
|
|
58
|
+
Durable
|
|
59
|
+
>
|
|
6
60
|
|
|
7
61
|
/*
|
|
8
62
|
Options every rpc overload accepts: the OpenAPI 200 `outputSchema`, the
|
|
@@ -34,14 +88,17 @@ type MutatingRpcOpts = RpcBaseOpts & {
|
|
|
34
88
|
}
|
|
35
89
|
|
|
36
90
|
/*
|
|
37
|
-
Shared signature for every rpc helper (GET / POST / …).
|
|
91
|
+
Shared signature for every rpc helper (GET / POST / …). The handler's return
|
|
92
|
+
type is inferred whole (`Awaited<ReturnType<F>>`), then split: `SuccessBody`
|
|
93
|
+
becomes the caller's `Return`, `InferredErrors` becomes the rpc's `Errors`
|
|
94
|
+
(driving `isError`). Typed errors are raised by returning an
|
|
95
|
+
`error.typed(name, status, schema?)` constructor — there is no `errors:` opt.
|
|
96
|
+
Four overloads by argument source:
|
|
38
97
|
|
|
39
98
|
- `Rpc(fn, { inputSchema, outputSchema?, clients? })` — `Args` infers
|
|
40
99
|
from `InferInput<InputSchema>`, the handler receives
|
|
41
|
-
`InferOutput<InputSchema>`.
|
|
42
|
-
|
|
43
|
-
`opts.inputSchema`. `outputSchema` is an optional Standard Schema for
|
|
44
|
-
the success body — it feeds the OpenAPI 200 response and the MCP tool
|
|
100
|
+
`InferOutput<InputSchema>`. `outputSchema` is an optional Standard Schema
|
|
101
|
+
for the success body — it feeds the OpenAPI 200 response and the MCP tool
|
|
45
102
|
`outputSchema`. JSON Schema is projected from each schema's own
|
|
46
103
|
`toJSONSchema()` (wrap with withJsonSchema if the library lacks one).
|
|
47
104
|
`clients` controls which surfaces (browser / mcp / cli) expose this rpc.
|
|
@@ -57,9 +114,13 @@ Shared signature for every rpc helper (GET / POST / …). Four overloads:
|
|
|
57
114
|
just abandoned.
|
|
58
115
|
- `Rpc(fn, { clients })` — schemaless but with explicit client
|
|
59
116
|
targeting (e.g. server-internal RPC with `clients: { browser: false }`).
|
|
60
|
-
- `Rpc(fn)` — bare handler
|
|
61
|
-
|
|
62
|
-
`
|
|
117
|
+
- `Rpc(fn)` — bare handler, a single `F extends RpcFn` generic. Everything
|
|
118
|
+
is read off the handler: `Args` from its parameter (annotate it —
|
|
119
|
+
`POST((a: { id: string }) => …)` — or leave it nullary for `undefined`),
|
|
120
|
+
`Return` from the `TypedResponse<T>` brand on
|
|
121
|
+
`json`/`error`/`redirect`/`jsonl`/`sse`, `Errors` from any
|
|
122
|
+
`error.typed(...)` branches. You never pass `<Args, Return>` generics; a
|
|
123
|
+
stray one is a loud constraint error, not a silent `unknown` body.
|
|
63
124
|
*/
|
|
64
125
|
type RpcHelperOf<Opts> = {
|
|
65
126
|
/*
|
|
@@ -72,47 +133,76 @@ type RpcHelperOf<Opts> = {
|
|
|
72
133
|
jsonSchemaForSchema) — so only inputSchema feeds MCP/CLI/OpenAPI.
|
|
73
134
|
*/
|
|
74
135
|
<
|
|
75
|
-
|
|
136
|
+
R extends Response,
|
|
76
137
|
InputSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
77
138
|
FilesSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
78
|
-
Errors extends ErrorSpec = Record<string, never>,
|
|
79
139
|
>(
|
|
80
|
-
fn:
|
|
81
|
-
StandardSchemaV1.InferOutput<InputSchema> &
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
>,
|
|
140
|
+
fn: (
|
|
141
|
+
args: StandardSchemaV1.InferOutput<InputSchema> &
|
|
142
|
+
StandardSchemaV1.InferOutput<FilesSchema>,
|
|
143
|
+
) => R | Promise<R>,
|
|
85
144
|
opts: Opts & {
|
|
86
145
|
inputSchema: InputSchema
|
|
87
146
|
filesSchema: FilesSchema
|
|
88
|
-
errors?: Errors
|
|
89
147
|
},
|
|
90
|
-
): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>,
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
>(
|
|
96
|
-
|
|
97
|
-
opts: Opts & { inputSchema: InputSchema; errors?: Errors },
|
|
98
|
-
): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return, Errors>
|
|
99
|
-
<Args = undefined, Return = unknown, Errors extends ErrorSpec = Record<never, never>>(
|
|
100
|
-
fn: RemoteHandler<Args, Return, Errors>,
|
|
101
|
-
opts: Opts & { errors?: Errors },
|
|
102
|
-
): RemoteFunction<Args, Return, Errors>
|
|
103
|
-
<Args = undefined, Return = unknown>(
|
|
104
|
-
fn: RemoteHandler<Args, Return>,
|
|
105
|
-
): RemoteFunction<Args, Return>
|
|
148
|
+
): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, SuccessBody<R>, InferredErrors<R>>
|
|
149
|
+
<R extends Response, InputSchema extends StandardSchemaV1 = StandardSchemaV1>(
|
|
150
|
+
fn: (args: StandardSchemaV1.InferOutput<InputSchema>) => R | Promise<R>,
|
|
151
|
+
opts: Opts & { inputSchema: InputSchema },
|
|
152
|
+
): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, SuccessBody<R>, InferredErrors<R>>
|
|
153
|
+
<F extends RpcFn>(fn: F, opts: Opts): RpcOf<F>
|
|
154
|
+
<F extends RpcFn>(fn: F): RpcOf<F>
|
|
106
155
|
}
|
|
107
156
|
|
|
108
157
|
/* The read helpers (GET/HEAD): no `outbox` — a read has nothing to durably deliver. */
|
|
109
158
|
export type RpcHelper = RpcHelperOf<RpcBaseOpts>
|
|
110
159
|
|
|
160
|
+
/*
|
|
161
|
+
Durable-call overloads: an `outbox: true` opt returns a RemoteFunction whose `Durable` bit
|
|
162
|
+
is set, so `rpc.outbox` is the queue face rather than optionally-undefined. Mirrors the
|
|
163
|
+
opts-bearing base overloads (multipart-upload, schema'd, schemaless); the bare `Rpc(fn)`
|
|
164
|
+
form has no opts to carry `outbox`, so it stays non-durable. Intersected AHEAD of the base
|
|
165
|
+
set in MutatingRpcHelper so an `outbox: true` literal resolves here first.
|
|
166
|
+
*/
|
|
167
|
+
type DurableMutatingRpcHelper = {
|
|
168
|
+
<
|
|
169
|
+
R extends Response,
|
|
170
|
+
InputSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
171
|
+
FilesSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
172
|
+
>(
|
|
173
|
+
fn: (
|
|
174
|
+
args: StandardSchemaV1.InferOutput<InputSchema> &
|
|
175
|
+
StandardSchemaV1.InferOutput<FilesSchema>,
|
|
176
|
+
) => R | Promise<R>,
|
|
177
|
+
opts: MutatingRpcOpts & {
|
|
178
|
+
inputSchema: InputSchema
|
|
179
|
+
filesSchema: FilesSchema
|
|
180
|
+
outbox: true
|
|
181
|
+
},
|
|
182
|
+
): RemoteFunction<
|
|
183
|
+
StandardSchemaV1.InferInput<InputSchema>,
|
|
184
|
+
SuccessBody<R>,
|
|
185
|
+
InferredErrors<R>,
|
|
186
|
+
true
|
|
187
|
+
>
|
|
188
|
+
<R extends Response, InputSchema extends StandardSchemaV1 = StandardSchemaV1>(
|
|
189
|
+
fn: (args: StandardSchemaV1.InferOutput<InputSchema>) => R | Promise<R>,
|
|
190
|
+
opts: MutatingRpcOpts & { inputSchema: InputSchema; outbox: true },
|
|
191
|
+
): RemoteFunction<
|
|
192
|
+
StandardSchemaV1.InferInput<InputSchema>,
|
|
193
|
+
SuccessBody<R>,
|
|
194
|
+
InferredErrors<R>,
|
|
195
|
+
true
|
|
196
|
+
>
|
|
197
|
+
<F extends RpcFn>(fn: F, opts: MutatingRpcOpts & { outbox: true }): RpcOf<F, true>
|
|
198
|
+
}
|
|
199
|
+
|
|
111
200
|
/*
|
|
112
201
|
The mutating helpers (POST/PUT/PATCH/DELETE). A durable (`outbox`) call is a normal
|
|
113
202
|
RemoteFunction — it throws exactly like a non-durable one and only parks the request as a
|
|
114
203
|
side-effect on an unreachable server — so there is no separate return shape; `outbox` rides
|
|
115
204
|
MutatingRpcOpts and `rpc.outbox` exposes the queue. The distinct opts base is what makes
|
|
116
|
-
`outbox` legal here and a compile error on the read helpers
|
|
205
|
+
`outbox` legal here and a compile error on the read helpers; the durable overloads then set
|
|
206
|
+
the return type's `Durable` bit so `rpc.outbox` is present without an optional chain.
|
|
117
207
|
*/
|
|
118
|
-
export type MutatingRpcHelper = RpcHelperOf<MutatingRpcOpts>
|
|
208
|
+
export type MutatingRpcHelper = DurableMutatingRpcHelper & RpcHelperOf<MutatingRpcOpts>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ErrorSpec } from '../../../shared/types/ErrorSpec.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A `Response` branded with the typed error a handler RETURNED — the name it
|
|
5
|
+
travels as (`$abideError`) plus its spec entry (`{ status, data? }`). The rpc
|
|
6
|
+
helper reads this brand off the handler's inferred return union to build the
|
|
7
|
+
function's `Errors` surface, so `rpc.isError(e, 'name')` narrows `.kind` / `.data`
|
|
8
|
+
with no `errors:` option to declare.
|
|
9
|
+
|
|
10
|
+
Unlike `TypedResponse`'s `__body?` (optional, so a bare `new Response()` stays
|
|
11
|
+
assignable), the brand here is REQUIRED: it's the discriminant that tells the
|
|
12
|
+
helper an error branch from a success branch (the success body is extracted from
|
|
13
|
+
the non-error members, the error spec from these). The runtime value is a plain
|
|
14
|
+
serialized Response cast to this type — the brand is phantom, never assigned.
|
|
15
|
+
*/
|
|
16
|
+
export type TypedError<Name extends string, Entry extends ErrorSpec[string]> = Response & {
|
|
17
|
+
readonly __abideError: { name: Name; entry: Entry }
|
|
18
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
|
|
2
2
|
import type { ValidationErrorData } from '../../shared/types/ValidationErrorData.ts'
|
|
3
|
-
import {
|
|
3
|
+
import { typedErrorResponse } from '../runtime/typedErrorResponse.ts'
|
|
4
4
|
import { fieldErrorsFromIssues } from './fieldErrorsFromIssues.ts'
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
7
|
The framework-reserved `validation` typed error a 422 carries: the raw Standard
|
|
8
8
|
Schema `issues` plus the form-friendly field → first-message map. Routed through
|
|
9
|
-
the
|
|
9
|
+
the shared `typedErrorResponse` serializer so it emits the same
|
|
10
10
|
`{ $abideError, data }` body every typed error uses, and so the 422's reason
|
|
11
11
|
phrase reaches `HttpError.statusText`. The client parses it back onto
|
|
12
12
|
`HttpError.kind = 'validation'` / `.data: ValidationErrorData`.
|
|
13
13
|
*/
|
|
14
14
|
export function validationError(issues: readonly StandardSchemaV1.Issue[]): Response {
|
|
15
15
|
const data: ValidationErrorData = { issues, fields: fieldErrorsFromIssues(issues) }
|
|
16
|
-
return
|
|
16
|
+
return typedErrorResponse('validation', 422, data)
|
|
17
17
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Standard reason phrases for the statuses abide sets explicitly. Bun's `Response`
|
|
3
|
+
does not populate `statusText` from the status code, so there's no platform table
|
|
4
|
+
to read. Unlisted codes fall back to `HTTP <status>` at the call site.
|
|
5
|
+
*/
|
|
6
|
+
export const STATUS_TEXT: Record<number, string> = {
|
|
7
|
+
400: 'Bad Request',
|
|
8
|
+
401: 'Unauthorized',
|
|
9
|
+
403: 'Forbidden',
|
|
10
|
+
404: 'Not Found',
|
|
11
|
+
405: 'Method Not Allowed',
|
|
12
|
+
409: 'Conflict',
|
|
13
|
+
410: 'Gone',
|
|
14
|
+
413: 'Content Too Large',
|
|
15
|
+
422: 'Unprocessable Content',
|
|
16
|
+
429: 'Too Many Requests',
|
|
17
|
+
500: 'Internal Server Error',
|
|
18
|
+
501: 'Not Implemented',
|
|
19
|
+
502: 'Bad Gateway',
|
|
20
|
+
503: 'Service Unavailable',
|
|
21
|
+
504: 'Gateway Timeout',
|
|
22
|
+
}
|
|
@@ -17,6 +17,7 @@ import { isDebugNegated } from '../../shared/isDebugNegated.ts'
|
|
|
17
17
|
import { logClosingRecord } from '../../shared/logClosingRecord.ts'
|
|
18
18
|
import { OFFLINE_HEADER } from '../../shared/OFFLINE_HEADER.ts'
|
|
19
19
|
import { parseBoundedEnvInt } from '../../shared/parseBoundedEnvInt.ts'
|
|
20
|
+
import { responseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
20
21
|
import { SOCKETS_PATH } from '../../shared/SOCKETS_PATH.ts'
|
|
21
22
|
import { setAppName } from '../../shared/setAppName.ts'
|
|
22
23
|
import { setBaseResolver } from '../../shared/setBaseResolver.ts'
|
|
@@ -49,6 +50,7 @@ import { devHotModuleResponse } from './devHotModuleResponse.ts'
|
|
|
49
50
|
import { devReloadResponse } from './devReloadResponse.ts'
|
|
50
51
|
import { disableIdleTimeoutForStream } from './disableIdleTimeoutForStream.ts'
|
|
51
52
|
import { gzipResponse } from './gzipResponse.ts'
|
|
53
|
+
import { installAmbientScopeStore } from './installAmbientScopeStore.ts'
|
|
52
54
|
import { internalErrorResponse } from './internalErrorResponse.ts'
|
|
53
55
|
import { listenOnOpenPort } from './listenOnOpenPort.ts'
|
|
54
56
|
import { logExposedSurfaces } from './logExposedSurfaces.ts'
|
|
@@ -158,6 +160,12 @@ export async function createServer({
|
|
|
158
160
|
HTTP test harness gets the same behaviour as a real boot. elapsedMs is
|
|
159
161
|
computed at read time so every log line carries a current value.
|
|
160
162
|
*/
|
|
163
|
+
/*
|
|
164
|
+
Key the ambient lexical scope off the per-request ALS store, so concurrent
|
|
165
|
+
async SSR renders don't clobber one shared module global across the inline
|
|
166
|
+
`await`s they suspend on (see installAmbientScopeStore / CURRENT_SCOPE).
|
|
167
|
+
*/
|
|
168
|
+
installAmbientScopeStore()
|
|
161
169
|
setRequestScopeResolver(() => {
|
|
162
170
|
const store = requestContext.getStore()
|
|
163
171
|
if (!store) {
|
|
@@ -242,8 +250,11 @@ export async function createServer({
|
|
|
242
250
|
isn't installed. Resolved at boot so the fetch route below can branch on it.
|
|
243
251
|
*/
|
|
244
252
|
const inspectorHandler = await maybeMountInspector({ name: appName, version: appVersion })
|
|
245
|
-
/* Built on first request, then reused — the rpc registry is frozen after load.
|
|
246
|
-
|
|
253
|
+
/* Built on first request, then reused — the rpc registry is frozen after load.
|
|
254
|
+
Memoised as a promise so two concurrent cold requests share one build instead
|
|
255
|
+
of both building (the second otherwise clobbering the first). A rejected build
|
|
256
|
+
clears the memo so the next request retries rather than caching the failure. */
|
|
257
|
+
let openApiSpec: Promise<ReturnType<typeof buildOpenApiSpec>> | undefined
|
|
247
258
|
const cliCwd = process.cwd()
|
|
248
259
|
|
|
249
260
|
/* Request closing records are on by default — DEBUG=-abide is the off switch (negation, like the abide channel itself). */
|
|
@@ -362,12 +373,19 @@ export async function createServer({
|
|
|
362
373
|
const response = app?.handle
|
|
363
374
|
? await app.handle(req, (next) => handler(next, pathParams, store))
|
|
364
375
|
: await handler(req, pathParams, store)
|
|
376
|
+
/* Classify the body once (S2) and thread it into both downstream
|
|
377
|
+
steps — gzip + the closing-record stream monitor — instead of
|
|
378
|
+
each re-deriving from the Content-Type. */
|
|
379
|
+
const kind = responseBodyKind(response)
|
|
380
|
+
store.responseStreaming = kind === 'streaming'
|
|
381
|
+
// Streaming bodies (sse/jsonl, socket tail) opt out of the idle timeout.
|
|
382
|
+
if (kind === 'streaming') {
|
|
383
|
+
server.timeout(req, 0)
|
|
384
|
+
}
|
|
365
385
|
/* Gzip compressible dynamic bodies (SSR HTML, rpc/json, 404) when the
|
|
366
386
|
client accepts it; streaming frame protocols and static assets are
|
|
367
387
|
passed through untouched (see gzipResponse). */
|
|
368
|
-
|
|
369
|
-
// Streaming bodies (sse/jsonl, socket tail) opt out of the idle timeout.
|
|
370
|
-
return disableIdleTimeoutForStream(server, req, encoded)
|
|
388
|
+
return gzipResponse(req, response, kind)
|
|
371
389
|
})
|
|
372
390
|
}
|
|
373
391
|
|
|
@@ -583,14 +601,20 @@ export async function createServer({
|
|
|
583
601
|
req,
|
|
584
602
|
{},
|
|
585
603
|
async () => {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
604
|
+
openApiSpec ??= ensureRegistriesLoaded()
|
|
605
|
+
.then(() =>
|
|
606
|
+
buildOpenApiSpec({
|
|
607
|
+
title: appName,
|
|
608
|
+
version: appVersion,
|
|
609
|
+
}),
|
|
610
|
+
)
|
|
611
|
+
.catch((error) => {
|
|
612
|
+
// Don't cache a failed build — clear the memo so a
|
|
613
|
+
// later request retries instead of 500-ing forever.
|
|
614
|
+
openApiSpec = undefined
|
|
615
|
+
throw error
|
|
591
616
|
})
|
|
592
|
-
|
|
593
|
-
return Response.json(openApiSpec, {
|
|
617
|
+
return Response.json(await openApiSpec, {
|
|
594
618
|
headers: { 'Cache-Control': NO_STORE },
|
|
595
619
|
})
|
|
596
620
|
},
|
|
@@ -26,7 +26,7 @@ export async function devHotModuleResponse(moduleId: string): Promise<Response>
|
|
|
26
26
|
if (source === undefined) {
|
|
27
27
|
return new Response('not found', { status: 404 })
|
|
28
28
|
}
|
|
29
|
-
const code = compileModule(source, {
|
|
29
|
+
const { code } = compileModule(source, {
|
|
30
30
|
isLayout: moduleId.endsWith('layout.abide'),
|
|
31
31
|
moduleId,
|
|
32
32
|
hot: true,
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { ResponseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
2
|
+
import { responseBodyKind } from '../../shared/responseBodyKind.ts'
|
|
3
3
|
import { acceptsGzip } from './acceptsGzip.ts'
|
|
4
4
|
import { flushingGzipStream } from './flushingGzipStream.ts'
|
|
5
5
|
import { STREAMED_HTML_HEADER } from './STREAMED_HTML_HEADER.ts'
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
Compressible Content-Types — text and structured-text payloads. Binary or
|
|
9
|
-
already-compressed bodies (images, fonts, archives, zstd/gzip blobs) gain
|
|
10
|
-
nothing from a second pass and only burn CPU.
|
|
11
|
-
*/
|
|
12
|
-
const COMPRESSIBLE_TYPE =
|
|
13
|
-
/^(?:text\/|application\/(?:json|javascript|xml|[\w.-]+\+(?:json|xml))|image\/svg)/
|
|
14
|
-
|
|
15
7
|
/*
|
|
16
8
|
Gzips a dynamic response (SSR HTML, rpc/json replies, the plain 404) when the
|
|
17
9
|
client accepts it. Static assets never reach here — they already carry a
|
|
@@ -23,6 +15,12 @@ expose a string body's length before send, and measuring would mean buffering th
|
|
|
23
15
|
body — the framing overhead on the rare tiny body is negligible against
|
|
24
16
|
compressing every page and rpc payload.
|
|
25
17
|
|
|
18
|
+
`kind` is the body class the dispatch pipeline already computed (S2: classify
|
|
19
|
+
once, thread it in) — only `compressible` bodies gzip; `streaming` and `opaque`
|
|
20
|
+
pass through. Standalone callers (the health probe) omit it and it's derived
|
|
21
|
+
here. The streamed-HTML marker is handled independent of `kind` since it's
|
|
22
|
+
stripped even on a skip path.
|
|
23
|
+
|
|
26
24
|
Buffered bodies take the web CompressionStream (best ratio, one flush at close).
|
|
27
25
|
The streamed SSR document self-marks (STREAMED_HTML_HEADER) and takes a
|
|
28
26
|
per-chunk-flushing gzip instead: the plain CompressionStream buffers the head
|
|
@@ -30,7 +28,7 @@ until its deflate window fills, which defeats streaming (the browser can't
|
|
|
30
28
|
preload-scan the head or paint the pending shell until the stream nearly closes).
|
|
31
29
|
The marker is stripped so it never reaches the client.
|
|
32
30
|
*/
|
|
33
|
-
export function gzipResponse(req: Request, response: Response): Response {
|
|
31
|
+
export function gzipResponse(req: Request, response: Response, kind?: ResponseBodyKind): Response {
|
|
34
32
|
const streamedHtml = response.headers.has(STREAMED_HTML_HEADER)
|
|
35
33
|
if (streamedHtml) {
|
|
36
34
|
response.headers.delete(STREAMED_HTML_HEADER)
|
|
@@ -38,11 +36,11 @@ export function gzipResponse(req: Request, response: Response): Response {
|
|
|
38
36
|
if (!response.body || response.headers.has('Content-Encoding')) {
|
|
39
37
|
return response
|
|
40
38
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
39
|
+
/* A streamed-HTML document classifies as `compressible` (text/html) but the
|
|
40
|
+
marker was already stripped above, so re-derive after the strip when the
|
|
41
|
+
pipeline didn't hand a kind in. */
|
|
42
|
+
const bodyKind = kind ?? responseBodyKind(response)
|
|
43
|
+
if (!acceptsGzip(req) || bodyKind !== 'compressible') {
|
|
46
44
|
return response
|
|
47
45
|
}
|
|
48
46
|
const headers = new Headers(response.headers)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ambientScopeBacking } from '../../ui/runtime/ambientScopeBacking.ts'
|
|
2
|
+
import type { Scope } from '../../ui/types/Scope.ts'
|
|
3
|
+
import { requestContext } from './requestContext.ts'
|
|
4
|
+
|
|
5
|
+
/* Ambient scope when no request is in flight (server boot, a standalone render). */
|
|
6
|
+
let outsideRequest: Scope | undefined
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
Swaps the ambient-scope backing to one keyed off the per-request AsyncLocalStorage
|
|
10
|
+
store, so `CURRENT_SCOPE.current` isolates per request instead of living in one
|
|
11
|
+
module global (see CURRENT_SCOPE). SSR is partly async — a render `await`s inline
|
|
12
|
+
between its `enterScope`/`exitScope` bracket — and the async context propagates the
|
|
13
|
+
right store across those awaits, so a resumed render reads ITS OWN scope even while
|
|
14
|
+
another request renders concurrently. Called once at server boot (createServer),
|
|
15
|
+
alongside the request-scope resolver install. Reads/writes outside any request fall
|
|
16
|
+
back to a module variable (unchanged behaviour for standalone renders).
|
|
17
|
+
*/
|
|
18
|
+
export function installAmbientScopeStore(): void {
|
|
19
|
+
ambientScopeBacking.active = {
|
|
20
|
+
get: () => {
|
|
21
|
+
const store = requestContext.getStore()
|
|
22
|
+
return store ? store.currentScope : outsideRequest
|
|
23
|
+
},
|
|
24
|
+
set: (value) => {
|
|
25
|
+
const store = requestContext.getStore()
|
|
26
|
+
if (store) {
|
|
27
|
+
store.currentScope = value
|
|
28
|
+
} else {
|
|
29
|
+
outsideRequest = value
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -120,7 +120,10 @@ export function runWithRequestScope(
|
|
|
120
120
|
})
|
|
121
121
|
})
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
/* Reuse the dispatch pipeline's single S2 classification when it stashed
|
|
124
|
+
one; callers that don't (mcp, the fetch fallback, tests) classify here. */
|
|
125
|
+
const streaming = store.responseStreaming ?? isStreamingResponse(response)
|
|
126
|
+
if (response.body && streaming) {
|
|
124
127
|
/*
|
|
125
128
|
A hand-pumped wrap rather than a TransformStream: streams typically
|
|
126
129
|
end by client disconnect (an SSE tab closing), which cancels the
|
|
@@ -74,6 +74,8 @@ export async function snapshotEntryFromCache(
|
|
|
74
74
|
statusText: response.statusText,
|
|
75
75
|
headers: Array.from(response.headers.entries()),
|
|
76
76
|
body,
|
|
77
|
+
/* Deferred by the SSR resume path → seed the shipped body lazily (no hydration decode). */
|
|
78
|
+
lazy: entry.deferred === true ? true : undefined,
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import type { TypedResponse } from '../rpc/types/TypedResponse.ts'
|
|
3
|
+
import { STATUS_TEXT } from './STATUS_TEXT.ts'
|
|
4
|
+
import { withResponseDefaults } from './withResponseDefaults.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Serializes a typed error as a `{ $abideError, data }` JSON body at `status`, with
|
|
8
|
+
the status reason phrase as statusText so it reaches `HttpError.statusText` on the
|
|
9
|
+
client (which parses the body back onto `HttpError.kind` / `.data`). `data` of
|
|
10
|
+
`undefined` drops the key (nullary errors). The single serializer shared by the
|
|
11
|
+
`error.typed(...)` constructors and the framework-reserved `validation` error.
|
|
12
|
+
*/
|
|
13
|
+
export function typedErrorResponse(
|
|
14
|
+
name: string,
|
|
15
|
+
status: number,
|
|
16
|
+
data: unknown,
|
|
17
|
+
): TypedResponse<never> {
|
|
18
|
+
return new Response(
|
|
19
|
+
JSON.stringify({ $abideError: name, data }),
|
|
20
|
+
withResponseDefaults(
|
|
21
|
+
{ statusText: STATUS_TEXT[status] ?? `HTTP ${status}` },
|
|
22
|
+
{ 'Content-Type': 'application/json', 'Cache-Control': NO_STORE },
|
|
23
|
+
status,
|
|
24
|
+
),
|
|
25
|
+
) as TypedResponse<never>
|
|
26
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CacheStore } from '../../../shared/types/CacheStore.ts'
|
|
2
2
|
import type { TraceContext } from '../../../shared/types/TraceContext.ts'
|
|
3
|
+
import type { Scope } from '../../../ui/types/Scope.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
Per-request state propagated through AsyncLocalStorage. Every field is
|
|
@@ -52,4 +53,28 @@ export type RequestStore = {
|
|
|
52
53
|
carried no file parts.
|
|
53
54
|
*/
|
|
54
55
|
files?: Record<string, File[]>
|
|
56
|
+
/*
|
|
57
|
+
The allowlisted headers forwarded onto in-process rpc Requests during this
|
|
58
|
+
request's SSR pass — derived once from the inbound headers (which don't change
|
|
59
|
+
within the scope) and reused by every defineRpc call. Each call clones it,
|
|
60
|
+
since buildRpcRequest mutates the Headers (content-type/ref-json) before
|
|
61
|
+
constructing the Request. Undefined until the first in-process rpc.
|
|
62
|
+
*/
|
|
63
|
+
forwardedHeaders?: Headers
|
|
64
|
+
/*
|
|
65
|
+
The body class of the response the dispatch pipeline produced, stashed so
|
|
66
|
+
runWithRequestScope's closing-record stream monitor reuses the single S2
|
|
67
|
+
classification instead of re-deriving from the Content-Type. Undefined for
|
|
68
|
+
callers that don't classify (mcp, the fetch fallback, tests) — those fall
|
|
69
|
+
back to classifying the response themselves.
|
|
70
|
+
*/
|
|
71
|
+
responseStreaming?: boolean
|
|
72
|
+
/*
|
|
73
|
+
The request's ambient lexical scope during its SSR pass — the backing for
|
|
74
|
+
`CURRENT_SCOPE.current` under the server's ALS-backed holder
|
|
75
|
+
(installAmbientScopeStore). Keeping it per-request isolates the ambient across
|
|
76
|
+
the inline `await`s a render suspends on, so concurrent renders don't clobber
|
|
77
|
+
one shared module global. Undefined until the render enters its first scope.
|
|
78
|
+
*/
|
|
79
|
+
currentScope?: Scope
|
|
55
80
|
}
|
|
@@ -17,8 +17,13 @@ export function withResponseDefaults(
|
|
|
17
17
|
status?: number,
|
|
18
18
|
): ResponseInit {
|
|
19
19
|
const headers = new Headers(defaultHeaders)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/* The dominant case carries no overrides — skip the second Headers
|
|
21
|
+
allocation + per-key copy and ship the defaults alone. Only merge when
|
|
22
|
+
the caller actually supplied headers. */
|
|
23
|
+
if (init?.headers !== undefined) {
|
|
24
|
+
new Headers(init.headers).forEach((value, key) => {
|
|
25
|
+
headers.set(key, value)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
23
28
|
return { ...init, headers, ...(status !== undefined && { status }) }
|
|
24
29
|
}
|
|
@@ -119,7 +119,11 @@ export function defineSocket<T>(name: string, opts: SocketOptions = {}): Socket<
|
|
|
119
119
|
cachedServer = getActiveServer()
|
|
120
120
|
}
|
|
121
121
|
const server = cachedServer
|
|
122
|
-
|
|
122
|
+
/* Remote fan-out is the only consumer of the encoded frame: in-process iterators
|
|
123
|
+
already received the live object via notify() above. Skip the full message-graph
|
|
124
|
+
walk (encodeRefJson) + native publish when zero ws clients are subscribed to the
|
|
125
|
+
topic — subscriberCount is a cheap C-side check, encodeRefJson is not. */
|
|
126
|
+
if (server && server.subscriberCount(topic) > 0) {
|
|
123
127
|
/* Typed against the shared wire contract so a `SocketServerFrame` change can't
|
|
124
128
|
silently drift from this construction site (the dispatcher's `send` is already
|
|
125
129
|
typed; this was the last `msg` frame built through an unchecked JSON.stringify). */
|
package/src/lib/server/sse.ts
CHANGED
|
@@ -5,7 +5,7 @@ Server-Sent Events (text/event-stream) — each frame becomes one
|
|
|
5
5
|
generator over plain HTTP so EventSource (or `tail(fn.stream(args))`
|
|
6
6
|
on the client) can consume it frame-by-frame.
|
|
7
7
|
|
|
8
|
-
export const orderFeed = GET
|
|
8
|
+
export const orderFeed = GET((args: Args) =>
|
|
9
9
|
sse(async function* () {
|
|
10
10
|
for await (const order of db.watchOrders(args)) yield order
|
|
11
11
|
}())
|
|
@@ -9,7 +9,7 @@ export class HttpError extends Error {
|
|
|
9
9
|
readonly status: number
|
|
10
10
|
readonly statusText: string
|
|
11
11
|
readonly response: Response
|
|
12
|
-
/* Set when the handler returned a typed error (`error
|
|
12
|
+
/* Set when the handler returned a typed error (`error.typed(...)` constructor) or a
|
|
13
13
|
validation 422: `kind` is the declared error name (or 'validation'), `data` the
|
|
14
14
|
payload it carried — parsed off the `{ $abideError, data }` body by decodeResponse.
|
|
15
15
|
`data` is typed `unknown` (a throw can't carry the rpc's per-kind type to the
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The `RpcArgs<Fn>` type alias both rpc d.ts writers emit verbatim: it lifts the
|
|
3
|
+
args type out of a RemoteFunction, dropping the FormData upload variant so a
|
|
4
|
+
url()/test-client call types against the rpc's plain args.
|
|
5
|
+
*/
|
|
6
|
+
export const RPC_ARGS_TYPE =
|
|
7
|
+
'type RpcArgs<Fn> = Fn extends (args: infer Args) => unknown ? Exclude<Args, FormData> : never'
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createCacheStore } from './createCacheStore.ts'
|
|
1
|
+
import { cacheStoreResolver } from './cacheStoreResolver.ts'
|
|
3
2
|
import type { CacheStore } from './types/CacheStore.ts'
|
|
4
3
|
|
|
5
4
|
/*
|
|
6
|
-
Resolves the active CacheStore
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
a single fallback store is created lazily so isolated tests still work.
|
|
5
|
+
Resolves the active CacheStore: the registered resolver's store, or a single
|
|
6
|
+
lazily-created fallback when none is registered (so isolated tests work). The
|
|
7
|
+
fallback creator guarantees a value, hence the non-null assertion.
|
|
10
8
|
*/
|
|
11
9
|
export function activeCacheStore(): CacheStore {
|
|
12
|
-
|
|
13
|
-
if (fromResolver) {
|
|
14
|
-
return fromResolver
|
|
15
|
-
}
|
|
16
|
-
if (!cacheStoreSlot.fallback) {
|
|
17
|
-
cacheStoreSlot.fallback = createCacheStore()
|
|
18
|
-
}
|
|
19
|
-
return cacheStoreSlot.fallback
|
|
10
|
+
return cacheStoreResolver.get()!
|
|
20
11
|
}
|