@visual-platform/next 0.3.0 → 0.4.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/dist/adapters.d.ts +51 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +79 -0
- package/dist/adapters.js.map +1 -0
- package/dist/client.d.ts +54 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +114 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +243 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +309 -11
- package/dist/index.js.map +1 -1
- package/dist/sources-entry.d.ts +4 -0
- package/dist/sources-entry.d.ts.map +1 -0
- package/dist/sources-entry.js +4 -0
- package/dist/sources-entry.js.map +1 -0
- package/dist/sources.d.ts +248 -0
- package/dist/sources.d.ts.map +1 -0
- package/dist/sources.js +352 -0
- package/dist/sources.js.map +1 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +52 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +12 -4
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { ACTION_BRAND, SOURCE_BRAND, type SourcePlan } from "@visual-platform/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* App sources (ADR-048, blueprint 67). The app registers its data once; pages reference sources by
|
|
4
|
+
* name and the SDK resolves — on this Next.js server only — the sources a page actually reads.
|
|
5
|
+
*
|
|
6
|
+
* sources: {
|
|
7
|
+
* siteConfig: appConfig, // plain value: read as is
|
|
8
|
+
* products: defineSource({ params, resolve: ({ params }) => cms.products(params) }),
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* Security by construction: a public source is cached across visitors, so its `context()` only ever
|
|
12
|
+
* sees the static context; only `visibility: "private"` sources see the request context, and they are
|
|
13
|
+
* never put in the shared cache.
|
|
14
|
+
*/
|
|
15
|
+
type StandardIssue = {
|
|
16
|
+
readonly message: string;
|
|
17
|
+
readonly path?: ReadonlyArray<PropertyKey | {
|
|
18
|
+
readonly key: PropertyKey;
|
|
19
|
+
}> | undefined;
|
|
20
|
+
};
|
|
21
|
+
type StandardResult<O> = {
|
|
22
|
+
readonly value: O;
|
|
23
|
+
readonly issues?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
readonly issues: ReadonlyArray<StandardIssue>;
|
|
26
|
+
};
|
|
27
|
+
export type StandardSchema<I = unknown, O = I> = {
|
|
28
|
+
readonly "~standard": {
|
|
29
|
+
readonly version: 1;
|
|
30
|
+
readonly vendor: string;
|
|
31
|
+
readonly validate: (value: unknown) => StandardResult<O> | Promise<StandardResult<O>>;
|
|
32
|
+
readonly types?: {
|
|
33
|
+
readonly input: I;
|
|
34
|
+
readonly output: O;
|
|
35
|
+
} | undefined;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
type OutputOf<S> = S extends StandardSchema<unknown, infer O> ? O : Record<string, unknown>;
|
|
39
|
+
export declare function validate<O>(schema: StandardSchema<unknown, O>, value: unknown): Promise<{
|
|
40
|
+
ok: true;
|
|
41
|
+
value: O;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
message: string;
|
|
45
|
+
}>;
|
|
46
|
+
export type SourceVisibility = "public" | "segment" | "private";
|
|
47
|
+
/** Static (cache-safe) context merged with the request context for private sources. */
|
|
48
|
+
export type VisualContextValue = Record<string, unknown>;
|
|
49
|
+
export type SourceResolveArgs<P> = {
|
|
50
|
+
params: P;
|
|
51
|
+
/** Content locale of the page. */
|
|
52
|
+
locale: string;
|
|
53
|
+
/**
|
|
54
|
+
* The app context. Public sources see the static context only; segment sources also `segment`;
|
|
55
|
+
* private sources the full request context (cookies, session…).
|
|
56
|
+
*/
|
|
57
|
+
context: () => Promise<VisualContextValue>;
|
|
58
|
+
/** Aborted when the source times out. Pass it to fetch(). */
|
|
59
|
+
signal: AbortSignal;
|
|
60
|
+
};
|
|
61
|
+
/** Same symbol the CLI checks (contracts SOURCE_BRAND) — `vp sources push` never imports this SDK. */
|
|
62
|
+
declare const SOURCE: typeof SOURCE_BRAND;
|
|
63
|
+
export type SourceDefinition<P = Record<string, unknown>, T = unknown> = {
|
|
64
|
+
readonly [SOURCE]: true;
|
|
65
|
+
label?: string | undefined;
|
|
66
|
+
description?: string | undefined;
|
|
67
|
+
/** Validates and types the params (defaults apply before the cache key is built). */
|
|
68
|
+
params?: StandardSchema<unknown, P> | undefined;
|
|
69
|
+
/** Validates the result (once per cache miss). Also the field list Studio shows after `vp sources push`. */
|
|
70
|
+
shape?: StandardSchema<unknown, T> | undefined;
|
|
71
|
+
/** Editor sample and fallback "sample". Never shown on the live site otherwise. */
|
|
72
|
+
sample?: T | undefined;
|
|
73
|
+
/** Data the app already has: read as is (no cache, no timeout). */
|
|
74
|
+
value?: T | undefined;
|
|
75
|
+
/** The route passes it: <VisualPage data={{ name }} />. Missing → fallback. */
|
|
76
|
+
provided?: boolean | undefined;
|
|
77
|
+
resolve?: ((args: SourceResolveArgs<P>) => T | Promise<T>) | undefined;
|
|
78
|
+
/** Several calls of this source in one render → one call (same order as `params`). */
|
|
79
|
+
batch?: ((params: P[], args: Omit<SourceResolveArgs<P>, "params">) => Promise<T[]>) | undefined;
|
|
80
|
+
/** Shared cache (public/segment only). Default: the SDK `revalidate`. Tag `visual:source:<project>:<name>` is always added. */
|
|
81
|
+
cache?: {
|
|
82
|
+
revalidate?: number | false | undefined;
|
|
83
|
+
tags?: string[] | undefined;
|
|
84
|
+
} | undefined;
|
|
85
|
+
/** Milliseconds before the fallback is used. Default 1500. */
|
|
86
|
+
timeout?: number | undefined;
|
|
87
|
+
/** public (default): same for everyone · segment: per `context.segment` · private: per visitor, never cached. */
|
|
88
|
+
visibility?: SourceVisibility | undefined;
|
|
89
|
+
/** Previous name: `vp sources push` renames the Studio record so existing bindings keep working. */
|
|
90
|
+
renamedFrom?: string | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* "server" (default): resolved during the server render. "client": never in the HTML — a component that
|
|
93
|
+
* `needs` it gets a signed handle and loads it in the browser with `useVisualSource` (per-visitor data on
|
|
94
|
+
* a cached page). Studio-bound blocks cannot use client sources (they render on the server).
|
|
95
|
+
*/
|
|
96
|
+
load?: "server" | "client" | undefined;
|
|
97
|
+
/** For load "client": when the browser fetches it. Default "load". */
|
|
98
|
+
when?: "load" | "idle" | "visible" | "interaction" | undefined;
|
|
99
|
+
/** Server source that should not hold the page: its blocks stream in (Suspense) when it arrives. */
|
|
100
|
+
stream?: boolean | undefined;
|
|
101
|
+
};
|
|
102
|
+
/** What a component that `needs` a client source receives as the prop. Pass it to useVisualSource(). */
|
|
103
|
+
export type VisualSourceHandle = {
|
|
104
|
+
readonly $vpSource: string;
|
|
105
|
+
readonly source: string;
|
|
106
|
+
readonly when: "load" | "idle" | "visible" | "interaction";
|
|
107
|
+
};
|
|
108
|
+
export declare const isSourceHandle: (v: unknown) => v is VisualSourceHandle;
|
|
109
|
+
/** Any value (read as is), a function (resolve shorthand), or defineSource({...}). */
|
|
110
|
+
export type SourceEntry = SourceDefinition<any, any> | ((args: SourceResolveArgs<any>) => unknown) | string | number | boolean | null | readonly unknown[] | Record<string, unknown>;
|
|
111
|
+
export declare function defineSource<S extends StandardSchema | undefined = undefined, T = unknown>(def: Omit<SourceDefinition<OutputOf<S>, T>, typeof SOURCE | "params"> & {
|
|
112
|
+
params?: S;
|
|
113
|
+
}): SourceDefinition<OutputOf<S>, T>;
|
|
114
|
+
declare const ACTION: typeof ACTION_BRAND;
|
|
115
|
+
export type ActionRunArgs = {
|
|
116
|
+
/** The Studio event that triggered it (value, detail, itemIndex…), plain data. */
|
|
117
|
+
event: Record<string, unknown>;
|
|
118
|
+
/** Full app context (static + request: cookies/session) — actions always run per visitor. */
|
|
119
|
+
context: () => Promise<VisualContextValue>;
|
|
120
|
+
locale: string;
|
|
121
|
+
};
|
|
122
|
+
export type ActionDefinition<I = Record<string, unknown>, R = unknown> = {
|
|
123
|
+
readonly [ACTION]: true;
|
|
124
|
+
label?: string | undefined;
|
|
125
|
+
description?: string | undefined;
|
|
126
|
+
/** Validates the input. Never trust it: check permissions in `run` with the context. */
|
|
127
|
+
input?: StandardSchema<unknown, I> | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* Where input values come from, read on the server from the data the block was rendered with:
|
|
130
|
+
* { productId: "item.id" } (inside a Repeat) or { plan: "plans.0.id" }. Event values are in `event`.
|
|
131
|
+
*/
|
|
132
|
+
from?: Record<string, string> | undefined;
|
|
133
|
+
run: (input: I, args: ActionRunArgs) => R | Promise<R>;
|
|
134
|
+
/** Sources to refresh after success (shared cache tags + client handles in the browser). */
|
|
135
|
+
revalidates?: string[] | undefined;
|
|
136
|
+
};
|
|
137
|
+
/** Thrown inside `run` to show a message to the visitor (other errors stay generic). */
|
|
138
|
+
export declare class ActionError extends Error {
|
|
139
|
+
}
|
|
140
|
+
export declare function defineAction<S extends StandardSchema | undefined = undefined, R = unknown>(def: Omit<ActionDefinition<OutputOf<S>, R>, typeof ACTION | "input"> & {
|
|
141
|
+
input?: S;
|
|
142
|
+
}): ActionDefinition<OutputOf<S>, R>;
|
|
143
|
+
export declare const isActionDefinition: (v: unknown) => v is ActionDefinition;
|
|
144
|
+
export declare const isSourceDefinition: (v: unknown) => v is SourceDefinition;
|
|
145
|
+
/** Normalize an entry: plain values and functions become definitions. */
|
|
146
|
+
export declare function toDefinition(entry: SourceEntry): SourceDefinition;
|
|
147
|
+
export type VisualContextConfig = {
|
|
148
|
+
/** Cache-safe values (no cookies/headers): currency by locale, feature defaults… */
|
|
149
|
+
static?: ((a: {
|
|
150
|
+
locale: string;
|
|
151
|
+
}) => VisualContextValue | Promise<VisualContextValue>) | undefined;
|
|
152
|
+
/** Per visitor (cookies, session). Runs only when a page needs it — that page then renders per request. */
|
|
153
|
+
request?: ((a: {
|
|
154
|
+
locale: string;
|
|
155
|
+
}) => VisualContextValue | Promise<VisualContextValue>) | undefined;
|
|
156
|
+
};
|
|
157
|
+
export type SourceErrorCode = "SOURCE_CLIENT_ONLY" | "SOURCE_NOT_REGISTERED" | "SOURCE_NOT_PROVIDED" | "SOURCE_PARAM_INVALID" | "SOURCE_SHAPE_INVALID" | "SOURCE_TIMEOUT" | "SOURCE_FAILED" | "SOURCE_CYCLE";
|
|
158
|
+
export type SourceError = {
|
|
159
|
+
input: string;
|
|
160
|
+
source: string;
|
|
161
|
+
code: SourceErrorCode;
|
|
162
|
+
message: string;
|
|
163
|
+
};
|
|
164
|
+
/** Cross-request cache (Next `unstable_cache` in the SDK; injectable for tests and other runtimes). */
|
|
165
|
+
export type SourceCache = <T>(fn: () => Promise<T>, keyParts: string[], opts: {
|
|
166
|
+
revalidate?: number | false | undefined;
|
|
167
|
+
tags: string[];
|
|
168
|
+
}) => Promise<T>;
|
|
169
|
+
export type ResolverOptions = {
|
|
170
|
+
projectId: string;
|
|
171
|
+
sources: Record<string, SourceEntry>;
|
|
172
|
+
context?: VisualContextConfig | undefined;
|
|
173
|
+
cache?: SourceCache | undefined;
|
|
174
|
+
/** Default `revalidate` for shared-cache sources (the SDK passes its own). */
|
|
175
|
+
revalidate?: number | undefined;
|
|
176
|
+
timeout?: number | undefined;
|
|
177
|
+
/** Max sources resolving at once per render. Default 6. */
|
|
178
|
+
concurrency?: number | undefined;
|
|
179
|
+
/** Secret for client-source handles (VISUAL_SOURCE_SECRET); resolved lazily, only when a client source is used. */
|
|
180
|
+
secret?: (() => string) | undefined;
|
|
181
|
+
};
|
|
182
|
+
export type ResolveRequest = {
|
|
183
|
+
plan: SourcePlan;
|
|
184
|
+
locale: string;
|
|
185
|
+
route?: Record<string, unknown> | undefined;
|
|
186
|
+
query?: Record<string, string | string[] | undefined> | undefined;
|
|
187
|
+
/** Names the route already passed (prop `data`): never resolved again. */
|
|
188
|
+
provided?: Record<string, unknown> | undefined;
|
|
189
|
+
/** Per-request memo (React `cache()` in the SDK) shared by every page/component of the request. */
|
|
190
|
+
store: Map<string, Promise<unknown>>;
|
|
191
|
+
/**
|
|
192
|
+
* Inputs still read once the data known so far is applied (blocks hidden by a showIf that is already
|
|
193
|
+
* decidable are skipped). Called before each layer; omitted → everything in the plan is needed.
|
|
194
|
+
*/
|
|
195
|
+
needed?: ((scope: Record<string, unknown>) => Set<string>) | undefined;
|
|
196
|
+
/** Errors of streamed sources arrive after the response started. */
|
|
197
|
+
onLateError?: ((e: SourceError) => void) | undefined;
|
|
198
|
+
};
|
|
199
|
+
export type SourceStat = {
|
|
200
|
+
input: string;
|
|
201
|
+
source: string;
|
|
202
|
+
ms: number;
|
|
203
|
+
cache: "hit" | "miss" | "memo" | "private" | "value" | "client" | "stream" | "skipped" | "error";
|
|
204
|
+
};
|
|
205
|
+
export type ResolveOutcome = {
|
|
206
|
+
data: Record<string, unknown>;
|
|
207
|
+
/** Inputs whose fallback is "hide" and whose source failed. */
|
|
208
|
+
unavailable: Set<string>;
|
|
209
|
+
errors: SourceError[];
|
|
210
|
+
/** The request context ran → this render is per visitor (dynamic). */
|
|
211
|
+
requestContext: string[];
|
|
212
|
+
/** Streamed inputs (stream: true): render their blocks in <Suspense>. Resolve to undefined on failure. */
|
|
213
|
+
deferred: Map<string, Promise<unknown>>;
|
|
214
|
+
/** Per input: time, cache hit/miss… (debug panel). */
|
|
215
|
+
stats: SourceStat[];
|
|
216
|
+
};
|
|
217
|
+
export declare const sourceTag: (projectId: string, name: string) => string;
|
|
218
|
+
export declare function createSourceResolver(opts: ResolverOptions): {
|
|
219
|
+
resolve: (r: ResolveRequest) => Promise<ResolveOutcome>;
|
|
220
|
+
resolveHandle: (token: unknown) => Promise<{
|
|
221
|
+
ok: true;
|
|
222
|
+
data: unknown;
|
|
223
|
+
visibility: SourceVisibility;
|
|
224
|
+
} | {
|
|
225
|
+
ok: false;
|
|
226
|
+
status: number;
|
|
227
|
+
error: string;
|
|
228
|
+
}>;
|
|
229
|
+
resolveDirect: (name: string, rawParams: unknown, locale: string) => Promise<{
|
|
230
|
+
ok: true;
|
|
231
|
+
data: unknown;
|
|
232
|
+
} | {
|
|
233
|
+
ok: false;
|
|
234
|
+
status: number;
|
|
235
|
+
error: string;
|
|
236
|
+
}>;
|
|
237
|
+
staticContext: (r: ResolveRequest) => Promise<VisualContextValue>;
|
|
238
|
+
requestContext: (r: ResolveRequest) => Promise<{
|
|
239
|
+
[x: string]: unknown;
|
|
240
|
+
}>;
|
|
241
|
+
has: (name: string) => boolean;
|
|
242
|
+
names: () => string[];
|
|
243
|
+
definition: (name: string) => SourceDefinition<Record<string, unknown>, unknown> | undefined;
|
|
244
|
+
};
|
|
245
|
+
/** Constant-time string compare for webhook secrets. */
|
|
246
|
+
export declare function safeEqual(a: string, b: string): boolean;
|
|
247
|
+
export {};
|
|
248
|
+
//# sourceMappingURL=sources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"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"}
|
package/dist/sources.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { ACTION_BRAND, bindParams, SOURCE_BRAND, stableKey } from "@visual-platform/contracts";
|
|
2
|
+
import { signToken, verifyToken } from "./tokens.js";
|
|
3
|
+
export async function validate(schema, value) {
|
|
4
|
+
const r = await schema["~standard"].validate(value);
|
|
5
|
+
if (r.issues) {
|
|
6
|
+
const first = r.issues[0];
|
|
7
|
+
const path = first?.path?.map((p) => (typeof p === "object" ? String(p.key) : String(p))).join(".");
|
|
8
|
+
return { ok: false, message: `${path ? `${path}: ` : ""}${first?.message ?? "invalid"}` };
|
|
9
|
+
}
|
|
10
|
+
return { ok: true, value: r.value };
|
|
11
|
+
}
|
|
12
|
+
/** Same symbol the CLI checks (contracts SOURCE_BRAND) — `vp sources push` never imports this SDK. */
|
|
13
|
+
const SOURCE = SOURCE_BRAND;
|
|
14
|
+
export const isSourceHandle = (v) => !!v && typeof v === "object" && typeof v.$vpSource === "string";
|
|
15
|
+
function assertServer(what) {
|
|
16
|
+
if (typeof window !== "undefined")
|
|
17
|
+
throw new Error(`[visual] ${what} is server-only — never import your sources in a Client Component.`);
|
|
18
|
+
}
|
|
19
|
+
export function defineSource(def) {
|
|
20
|
+
assertServer("defineSource");
|
|
21
|
+
const kinds = [def.value !== undefined, !!def.provided, !!def.resolve].filter(Boolean).length;
|
|
22
|
+
if (kinds !== 1)
|
|
23
|
+
throw new Error("[visual] defineSource: give exactly one of value, provided or resolve");
|
|
24
|
+
if (def.timeout !== undefined && !(def.timeout > 0))
|
|
25
|
+
throw new Error("[visual] defineSource: timeout must be > 0 ms");
|
|
26
|
+
return { ...def, [SOURCE]: true };
|
|
27
|
+
}
|
|
28
|
+
// ---------- actions (ADR-048 P4) ----------
|
|
29
|
+
const ACTION = ACTION_BRAND;
|
|
30
|
+
/** Thrown inside `run` to show a message to the visitor (other errors stay generic). */
|
|
31
|
+
export class ActionError extends Error {
|
|
32
|
+
}
|
|
33
|
+
export function defineAction(def) {
|
|
34
|
+
assertServer("defineAction");
|
|
35
|
+
if (typeof def.run !== "function")
|
|
36
|
+
throw new Error("[visual] defineAction: run is required");
|
|
37
|
+
for (const p of Object.values(def.from ?? {}))
|
|
38
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z0-9_]+){0,9}$/.test(p))
|
|
39
|
+
throw new Error(`[visual] defineAction: bad path "${p}" in from`);
|
|
40
|
+
return { ...def, [ACTION]: true };
|
|
41
|
+
}
|
|
42
|
+
export const isActionDefinition = (v) => !!v && typeof v === "object" && v[ACTION] === true;
|
|
43
|
+
export const isSourceDefinition = (v) => !!v && typeof v === "object" && v[SOURCE] === true;
|
|
44
|
+
/** Normalize an entry: plain values and functions become definitions. */
|
|
45
|
+
export function toDefinition(entry) {
|
|
46
|
+
if (isSourceDefinition(entry))
|
|
47
|
+
return entry;
|
|
48
|
+
if (typeof entry === "function")
|
|
49
|
+
return { [SOURCE]: true, resolve: entry };
|
|
50
|
+
return { [SOURCE]: true, value: entry };
|
|
51
|
+
}
|
|
52
|
+
export const sourceTag = (projectId, name) => `visual:source:${projectId}:${name}`;
|
|
53
|
+
class Fail extends Error {
|
|
54
|
+
code;
|
|
55
|
+
constructor(code, message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function limiter(max) {
|
|
61
|
+
let active = 0;
|
|
62
|
+
const queue = [];
|
|
63
|
+
return async (fn) => {
|
|
64
|
+
if (active >= max)
|
|
65
|
+
await new Promise((r) => queue.push(r));
|
|
66
|
+
active++;
|
|
67
|
+
try {
|
|
68
|
+
return await fn();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
active--;
|
|
72
|
+
queue.shift()?.();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function withTimeout(ms, run) {
|
|
77
|
+
const ctl = new AbortController();
|
|
78
|
+
let timer;
|
|
79
|
+
const timeout = new Promise((_, reject) => {
|
|
80
|
+
timer = setTimeout(() => { ctl.abort(); reject(new Fail("SOURCE_TIMEOUT", `no answer after ${ms} ms`)); }, ms);
|
|
81
|
+
});
|
|
82
|
+
return Promise.race([run(ctl.signal), timeout]).finally(() => clearTimeout(timer));
|
|
83
|
+
}
|
|
84
|
+
export function createSourceResolver(opts) {
|
|
85
|
+
assertServer("createVisualNext({ sources })");
|
|
86
|
+
const defs = new Map(Object.entries(opts.sources).map(([k, v]) => [k, toDefinition(v)]));
|
|
87
|
+
const run = limiter(opts.concurrency ?? 6);
|
|
88
|
+
const passthrough = (fn) => fn();
|
|
89
|
+
const shared = opts.cache ?? passthrough;
|
|
90
|
+
/** Context loaders memoized in the request store (static ≠ request: the latter makes the page dynamic). */
|
|
91
|
+
const staticCtx = (r) => memo(r.store, `ctx:static:${r.locale}`, async () => (await opts.context?.static?.({ locale: r.locale })) ?? {});
|
|
92
|
+
const requestCtx = (r) => memo(r.store, `ctx:request:${r.locale}`, async () => (await opts.context?.request?.({ locale: r.locale })) ?? {});
|
|
93
|
+
const fullCtx = async (r) => ({ ...(await staticCtx(r)), ...(await requestCtx(r)) });
|
|
94
|
+
async function resolve(r) {
|
|
95
|
+
const out = { data: {}, unavailable: new Set(), errors: [], requestContext: [], deferred: new Map(), stats: [] };
|
|
96
|
+
// An input another input depends on is never streamed (its value is needed for their params).
|
|
97
|
+
const dependedOn = new Set(r.plan.layers.flat().flatMap((e) => e.dependsOn));
|
|
98
|
+
const provided = r.provided ?? {};
|
|
99
|
+
// Context keys used as params: static first, the request context only for keys static lacks.
|
|
100
|
+
let ctxValues = {};
|
|
101
|
+
if (r.plan.contextKeys.length) {
|
|
102
|
+
ctxValues = await staticCtx(r);
|
|
103
|
+
if (opts.context?.request && r.plan.contextKeys.some((k) => readKey(ctxValues, k) === undefined)) {
|
|
104
|
+
ctxValues = await fullCtx(r);
|
|
105
|
+
out.requestContext.push(`context params (${r.plan.contextKeys.join(", ")})`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const fallback = (e, def, err) => {
|
|
109
|
+
out.errors.push(err);
|
|
110
|
+
if (e.fallback === "hide")
|
|
111
|
+
out.unavailable.add(e.input);
|
|
112
|
+
else if (e.fallback === "sample" && def?.sample !== undefined)
|
|
113
|
+
out.data[e.input] = def.sample;
|
|
114
|
+
};
|
|
115
|
+
const remaining = r.plan.layers.flat();
|
|
116
|
+
for (const [li, layer] of r.plan.layers.entries()) {
|
|
117
|
+
const jobs = [];
|
|
118
|
+
// Skip inputs only read by blocks whose showIf is already false (and nothing needed depends on them).
|
|
119
|
+
let keep = null;
|
|
120
|
+
if (r.needed && li > 0) {
|
|
121
|
+
const needed = r.needed({ ...provided, ...out.data });
|
|
122
|
+
keep = new Set();
|
|
123
|
+
const byName = new Map(remaining.map((e) => [e.input, e]));
|
|
124
|
+
const visit = (n) => { if (keep.has(n))
|
|
125
|
+
return; keep.add(n); for (const d of byName.get(n)?.dependsOn ?? [])
|
|
126
|
+
visit(d); };
|
|
127
|
+
for (const e of remaining)
|
|
128
|
+
if (needed.has(e.input) || e.input.startsWith("$"))
|
|
129
|
+
visit(e.input);
|
|
130
|
+
}
|
|
131
|
+
for (const e of layer) {
|
|
132
|
+
remaining.splice(remaining.indexOf(e), 1);
|
|
133
|
+
if (e.input in provided)
|
|
134
|
+
continue;
|
|
135
|
+
if (keep && !keep.has(e.input)) {
|
|
136
|
+
out.stats.push({ input: e.input, source: e.sourceRef, ms: 0, cache: "skipped" });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const def = defs.get(e.sourceRef);
|
|
140
|
+
const err = (code, message) => ({ input: e.input, source: e.sourceRef, code, message });
|
|
141
|
+
if (!def) {
|
|
142
|
+
// Legacy "app" inputs (matched by name) may simply not be registered: the route passes them, or not.
|
|
143
|
+
if (e.kind === "appSource")
|
|
144
|
+
fallback(e, def, err("SOURCE_NOT_REGISTERED", `the app has no source "${e.sourceRef}"`));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (def.value !== undefined) {
|
|
148
|
+
out.data[e.input] = def.value;
|
|
149
|
+
out.stats.push({ input: e.input, source: e.sourceRef, ms: 0, cache: "value" });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (def.provided) {
|
|
153
|
+
fallback(e, def, err("SOURCE_NOT_PROVIDED", `pass it from the route: data={{ ${e.input} }}`));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const raw = bindParams(e.params, { locale: r.locale, route: r.route, query: r.query, context: (k) => readKey(ctxValues, k), scope: { ...out.data, ...provided } });
|
|
157
|
+
let params = raw;
|
|
158
|
+
if (def.params) {
|
|
159
|
+
const v = await validate(def.params, raw);
|
|
160
|
+
if (!v.ok) {
|
|
161
|
+
fallback(e, def, err("SOURCE_PARAM_INVALID", v.message));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
params = v.value;
|
|
165
|
+
}
|
|
166
|
+
if (def.load === "client") {
|
|
167
|
+
// Only components that `need` it get a signed handle; Studio-bound blocks render on the server.
|
|
168
|
+
if (!e.input.startsWith("$")) {
|
|
169
|
+
fallback(e, def, err("SOURCE_CLIENT_ONLY", "a load: \"client\" source can only feed a component that `needs` it (useVisualSource)"));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!opts.secret) {
|
|
173
|
+
fallback(e, def, err("SOURCE_FAILED", "client sources need a secret"));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const token = await signToken({ s: e.sourceRef, p: params, l: r.locale, x: Math.floor(Date.now() / 1000) + 3600 }, opts.secret());
|
|
177
|
+
out.data[e.input] = { $vpSource: token, source: e.sourceRef, when: def.when ?? "load" };
|
|
178
|
+
out.stats.push({ input: e.input, source: e.sourceRef, ms: 0, cache: "client" });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const visibility = def.visibility ?? "public";
|
|
182
|
+
let segment = "";
|
|
183
|
+
if (visibility === "segment") {
|
|
184
|
+
const ctx = await fullCtx(r);
|
|
185
|
+
out.requestContext.push(`"${e.input}" (segment)`);
|
|
186
|
+
segment = String(ctx.segment ?? "");
|
|
187
|
+
}
|
|
188
|
+
if (visibility === "private")
|
|
189
|
+
out.requestContext.push(`"${e.input}" (private)`);
|
|
190
|
+
jobs.push({ e, def, params, key: `src:${e.sourceRef}:${r.locale}:${segment}:${visibility === "private" ? "p:" : ""}${stableKey(params)}` });
|
|
191
|
+
}
|
|
192
|
+
// Same source + params in this layer (or earlier in the request) → one call. Batchable sources → one call.
|
|
193
|
+
const bySource = new Map();
|
|
194
|
+
for (const j of jobs)
|
|
195
|
+
if (!r.store.has(j.key))
|
|
196
|
+
bySource.set(j.e.sourceRef, [...(bySource.get(j.e.sourceRef) ?? []), j]);
|
|
197
|
+
for (const [name, list] of bySource) {
|
|
198
|
+
const def = list[0].def;
|
|
199
|
+
const unique = [...new Map(list.map((j) => [j.key, j])).values()];
|
|
200
|
+
if (def.batch && unique.length > 1) {
|
|
201
|
+
const all = memo(r.store, `batch:${unique.map((j) => j.key).join("|")}`, () => call(r, name, def, unique.map((j) => j.params), true));
|
|
202
|
+
unique.forEach((j, i) => r.store.set(j.key, all.then((rows) => {
|
|
203
|
+
if (!Array.isArray(rows) || rows.length !== unique.length)
|
|
204
|
+
throw new Fail("SOURCE_FAILED", "batch must return one result per params, in order");
|
|
205
|
+
return rows[i];
|
|
206
|
+
})));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const asError = (j, x) => ({ input: j.e.input, source: j.e.sourceRef, code: x instanceof Fail ? x.code : "SOURCE_FAILED", message: x instanceof Error ? x.message : String(x) });
|
|
210
|
+
await Promise.all(jobs.map(async (j) => {
|
|
211
|
+
const t0 = Date.now();
|
|
212
|
+
const hit = r.store.has(j.key);
|
|
213
|
+
const flag = { executed: false };
|
|
214
|
+
const p = memo(r.store, j.key, () => call(r, j.e.sourceRef, j.def, j.params, false, flag));
|
|
215
|
+
if (j.def.stream && !dependedOn.has(j.e.input)) {
|
|
216
|
+
// Streamed: the page does not wait; its blocks render in <Suspense> when it arrives.
|
|
217
|
+
out.deferred.set(j.e.input, p.then((v) => v, (x) => {
|
|
218
|
+
const er = asError(j, x);
|
|
219
|
+
r.onLateError?.(er);
|
|
220
|
+
return j.e.fallback === "sample" ? j.def.sample : undefined;
|
|
221
|
+
}));
|
|
222
|
+
if (j.e.fallback === "hide")
|
|
223
|
+
out.unavailable.add(j.e.input); // applies only if it fails (value missing)
|
|
224
|
+
out.stats.push({ input: j.e.input, source: j.e.sourceRef, ms: 0, cache: "stream" });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
out.data[j.e.input] = await p;
|
|
229
|
+
const visibility = j.def.visibility ?? "public";
|
|
230
|
+
out.stats.push({ input: j.e.input, source: j.e.sourceRef, ms: Date.now() - t0, cache: hit ? "memo" : visibility === "private" ? "private" : flag.executed ? "miss" : "hit" });
|
|
231
|
+
}
|
|
232
|
+
catch (x) {
|
|
233
|
+
fallback(j.e, j.def, asError(j, x));
|
|
234
|
+
out.stats.push({ input: j.e.input, source: j.e.sourceRef, ms: Date.now() - t0, cache: "error" });
|
|
235
|
+
}
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
/** One (or one batched) source call: timeout, shape check, shared cache unless private. */
|
|
241
|
+
function call(r, name, def, params, batched, flag) {
|
|
242
|
+
const visibility = def.visibility ?? "public";
|
|
243
|
+
const context = visibility === "private" ? () => fullCtx(r)
|
|
244
|
+
: visibility === "segment" ? async () => ({ ...(await staticCtx(r)), segment: (await fullCtx(r)).segment })
|
|
245
|
+
: () => staticCtx(r);
|
|
246
|
+
const exec = () => run(() => withTimeout(def.timeout ?? opts.timeout ?? 1500, async (signal) => {
|
|
247
|
+
if (flag)
|
|
248
|
+
flag.executed = true;
|
|
249
|
+
const base = { locale: r.locale, context, signal };
|
|
250
|
+
const result = batched ? await def.batch(params, base) : await def.resolve({ ...base, params: params });
|
|
251
|
+
if (def.shape) {
|
|
252
|
+
const rows = batched ? result : [result];
|
|
253
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
254
|
+
const v = await validate(def.shape, row);
|
|
255
|
+
if (!v.ok)
|
|
256
|
+
throw new Fail("SOURCE_SHAPE_INVALID", v.message);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}));
|
|
261
|
+
if (visibility === "private")
|
|
262
|
+
return exec();
|
|
263
|
+
const segment = visibility === "segment" ? (fullCtx(r).then((c) => String(c.segment ?? ""))) : Promise.resolve("");
|
|
264
|
+
return segment.then((seg) => shared(exec, ["visual-source", opts.projectId, name, r.locale, seg, batched ? "batch" : "one", stableKey(params)], {
|
|
265
|
+
revalidate: def.cache?.revalidate ?? opts.revalidate,
|
|
266
|
+
tags: [sourceTag(opts.projectId, name), ...(def.cache?.tags ?? [])],
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Resolve a client-source handle sent back by the browser: verify the signature and expiry, re-validate
|
|
271
|
+
* the params, then call the source as the server render would (private sources see this visitor's
|
|
272
|
+
* request context). Null when the handle is not valid for a client source.
|
|
273
|
+
*/
|
|
274
|
+
async function resolveHandle(token) {
|
|
275
|
+
if (!opts.secret)
|
|
276
|
+
return { ok: false, status: 404, error: "no client sources" };
|
|
277
|
+
const p = await verifyToken(token, opts.secret());
|
|
278
|
+
if (!p)
|
|
279
|
+
return { ok: false, status: 403, error: "invalid or expired handle" };
|
|
280
|
+
const def = defs.get(p.s);
|
|
281
|
+
if (!def || def.load !== "client" || !def.resolve)
|
|
282
|
+
return { ok: false, status: 403, error: "not a client source" };
|
|
283
|
+
let params = p.p;
|
|
284
|
+
if (def.params) {
|
|
285
|
+
const v = await validate(def.params, p.p);
|
|
286
|
+
if (!v.ok)
|
|
287
|
+
return { ok: false, status: 400, error: v.message };
|
|
288
|
+
params = v.value;
|
|
289
|
+
}
|
|
290
|
+
const r = { plan: { layers: [], contextKeys: [] }, locale: p.l, store: new Map() };
|
|
291
|
+
try {
|
|
292
|
+
return { ok: true, data: await call(r, p.s, def, params, false), visibility: def.visibility ?? "public" };
|
|
293
|
+
}
|
|
294
|
+
catch (x) {
|
|
295
|
+
return { ok: false, status: x instanceof Fail && x.code === "SOURCE_TIMEOUT" ? 504 : 502, error: x instanceof Fail ? x.code : "SOURCE_FAILED" };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Resolve one source directly (editor live preview): public sources only, params validated. */
|
|
299
|
+
async function resolveDirect(name, rawParams, locale) {
|
|
300
|
+
const def = defs.get(name);
|
|
301
|
+
if (!def)
|
|
302
|
+
return { ok: false, status: 404, error: "unknown source" };
|
|
303
|
+
if ((def.visibility ?? "public") !== "public")
|
|
304
|
+
return { ok: false, status: 403, error: "only public sources can be previewed" };
|
|
305
|
+
if (def.value !== undefined)
|
|
306
|
+
return { ok: true, data: def.value };
|
|
307
|
+
if (!def.resolve)
|
|
308
|
+
return { ok: false, status: 400, error: "this source is passed by the route" };
|
|
309
|
+
let params = rawParams ?? {};
|
|
310
|
+
if (def.params) {
|
|
311
|
+
const v = await validate(def.params, params);
|
|
312
|
+
if (!v.ok)
|
|
313
|
+
return { ok: false, status: 400, error: v.message };
|
|
314
|
+
params = v.value;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
return { ok: true, data: await call({ plan: { layers: [], contextKeys: [] }, locale, store: new Map() }, name, def, params, false) };
|
|
318
|
+
}
|
|
319
|
+
catch (x) {
|
|
320
|
+
return { ok: false, status: 502, error: x instanceof Fail ? x.code : "SOURCE_FAILED" };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { resolve, resolveHandle, resolveDirect, staticContext: staticCtx, requestContext: fullCtx, has: (name) => defs.has(name), names: () => [...defs.keys()], definition: (name) => defs.get(name) };
|
|
324
|
+
}
|
|
325
|
+
function memo(store, key, fn) {
|
|
326
|
+
let p = store.get(key);
|
|
327
|
+
if (!p) {
|
|
328
|
+
p = fn();
|
|
329
|
+
store.set(key, p);
|
|
330
|
+
p.catch(() => { });
|
|
331
|
+
}
|
|
332
|
+
return p;
|
|
333
|
+
}
|
|
334
|
+
function readKey(obj, key) {
|
|
335
|
+
let cur = obj;
|
|
336
|
+
for (const seg of key.split(".")) {
|
|
337
|
+
if (!cur || typeof cur !== "object" || !Object.prototype.hasOwnProperty.call(cur, seg))
|
|
338
|
+
return undefined;
|
|
339
|
+
cur = cur[seg];
|
|
340
|
+
}
|
|
341
|
+
return cur;
|
|
342
|
+
}
|
|
343
|
+
/** Constant-time string compare for webhook secrets. */
|
|
344
|
+
export function safeEqual(a, b) {
|
|
345
|
+
if (a.length !== b.length || !a)
|
|
346
|
+
return false;
|
|
347
|
+
let diff = 0;
|
|
348
|
+
for (let i = 0; i < a.length; i++)
|
|
349
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
350
|
+
return diff === 0;
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=sources.js.map
|