@timber-js/app 0.2.0-alpha.163 → 0.2.0-alpha.165
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/dist/_chunks/{actions-cjklt63G.js → actions-CSDD6x7U.js} +2 -2
- package/dist/_chunks/{actions-cjklt63G.js.map → actions-CSDD6x7U.js.map} +1 -1
- package/dist/_chunks/{cache-api-CzYUlgXA.js → cache-api-eb1gydM7.js} +41 -13
- package/dist/_chunks/cache-api-eb1gydM7.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-NfLbLnDw.js → cli-schema-sync-mGfRbjh2.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-NfLbLnDw.js.map → cli-schema-sync-mGfRbjh2.js.map} +1 -1
- package/dist/_chunks/{plugin-context-DeAxFRMq.js → plugin-context-BnaiU_cF.js} +37 -2
- package/dist/_chunks/plugin-context-BnaiU_cF.js.map +1 -0
- package/dist/_chunks/{walkers-9mz9T7mb.js → walkers-BL3MCMgO.js} +2 -2
- package/dist/_chunks/{walkers-9mz9T7mb.js.map → walkers-BL3MCMgO.js.map} +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/nitro.d.ts +40 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +133 -73
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/index.d.ts +3 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/redis-handler.d.ts +1 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/tag-aware-handler.d.ts +1 -0
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/slot-context.d.ts +29 -0
- package/dist/client/slot-context.d.ts.map +1 -0
- package/dist/client/slot-outlet.d.ts +16 -0
- package/dist/client/slot-outlet.d.ts.map +1 -0
- package/dist/client/slot-provider.d.ts +20 -0
- package/dist/client/slot-provider.d.ts.map +1 -0
- package/dist/config-types.d.ts +2 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/dev-tools/logs.d.ts.map +1 -1
- package/dist/index.js +293 -124
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +27 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/prebuilt-options-analysis.d.ts +40 -0
- package/dist/plugins/prebuilt-options-analysis.d.ts.map +1 -0
- package/dist/plugins/prebuilt.d.ts.map +1 -1
- package/dist/plugins/prerender-sugar.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/html-injector-core.d.ts +30 -9
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/internal.js +346 -51
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/prebuilt/cache-key.d.ts +4 -0
- package/dist/server/prebuilt/cache-key.d.ts.map +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +23 -0
- package/dist/server/prebuilt/key-discipline.d.ts.map +1 -0
- package/dist/server/prebuilt/slots.d.ts +74 -0
- package/dist/server/prebuilt/slots.d.ts.map +1 -0
- package/dist/server/prebuilt-builder.d.ts.map +1 -1
- package/dist/server/prebuilt-runtime.d.ts +12 -2
- package/dist/server/prebuilt-runtime.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts +29 -0
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/docs/api/34-api-config.mdx +165 -3
- package/docs/learn/00-introduction.mdx +78 -44
- package/docs/learn/13-configuration.mdx +27 -8
- package/package.json +3 -2
- package/src/adapters/cloudflare-kv-cache.ts +5 -1
- package/src/adapters/nitro.ts +188 -68
- package/src/cache/index.ts +25 -2
- package/src/cache/redis-handler.ts +27 -7
- package/src/cache/tag-aware-handler.ts +5 -1
- package/src/cache/timber-cache.ts +21 -10
- package/src/client/slot-context.ts +48 -0
- package/src/client/slot-outlet.tsx +22 -0
- package/src/client/slot-provider.tsx +25 -0
- package/src/config-types.ts +2 -1
- package/src/dev-tools/logs.ts +7 -0
- package/src/plugin-context.ts +54 -0
- package/src/plugins/cache.ts +1 -2
- package/src/plugins/client-chunks.ts +42 -1
- package/src/plugins/dev-server.ts +12 -69
- package/src/plugins/prebuilt-options-analysis.ts +175 -0
- package/src/plugins/prebuilt.ts +82 -127
- package/src/plugins/prerender-sugar.ts +1 -2
- package/src/server/html-injector-core.ts +85 -27
- package/src/server/html-injectors.ts +5 -1
- package/src/server/node-stream-transforms.ts +6 -1
- package/src/server/pipeline-phases.ts +4 -1
- package/src/server/prebuilt/cache-key.ts +74 -0
- package/src/server/prebuilt/key-discipline.ts +53 -0
- package/src/server/prebuilt/slots.ts +167 -0
- package/src/server/prebuilt-builder.ts +57 -23
- package/src/server/prebuilt-runtime.ts +144 -60
- package/src/server/rsc-entry/deny-fallback.ts +92 -0
- package/src/server/rsc-entry/index.ts +16 -70
- package/dist/_chunks/cache-api-CzYUlgXA.js.map +0 -1
- package/dist/_chunks/plugin-context-DeAxFRMq.js.map +0 -1
package/dist/server/internal.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as classifyMetadataRoute, i as METADATA_ROUTE_CONVENTIONS, o as getMetadataRouteAutoLink, r as canonicalize, s as getMetadataRouteServePath } from "../_chunks/canonicalize-P41GR6tY.js";
|
|
2
2
|
import { n as classifyUrlSegment } from "../_chunks/segment-classify-CDDRVKs7.js";
|
|
3
3
|
import { A as runWithRequestContext, D as getSearchParams, G as getTraceId, H as generateTraceId, I as getSetCookieHeaders, J as setSpanAttribute, K as replaceTraceId, L as getWaitUntil, M as setMutableCookieContext, N as setSegmentParams, O as getSegmentParams, P as getCookie, Q as requestContextAls, R as isDebug, T as getHeader, U as getOtelTraceId, X as earlyHintsSenderAls, Y as withSpan, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, et as timingAls, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMatchedSegmentPath, k as markResponseFlushed, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as runWithTraceId, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as applyRequestHeaderOverlay } from "../_chunks/logger-B_O6-mdJ.js";
|
|
4
|
-
import { i as coerce, t as executeAction } from "../_chunks/actions-
|
|
5
|
-
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-
|
|
4
|
+
import { i as coerce, t as executeAction } from "../_chunks/actions-CSDD6x7U.js";
|
|
5
|
+
import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-eb1gydM7.js";
|
|
6
6
|
import "../client/error-boundary.js";
|
|
7
7
|
import { n as toBracketKey } from "../_chunks/resolve-schema-3iUvBV5T.js";
|
|
8
8
|
import "../_chunks/segment-context-ZDnXDkbz.js";
|
|
@@ -1633,17 +1633,17 @@ async function coerceSegmentParams(match) {
|
|
|
1633
1633
|
* See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
|
|
1634
1634
|
*/
|
|
1635
1635
|
var EMPTY_SEGMENT_UPDATES = /* @__PURE__ */ new Map();
|
|
1636
|
-
var CTX_KEY$
|
|
1637
|
-
function getOrCreateContext$
|
|
1638
|
-
const existing = globalThis[CTX_KEY$
|
|
1636
|
+
var CTX_KEY$2 = Symbol.for("__timber_segment_update_ctx");
|
|
1637
|
+
function getOrCreateContext$2() {
|
|
1638
|
+
const existing = globalThis[CTX_KEY$2];
|
|
1639
1639
|
if (existing !== void 0) return existing;
|
|
1640
1640
|
if (typeof React.createContext === "function") {
|
|
1641
1641
|
const ctx = React.createContext(EMPTY_SEGMENT_UPDATES);
|
|
1642
|
-
globalThis[CTX_KEY$
|
|
1642
|
+
globalThis[CTX_KEY$2] = ctx;
|
|
1643
1643
|
return ctx;
|
|
1644
1644
|
}
|
|
1645
1645
|
}
|
|
1646
|
-
getOrCreateContext$
|
|
1646
|
+
getOrCreateContext$2();
|
|
1647
1647
|
//#endregion
|
|
1648
1648
|
//#region src/client/segment-outlet.tsx
|
|
1649
1649
|
/**
|
|
@@ -1690,17 +1690,17 @@ getOrCreateContext$1();
|
|
|
1690
1690
|
*
|
|
1691
1691
|
* See design/45-cache-lifetimes.md §Layout Propagation.
|
|
1692
1692
|
*/
|
|
1693
|
-
var CTX_KEY = Symbol.for("__timber_child_segment_ctx");
|
|
1694
|
-
function getOrCreateContext() {
|
|
1695
|
-
const existing = globalThis[CTX_KEY];
|
|
1693
|
+
var CTX_KEY$1 = Symbol.for("__timber_child_segment_ctx");
|
|
1694
|
+
function getOrCreateContext$1() {
|
|
1695
|
+
const existing = globalThis[CTX_KEY$1];
|
|
1696
1696
|
if (existing !== void 0) return existing;
|
|
1697
1697
|
if (typeof React.createContext === "function") {
|
|
1698
1698
|
const ctx = React.createContext(null);
|
|
1699
|
-
globalThis[CTX_KEY] = ctx;
|
|
1699
|
+
globalThis[CTX_KEY$1] = ctx;
|
|
1700
1700
|
return ctx;
|
|
1701
1701
|
}
|
|
1702
1702
|
}
|
|
1703
|
-
var ChildSegmentContext = getOrCreateContext();
|
|
1703
|
+
var ChildSegmentContext = getOrCreateContext$1();
|
|
1704
1704
|
//#endregion
|
|
1705
1705
|
//#region src/client/child-segment-outlet.tsx
|
|
1706
1706
|
/**
|
|
@@ -2287,7 +2287,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2287
2287
|
const responseHeaders = new Headers();
|
|
2288
2288
|
const requestHeaderOverlay = new Headers();
|
|
2289
2289
|
responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
|
|
2290
|
-
if (config.earlyHints) try {
|
|
2290
|
+
if (config.earlyHints && !isRscRequest) try {
|
|
2291
2291
|
await config.earlyHints(match, req, responseHeaders);
|
|
2292
2292
|
} catch (err) {
|
|
2293
2293
|
swallow(err, "early hints hook threw");
|
|
@@ -3195,6 +3195,58 @@ function toUnkeyable(error) {
|
|
|
3195
3195
|
return null;
|
|
3196
3196
|
}
|
|
3197
3197
|
/**
|
|
3198
|
+
* Split a props record into data props (participate in the cache key) and
|
|
3199
|
+
* slot values (React-element holes, excluded from the key) per the
|
|
3200
|
+
* component's declared `slots` option (TIM-1173).
|
|
3201
|
+
*
|
|
3202
|
+
* Every DECLARED slot lands in `slotValues` — present or not — because the
|
|
3203
|
+
* capture render always injects a SlotOutlet placeholder for each declared
|
|
3204
|
+
* slot: the cached shell is identical whether the caller passed the slot
|
|
3205
|
+
* or omitted it, so slot presence must not vary the key. `dataProps` is
|
|
3206
|
+
* everything else and is what gets hashed.
|
|
3207
|
+
*
|
|
3208
|
+
* Returns null when the props OBJECT itself is outside the identity
|
|
3209
|
+
* contract at the top level — accessors, symbol-keyed or non-enumerable
|
|
3210
|
+
* properties, or a non-`Object.prototype` prototype. Splitting such an
|
|
3211
|
+
* object would normalize away observable state (and reading an accessor
|
|
3212
|
+
* would invoke user code, which this walk must never do) BEFORE
|
|
3213
|
+
* `computeComponentCacheKey` gets the chance to reject it — a cached
|
|
3214
|
+
* shell could then be served for inputs a live render distinguishes.
|
|
3215
|
+
* Null means unkeyable: the caller renders live with the original props.
|
|
3216
|
+
* The walk is descriptor-reads only; getters are never invoked. Nested
|
|
3217
|
+
* data-prop values still face the full allowlist when hashed.
|
|
3218
|
+
*/
|
|
3219
|
+
/** Create an own enumerable data property — safe for keys like `__proto__`. */
|
|
3220
|
+
function defineOwn(target, key, value) {
|
|
3221
|
+
Object.defineProperty(target, key, {
|
|
3222
|
+
value,
|
|
3223
|
+
enumerable: true,
|
|
3224
|
+
writable: true,
|
|
3225
|
+
configurable: true
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
function splitSlotProps(props, slots) {
|
|
3229
|
+
try {
|
|
3230
|
+
if (Object.getPrototypeOf(props) !== Object.prototype) return null;
|
|
3231
|
+
if (Object.getOwnPropertySymbols(props).length > 0) return null;
|
|
3232
|
+
const slotSet = new Set(slots);
|
|
3233
|
+
const dataProps = {};
|
|
3234
|
+
const slotValues = {};
|
|
3235
|
+
for (const key of Object.getOwnPropertyNames(props)) {
|
|
3236
|
+
const desc = Object.getOwnPropertyDescriptor(props, key);
|
|
3237
|
+
if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
|
|
3238
|
+
defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
|
|
3239
|
+
}
|
|
3240
|
+
for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
|
|
3241
|
+
return {
|
|
3242
|
+
dataProps,
|
|
3243
|
+
slotValues
|
|
3244
|
+
};
|
|
3245
|
+
} catch (error) {
|
|
3246
|
+
return toUnkeyable(error);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3198
3250
|
* Compute the cache-entry hash for a component render:
|
|
3199
3251
|
* `sha256({props, params}).slice(0, 16)` over canonical JSON.
|
|
3200
3252
|
*
|
|
@@ -3212,6 +3264,216 @@ function computeComponentCacheKey(props, params) {
|
|
|
3212
3264
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
3213
3265
|
}
|
|
3214
3266
|
//#endregion
|
|
3267
|
+
//#region src/client/slot-context.ts
|
|
3268
|
+
/**
|
|
3269
|
+
* SlotContext — delivers live slot content to SlotOutlet holes inside
|
|
3270
|
+
* cached component shells (TIM-1173).
|
|
3271
|
+
*
|
|
3272
|
+
* A `cache.component(Fn, { slots: [...] })` shell is captured with a
|
|
3273
|
+
* stable <SlotOutlet slot={name} /> client reference in place of each
|
|
3274
|
+
* declared slot prop. At request time the revived shell is wrapped in a
|
|
3275
|
+
* SlotsProvider carrying the live slot values; each outlet reads its
|
|
3276
|
+
* value from this context by name.
|
|
3277
|
+
*
|
|
3278
|
+
* The value is a per-instance record — the NEAREST provider wins, so two
|
|
3279
|
+
* instances of the same cached component on one page each resolve their
|
|
3280
|
+
* own children, and a slot component nested inside another slot
|
|
3281
|
+
* component's children reads its own provider, not the outer one.
|
|
3282
|
+
*
|
|
3283
|
+
* Generalizes the ChildSegmentContext pattern (TIM-1181) from a single
|
|
3284
|
+
* implicit `children` hole on layouts to explicitly declared, named slot
|
|
3285
|
+
* props on any cached component.
|
|
3286
|
+
*
|
|
3287
|
+
* SINGLETON GUARANTEE: globalThis + Symbol.for — the RSC client bundler
|
|
3288
|
+
* can duplicate this module across chunks; globalThis guarantees a single
|
|
3289
|
+
* context instance. Same pattern as ChildSegmentContext.
|
|
3290
|
+
*
|
|
3291
|
+
* See design/45-cache-lifetimes.md §Slot Components.
|
|
3292
|
+
*/
|
|
3293
|
+
var CTX_KEY = Symbol.for("__timber_slot_ctx");
|
|
3294
|
+
function getOrCreateContext() {
|
|
3295
|
+
const existing = globalThis[CTX_KEY];
|
|
3296
|
+
if (existing !== void 0) return existing;
|
|
3297
|
+
if (typeof React.createContext === "function") {
|
|
3298
|
+
const ctx = React.createContext(null);
|
|
3299
|
+
globalThis[CTX_KEY] = ctx;
|
|
3300
|
+
return ctx;
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
var SlotContext = getOrCreateContext();
|
|
3304
|
+
//#endregion
|
|
3305
|
+
//#region src/client/slot-outlet.tsx
|
|
3306
|
+
/**
|
|
3307
|
+
* SlotOutlet — the hole in a cached component shell (TIM-1173).
|
|
3308
|
+
*
|
|
3309
|
+
* At capture time the prebuilt runtime renders the wrapped component with
|
|
3310
|
+
* <SlotOutlet slot={name} /> in place of each declared slot prop. Being a
|
|
3311
|
+
* client component, it serializes into the flight payload as a stable,
|
|
3312
|
+
* deterministic client reference — the same bytes regardless of what live
|
|
3313
|
+
* content will fill the hole. At request time the revived shell sits under
|
|
3314
|
+
* a SlotsProvider and each outlet reads its live value from SlotContext.
|
|
3315
|
+
*
|
|
3316
|
+
* See design/45-cache-lifetimes.md §Slot Components.
|
|
3317
|
+
*/
|
|
3318
|
+
function SlotOutlet({ slot }) {
|
|
3319
|
+
const values = useContext(SlotContext);
|
|
3320
|
+
return values ? values[slot] ?? null : null;
|
|
3321
|
+
}
|
|
3322
|
+
//#endregion
|
|
3323
|
+
//#region src/client/slot-provider.tsx
|
|
3324
|
+
/**
|
|
3325
|
+
* SlotsProvider — wraps a revived cached shell to supply live slot content
|
|
3326
|
+
* to the SlotOutlet holes inside it (TIM-1173).
|
|
3327
|
+
*
|
|
3328
|
+
* Tree structure per cached-component instance:
|
|
3329
|
+
* SlotsProvider(values={children: <Live />})
|
|
3330
|
+
* └── revived shell
|
|
3331
|
+
* └── SlotOutlet(slot="children") → reads context → <Live />
|
|
3332
|
+
*
|
|
3333
|
+
* See design/45-cache-lifetimes.md §Slot Components.
|
|
3334
|
+
*/
|
|
3335
|
+
function SlotsProvider({ values, children }) {
|
|
3336
|
+
return createElement(SlotContext.Provider, { value: values }, children);
|
|
3337
|
+
}
|
|
3338
|
+
//#endregion
|
|
3339
|
+
//#region src/server/prebuilt/slots.ts
|
|
3340
|
+
/**
|
|
3341
|
+
* Slot-component helpers for cache.component (TIM-1173).
|
|
3342
|
+
*
|
|
3343
|
+
* A `slots: ['children', ...]` declaration caches the component's output
|
|
3344
|
+
* as a shell with a stable <SlotOutlet slot={name} /> client-reference
|
|
3345
|
+
* hole per declared slot prop; live slot content composes in per instance
|
|
3346
|
+
* at request time via SlotsProvider/SlotContext. This generalizes the
|
|
3347
|
+
* TIM-1181 layout mechanism (ChildSegmentOutlet) to explicitly declared,
|
|
3348
|
+
* named slots on any cached component.
|
|
3349
|
+
*
|
|
3350
|
+
* Both the production wrapper (cache path) and the dev wrapper (no cache,
|
|
3351
|
+
* same tree shape for dev/prod parity) build their render from these
|
|
3352
|
+
* helpers. See design/45-cache-lifetimes.md §Slot Components.
|
|
3353
|
+
*/
|
|
3354
|
+
/**
|
|
3355
|
+
* Is this value the framework-injected `<ChildSegmentOutlet />` element?
|
|
3356
|
+
* Total: the `in` probe and `.type` read run against a user-controlled
|
|
3357
|
+
* value, and a Proxy trap can throw — that must classify as "not the
|
|
3358
|
+
* outlet" (the canonicalizer then rejects the value as unkeyable → live
|
|
3359
|
+
* render), not abort the request (codex P2 on PR #890).
|
|
3360
|
+
*/
|
|
3361
|
+
function isChildSegmentOutletElement(value) {
|
|
3362
|
+
try {
|
|
3363
|
+
return value != null && typeof value === "object" && "type" in value && value.type === ChildSegmentOutlet;
|
|
3364
|
+
} catch {
|
|
3365
|
+
return false;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
/**
|
|
3369
|
+
* Dense array of non-empty strings — no holes, no non-string entries.
|
|
3370
|
+
* Total: the reads run against a user-controlled value at module load,
|
|
3371
|
+
* and a throwing Proxy trap must classify as invalid, not crash startup.
|
|
3372
|
+
*/
|
|
3373
|
+
function isDenseStringArray(value) {
|
|
3374
|
+
try {
|
|
3375
|
+
if (!Array.isArray(value)) return false;
|
|
3376
|
+
for (let i = 0; i < value.length; i++) {
|
|
3377
|
+
if (!Object.hasOwn(value, i)) return false;
|
|
3378
|
+
const entry = value[i];
|
|
3379
|
+
if (typeof entry !== "string" || entry.length === 0) return false;
|
|
3380
|
+
}
|
|
3381
|
+
return true;
|
|
3382
|
+
} catch {
|
|
3383
|
+
return false;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
/**
|
|
3387
|
+
* Validate the `slots` option. Both the production and dev wrappers gate
|
|
3388
|
+
* on this — slot substitution is active in dev exactly when it would be
|
|
3389
|
+
* active in production, so the tree shape a component sees never
|
|
3390
|
+
* diverges between the two.
|
|
3391
|
+
*/
|
|
3392
|
+
function resolveSlotNames(id, options) {
|
|
3393
|
+
const slots = options.slots;
|
|
3394
|
+
if (slots === void 0) return { kind: "none" };
|
|
3395
|
+
if (!isDenseStringArray(slots)) {
|
|
3396
|
+
console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
|
|
3397
|
+
return { kind: "disabled" };
|
|
3398
|
+
}
|
|
3399
|
+
if (slots.length === 0) return { kind: "none" };
|
|
3400
|
+
if (!(options.ttl !== void 0 || options.tags != null)) {
|
|
3401
|
+
console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
|
|
3402
|
+
return { kind: "disabled" };
|
|
3403
|
+
}
|
|
3404
|
+
return {
|
|
3405
|
+
kind: "active",
|
|
3406
|
+
slots
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Split the live props into the cacheable shell render (data props plus a
|
|
3411
|
+
* deterministic SlotOutlet placeholder per declared slot — identical
|
|
3412
|
+
* whether or not the caller passed the slot) and the per-instance live
|
|
3413
|
+
* slot values.
|
|
3414
|
+
*
|
|
3415
|
+
* Null when the props object itself is unkeyable at the top level
|
|
3416
|
+
* (accessors, hidden properties, exotic prototype — see splitSlotProps):
|
|
3417
|
+
* the caller must render live with the original props.
|
|
3418
|
+
*/
|
|
3419
|
+
function prepareSlotRender(props, slots) {
|
|
3420
|
+
const split = splitSlotProps(props ?? {}, slots);
|
|
3421
|
+
if (split === null) return null;
|
|
3422
|
+
const { dataProps, slotValues } = split;
|
|
3423
|
+
const shellProps = { ...dataProps };
|
|
3424
|
+
for (const slot of slots) Object.defineProperty(shellProps, slot, {
|
|
3425
|
+
value: createElement(SlotOutlet, { slot }),
|
|
3426
|
+
enumerable: true,
|
|
3427
|
+
writable: true,
|
|
3428
|
+
configurable: true
|
|
3429
|
+
});
|
|
3430
|
+
return {
|
|
3431
|
+
shellProps,
|
|
3432
|
+
dataProps,
|
|
3433
|
+
slotValues
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
/**
|
|
3437
|
+
* Wrap a rendered shell (revived payload or live element) in a
|
|
3438
|
+
* per-instance SlotsProvider feeding the live values to its outlets.
|
|
3439
|
+
*/
|
|
3440
|
+
function wrapInSlotsProvider(shell, slotValues) {
|
|
3441
|
+
return createElement(SlotsProvider, {
|
|
3442
|
+
values: slotValues,
|
|
3443
|
+
children: shell
|
|
3444
|
+
});
|
|
3445
|
+
}
|
|
3446
|
+
//#endregion
|
|
3447
|
+
//#region src/server/prebuilt/key-discipline.ts
|
|
3448
|
+
/** Components already warned — exported so tests can reset between cases. */
|
|
3449
|
+
var keyDisciplineWarned = /* @__PURE__ */ new Set();
|
|
3450
|
+
var REQUEST_CONTEXT_FIELDS = {
|
|
3451
|
+
headers: "headers()",
|
|
3452
|
+
parsedCookies: "cookies()",
|
|
3453
|
+
searchParams: "searchParams"
|
|
3454
|
+
};
|
|
3455
|
+
/**
|
|
3456
|
+
* Wrap a store with a proxy that records per-request field access.
|
|
3457
|
+
* The proxy forwards all reads/writes transparently — it only observes.
|
|
3458
|
+
*/
|
|
3459
|
+
function createKeyDisciplineProxy(store) {
|
|
3460
|
+
const accessedApis = /* @__PURE__ */ new Set();
|
|
3461
|
+
return {
|
|
3462
|
+
proxy: new Proxy(store, { get(target, prop, receiver) {
|
|
3463
|
+
const api = REQUEST_CONTEXT_FIELDS[prop];
|
|
3464
|
+
if (api) accessedApis.add(api);
|
|
3465
|
+
return Reflect.get(target, prop, receiver);
|
|
3466
|
+
} }),
|
|
3467
|
+
accessedApis
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
function emitKeyDisciplineWarning(id, apis) {
|
|
3471
|
+
if (keyDisciplineWarned.has(id)) return;
|
|
3472
|
+
keyDisciplineWarned.add(id);
|
|
3473
|
+
const apiList = Array.from(apis).join(", ");
|
|
3474
|
+
console.warn(`[timber] cache.component key-discipline warning: "${id}" reads ${apiList} but the cache key is computed from props and segment params only. If the output varies by ${apiList}, different users may see each other's cached content. Either pass request-derived values as props (included in the cache key) or remove the ${apiList} call.`);
|
|
3475
|
+
}
|
|
3476
|
+
//#endregion
|
|
3215
3477
|
//#region src/server/prebuilt/capture-state.ts
|
|
3216
3478
|
/** Thrown by the wrapper when a capture renders its own component. */
|
|
3217
3479
|
var PrebuiltCaptureCycleError = class extends Error {
|
|
@@ -3343,6 +3605,26 @@ function resolveKeyParams(store) {
|
|
|
3343
3605
|
}
|
|
3344
3606
|
return main;
|
|
3345
3607
|
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Mirror the production cache-key decision for a slot render, with no
|
|
3610
|
+
* cache IO — used by the dev wrapper so slot substitution happens in dev
|
|
3611
|
+
* exactly when production would serve the cache path rather than the
|
|
3612
|
+
* live fallback (dev/prod parity, codex P2 on PR #890): requires an ALS
|
|
3613
|
+
* store, keyable params (slot-param divergence unkeys), and keyable data
|
|
3614
|
+
* props (after the ChildSegmentOutlet strip).
|
|
3615
|
+
*/
|
|
3616
|
+
function isSlotRenderKeyable(dataProps) {
|
|
3617
|
+
const store = requestContextAls.getStore();
|
|
3618
|
+
if (!store) return false;
|
|
3619
|
+
const keyParams = resolveKeyParams(store);
|
|
3620
|
+
if (keyParams === null) return false;
|
|
3621
|
+
let keyProps = dataProps;
|
|
3622
|
+
if (isChildSegmentOutletElement(keyProps.children)) {
|
|
3623
|
+
const { children: _children, ...rest } = keyProps;
|
|
3624
|
+
keyProps = rest;
|
|
3625
|
+
}
|
|
3626
|
+
return computeComponentCacheKey(keyProps, keyParams) !== null;
|
|
3627
|
+
}
|
|
3346
3628
|
/** Wrap payload bytes as a single-chunk ReadableStream for the decoder. */
|
|
3347
3629
|
function bytesToStream(bytes) {
|
|
3348
3630
|
return new ReadableStream({ start(controller) {
|
|
@@ -3354,11 +3636,34 @@ function bytesToStream(bytes) {
|
|
|
3354
3636
|
function hasRuntimeLifetime(options) {
|
|
3355
3637
|
return options.ttl !== void 0 || options.tags !== void 0 && options.tags !== null;
|
|
3356
3638
|
}
|
|
3357
|
-
/**
|
|
3639
|
+
/**
|
|
3640
|
+
* Resolve tags from options (may be static array or function of props).
|
|
3641
|
+
* Function-form tags receive DATA props only — slot props are excluded so
|
|
3642
|
+
* the tags fn never sees SlotOutlet placeholders (capture paths pass
|
|
3643
|
+
* placeholder-substituted props) and tags stay deterministic per key.
|
|
3644
|
+
*/
|
|
3358
3645
|
function resolveTags(options, props) {
|
|
3359
3646
|
if (!options.tags) return [];
|
|
3360
3647
|
if (Array.isArray(options.tags)) return options.tags;
|
|
3361
|
-
|
|
3648
|
+
const slots = options.slots;
|
|
3649
|
+
const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
|
|
3650
|
+
return options.tags(fnProps);
|
|
3651
|
+
}
|
|
3652
|
+
/**
|
|
3653
|
+
* Serve a slot component from the cache: split live slot values out of the
|
|
3654
|
+
* props, look up / capture the shell (data props + a deterministic
|
|
3655
|
+
* SlotOutlet placeholder per slot), and wrap the revived shell in a
|
|
3656
|
+
* per-instance SlotsProvider that feeds the live values to the outlets.
|
|
3657
|
+
*
|
|
3658
|
+
* Null on any miss — the caller renders live with the original props.
|
|
3659
|
+
*/
|
|
3660
|
+
async function tryReviveSlotShell(id, options, slots, props) {
|
|
3661
|
+
const render = prepareSlotRender(props, slots);
|
|
3662
|
+
if (render === null) return null;
|
|
3663
|
+
const { shellProps, dataProps, slotValues } = render;
|
|
3664
|
+
const revived = await tryRevivePayload(id, options, shellProps, dataProps);
|
|
3665
|
+
if (revived === null) return null;
|
|
3666
|
+
return { element: wrapInSlotsProvider(revived.element, slotValues) };
|
|
3362
3667
|
}
|
|
3363
3668
|
var rscFns = null;
|
|
3364
3669
|
function requireRscFunctions() {
|
|
@@ -3382,33 +3687,6 @@ async function revivePayload(id, payload, outcome) {
|
|
|
3382
3687
|
function getCaptureTimeoutMs() {
|
|
3383
3688
|
return componentTimeoutMs;
|
|
3384
3689
|
}
|
|
3385
|
-
var keyDisciplineWarned = /* @__PURE__ */ new Set();
|
|
3386
|
-
var REQUEST_CONTEXT_FIELDS = {
|
|
3387
|
-
headers: "headers()",
|
|
3388
|
-
parsedCookies: "cookies()",
|
|
3389
|
-
searchParams: "searchParams"
|
|
3390
|
-
};
|
|
3391
|
-
/**
|
|
3392
|
-
* Wrap a store with a proxy that records per-request field access.
|
|
3393
|
-
* The proxy forwards all reads/writes transparently — it only observes.
|
|
3394
|
-
*/
|
|
3395
|
-
function createKeyDisciplineProxy(store) {
|
|
3396
|
-
const accessedApis = /* @__PURE__ */ new Set();
|
|
3397
|
-
return {
|
|
3398
|
-
proxy: new Proxy(store, { get(target, prop, receiver) {
|
|
3399
|
-
const api = REQUEST_CONTEXT_FIELDS[prop];
|
|
3400
|
-
if (api) accessedApis.add(api);
|
|
3401
|
-
return Reflect.get(target, prop, receiver);
|
|
3402
|
-
} }),
|
|
3403
|
-
accessedApis
|
|
3404
|
-
};
|
|
3405
|
-
}
|
|
3406
|
-
function emitKeyDisciplineWarning(id, apis) {
|
|
3407
|
-
if (keyDisciplineWarned.has(id)) return;
|
|
3408
|
-
keyDisciplineWarned.add(id);
|
|
3409
|
-
const apiList = Array.from(apis).join(", ");
|
|
3410
|
-
console.warn(`[timber] cache.component key-discipline warning: "${id}" reads ${apiList} but the cache key is computed from props and segment params only. If the output varies by ${apiList}, different users may see each other's cached content. Either pass request-derived values as props (included in the cache key) or remove the ${apiList} call.`);
|
|
3411
|
-
}
|
|
3412
3690
|
/**
|
|
3413
3691
|
* Render a component in isolation and capture its flight payload bytes.
|
|
3414
3692
|
* Used for inline re-renders (tombstone/miss) and SWR background re-renders.
|
|
@@ -3539,7 +3817,7 @@ function backgroundRerender(id, component, props, cacheKey, options, hasSeed, sn
|
|
|
3539
3817
|
*
|
|
3540
3818
|
* Null on any miss — the caller renders live.
|
|
3541
3819
|
*/
|
|
3542
|
-
async function tryRevivePayload(id, options, props) {
|
|
3820
|
+
async function tryRevivePayload(id, options, props, keyPropsOverride) {
|
|
3543
3821
|
const hasSource = hasPrebuiltPayloadSource();
|
|
3544
3822
|
const hasRuntime = hasRuntimeLifetime(options);
|
|
3545
3823
|
if (!hasSource && !hasRuntime) return null;
|
|
@@ -3547,13 +3825,11 @@ async function tryRevivePayload(id, options, props) {
|
|
|
3547
3825
|
if (!store) return null;
|
|
3548
3826
|
const keyParams = (options.prerender ? await isParamIndependent(id) : false) ? {} : resolveKeyParams(store);
|
|
3549
3827
|
if (keyParams === null) return null;
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
if (childrenValue != null && typeof childrenValue === "object" && "type" in childrenValue && childrenValue.type === ChildSegmentOutlet) {
|
|
3554
|
-
const { children: _children, ...rest } = allProps;
|
|
3828
|
+
let keyProps = keyPropsOverride ?? props ?? {};
|
|
3829
|
+
if (isChildSegmentOutletElement(keyProps.children)) {
|
|
3830
|
+
const { children: _children, ...rest } = keyProps;
|
|
3555
3831
|
keyProps = rest;
|
|
3556
|
-
}
|
|
3832
|
+
}
|
|
3557
3833
|
const cacheKey = computeComponentCacheKey(keyProps, keyParams);
|
|
3558
3834
|
if (cacheKey === null) return null;
|
|
3559
3835
|
const outcome = options.fallbackOnMissingSegmentParams === false ? "failing the render (fallbackOnMissingSegmentParams: false)" : "rendering dynamically";
|
|
@@ -3605,6 +3881,19 @@ async function tryRevivePayload(id, options, props) {
|
|
|
3605
3881
|
* component live (miss).
|
|
3606
3882
|
*/
|
|
3607
3883
|
function __prebuilt(id, component, options = {}) {
|
|
3884
|
+
const resolution = resolveSlotNames(id, options);
|
|
3885
|
+
const slots = resolution.kind === "active" ? resolution.slots : null;
|
|
3886
|
+
if (resolution.kind === "active" && options.prerender) {
|
|
3887
|
+
console.warn(`[timber] cache.component "${id}": slots are runtime-tier only — prerender is ignored (the build cannot enumerate slot content). Use ttl/tags for the cache lifetime.`);
|
|
3888
|
+
options = {
|
|
3889
|
+
...options,
|
|
3890
|
+
prerender: void 0
|
|
3891
|
+
};
|
|
3892
|
+
}
|
|
3893
|
+
if (resolution.kind === "disabled") options = {
|
|
3894
|
+
...options,
|
|
3895
|
+
slots: void 0
|
|
3896
|
+
};
|
|
3608
3897
|
registry.set(id, {
|
|
3609
3898
|
id,
|
|
3610
3899
|
component,
|
|
@@ -3613,7 +3902,7 @@ function __prebuilt(id, component, options = {}) {
|
|
|
3613
3902
|
const wrapper = async (props) => {
|
|
3614
3903
|
noteWrapperRender(id, options);
|
|
3615
3904
|
if (isCaptureActive()) return createElement(component, props);
|
|
3616
|
-
const revived = await tryRevivePayload(id, options, props);
|
|
3905
|
+
const revived = slots ? await tryReviveSlotShell(id, options, slots, props) : await tryRevivePayload(id, options, props);
|
|
3617
3906
|
if (revived !== null) return revived.element;
|
|
3618
3907
|
if (options.fallbackOnMissingSegmentParams === false) throw new Error(`[timber] cache.component "${id}": no prebuilt payload for the current params and fallbackOnMissingSegmentParams is false`);
|
|
3619
3908
|
return createElement(component, props);
|
|
@@ -3635,13 +3924,19 @@ function getPrebuiltRegistry() {
|
|
|
3635
3924
|
* component on first render so the production behavior difference is visible.
|
|
3636
3925
|
* See design/44 §Dev Mode.
|
|
3637
3926
|
*/
|
|
3638
|
-
function __prebuiltDev(name, component,
|
|
3927
|
+
function __prebuiltDev(name, component, options = {}) {
|
|
3928
|
+
const resolution = resolveSlotNames(name, options);
|
|
3929
|
+
const slots = resolution.kind === "active" ? resolution.slots : null;
|
|
3639
3930
|
let logged = false;
|
|
3640
3931
|
const wrapper = (props) => {
|
|
3641
3932
|
if (!logged) {
|
|
3642
3933
|
logged = true;
|
|
3643
3934
|
console.log(`[timber] cache.component: "${name}" — rendering dynamically (dev mode)`);
|
|
3644
3935
|
}
|
|
3936
|
+
if (slots) {
|
|
3937
|
+
const render = prepareSlotRender(props, slots);
|
|
3938
|
+
if (render !== null && isSlotRenderKeyable(render.dataProps)) return wrapInSlotsProvider(createElement(component, render.shellProps), render.slotValues);
|
|
3939
|
+
}
|
|
3645
3940
|
return createElement(component, props);
|
|
3646
3941
|
};
|
|
3647
3942
|
return wrapper;
|