@plitzi/sdk-server 0.37.8 → 0.37.10
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/CHANGELOG.md +825 -0
- package/README.md +24 -2
- package/dist/actions.d.ts +1 -1
- package/dist/adapters/cloudAdapters.d.ts +5 -3
- package/dist/adapters/cloudAdapters.js +123 -63
- package/dist/adapters/space/types.d.ts +3 -0
- package/dist/core/configSeam.d.ts +14 -0
- package/dist/core/configSeam.js +114 -0
- package/dist/core/configSeam.test.d.ts +1 -0
- package/dist/core/createServer.js +11 -5
- package/dist/core/createServer.publicData.test.d.ts +1 -0
- package/dist/core/freePort.d.ts +10 -0
- package/dist/core/freePort.js +36 -0
- package/dist/core/freePort.test.d.ts +1 -0
- package/dist/core/http/stages/images.d.ts +6 -0
- package/dist/core/http/stages/images.js +47 -0
- package/dist/core/http/stages/images.test.d.ts +1 -0
- package/dist/core/server/fleet/channel.js +1 -1
- package/dist/core/server/fleet/relay.js +1 -1
- package/dist/core/services/action.js +5 -4
- package/dist/core/services/functionRoutes.d.ts +2 -2
- package/dist/core/services/functionRoutes.js +2 -2
- package/dist/core/services/registry.js +2 -0
- package/dist/functions-api.d.ts +23 -2
- package/dist/functions.d.ts +1 -1
- package/dist/functions.js +2 -2
- package/dist/helpers/cache/keys.d.ts +23 -10
- package/dist/helpers/cache/keys.js +16 -2
- package/dist/helpers/flagOverrides.d.ts +27 -0
- package/dist/helpers/flagOverrides.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/index.d.ts +1 -1
- package/dist/modules/actions/index.js +3 -2
- package/dist/modules/actions/moduleFor.js +5 -3
- package/dist/modules/actions/runtime/flags.d.ts +16 -0
- package/dist/modules/actions/runtime/flags.js +49 -0
- package/dist/modules/actions/runtime/flags.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvList.js +1 -1
- package/dist/modules/actions/runtime/renderResolver.js +30 -4
- package/dist/modules/actions/runtime/runAction.js +2 -0
- package/dist/modules/actions/taskCatalog.d.ts +3 -1
- package/dist/modules/actions/taskCatalog.js +1 -0
- package/dist/modules/actions/tasks/registry.js +4 -2
- package/dist/modules/actions/transport/callHandler.js +8 -0
- package/dist/modules/actions/types.d.ts +20 -1
- package/dist/modules/connectors/engine.js +1 -1
- package/dist/modules/functions/capabilities.js +1 -1
- package/dist/modules/functions/config.d.ts +19 -3
- package/dist/modules/functions/config.js +30 -14
- package/dist/modules/functions/contract.d.ts +10 -3
- package/dist/modules/functions/driver.js +4 -2
- package/dist/modules/functions/load.js +2 -2
- package/dist/modules/functions/manifest.d.ts +5 -3
- package/dist/modules/functions/manifest.js +37 -6
- package/dist/modules/functions/routes.d.ts +1 -1
- package/dist/modules/functions/routes.js +3 -3
- package/dist/modules/functions/runner/messages.js +1 -1
- package/dist/modules/functions/space.d.ts +7 -5
- package/dist/modules/functions/space.js +17 -10
- package/dist/modules/images/allowlist.d.ts +5 -0
- package/dist/modules/images/allowlist.js +18 -0
- package/dist/modules/images/imageProxy.d.ts +36 -0
- package/dist/modules/images/imageProxy.js +273 -0
- package/dist/modules/images/imageProxy.test.d.ts +1 -0
- package/dist/modules/images/transform.d.ts +10 -0
- package/dist/modules/images/transform.js +28 -0
- package/dist/modules/images/transform.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +12 -7
- package/dist/modules/rsc/connectorRscData.js +12 -8
- package/dist/modules/rsc/handler.js +18 -4
- package/dist/modules/rsc/publicFileResolver.d.ts +13 -0
- package/dist/modules/rsc/publicFileResolver.js +39 -0
- package/dist/modules/rsc/publicFileResolver.test.d.ts +1 -0
- package/dist/modules/rsc/resolveRscData.d.ts +4 -2
- package/dist/modules/rsc/resolveRscData.js +10 -2
- package/dist/modules/runtime/bundle.js +2 -2
- package/dist/modules/runtime/contract.d.ts +1 -1
- package/dist/modules/runtime/stages.d.ts +1 -1
- package/dist/modules/runtime/stages.js +1 -1
- package/dist/modules/ssr/Component.d.ts +5 -1
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/prepareRender.js +38 -10
- package/dist/modules/ssr/registerExternalPlugins.d.ts +5 -1
- package/dist/modules/ssr/registerExternalPlugins.js +6 -2
- package/package.json +9 -4
- package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
- package/skills/plitzi-authoring/SKILL.md +77 -74
- package/skills/plitzi-authoring/recipes/carousel.ts +114 -0
- package/skills/plitzi-authoring/recipes/detail-page.ts +70 -0
- package/skills/plitzi-authoring/recipes/embed-and-svg.ts +48 -0
- package/skills/plitzi-authoring/recipes/every-few-seconds.ts +119 -0
- package/skills/plitzi-authoring/recipes/feature-flag.ts +24 -0
- package/skills/plitzi-authoring/recipes/filter-a-list.ts +63 -0
- package/skills/plitzi-authoring/recipes/forms-and-modals.ts +64 -0
- package/skills/plitzi-authoring/recipes/from-tailwind.ts +52 -0
- package/skills/plitzi-authoring/recipes/link-to-a-section.ts +35 -0
- package/skills/plitzi-authoring/recipes/link-with-data.ts +40 -0
- package/skills/plitzi-authoring/recipes/marquee.ts +57 -0
- package/skills/plitzi-authoring/recipes/plugin.ts +53 -0
- package/skills/plitzi-authoring/recipes/scroll-a-row.ts +79 -0
- package/skills/plitzi-authoring/recipes/show-data.ts +73 -0
- package/skills/plitzi-authoring/recipes/style-a-page.ts +45 -0
- package/skills/plitzi-authoring/recipes/typed-data.ts +64 -0
- package/skills/plitzi-authoring/recipes/usable-without-sight.ts +39 -0
- package/skills/plitzi-authoring/reference/accessibility.md +2 -2
- package/skills/plitzi-authoring/reference/authoring-errors.md +195 -53
- package/skills/plitzi-authoring/reference/colours-and-motion.md +63 -0
- package/skills/plitzi-authoring/reference/components.md +83 -0
- package/skills/plitzi-authoring/reference/data-and-visibility.md +29 -29
- package/skills/plitzi-authoring/reference/efficiency.md +53 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +39 -24
- package/skills/plitzi-authoring/reference/feature-flags.md +78 -0
- package/skills/plitzi-authoring/reference/flows.md +26 -37
- package/skills/plitzi-authoring/reference/kept-state.md +28 -0
- package/skills/plitzi-authoring/reference/layouts.md +20 -3
- package/skills/plitzi-authoring/reference/lists.md +45 -16
- package/skills/plitzi-authoring/reference/plugins.md +27 -0
- package/skills/plitzi-authoring/reference/realtime.md +35 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +8 -3
- package/skills/plitzi-authoring/reference/{templates-and-export.md → snippets-and-export.md} +15 -8
- package/skills/plitzi-authoring/reference/structure.md +29 -20
- package/skills/plitzi-authoring/reference/tailwind.md +19 -0
- package/skills/plitzi-authoring/reference/templates.md +6 -5
- package/skills/plitzi-authoring/reference/testing.md +11 -0
- package/skills/plitzi-authoring/reference/typed-sources.md +20 -0
- package/skills/plitzi-authoring/reference/validation.md +6 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
|
+
import { createImageProxy } from "../../../modules/images/imageProxy.js";
|
|
3
|
+
import { loadSharpTransform } from "../../../modules/images/transform.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { IMAGE_PATH } from "@plitzi/sdk-shared/helpers/images";
|
|
6
|
+
//#region src/core/http/stages/images.ts
|
|
7
|
+
/** Where pictures are resized, when this server resizes them: published to the SDK, which builds every `srcset` on it. */
|
|
8
|
+
var imagesPathOf = (config) => config.images && config.images.domains.length > 0 ? IMAGE_PATH : void 0;
|
|
9
|
+
/** One proxy per configuration, made on the first request: `sharp` is looked for once, and said once when missing. */
|
|
10
|
+
var proxies = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
var proxyFor = (images) => {
|
|
12
|
+
const known = proxies.get(images);
|
|
13
|
+
if (known) return known;
|
|
14
|
+
const made = loadSharpTransform().then((transform) => {
|
|
15
|
+
if (!transform) serverLog.warn("IMAGES", "sharp is not installed: pictures are passed through and kept, not resized (npm i sharp).");
|
|
16
|
+
return createImageProxy({
|
|
17
|
+
domains: images.domains,
|
|
18
|
+
cacheDir: path.resolve(images.cacheDir ?? ".plitzi/images"),
|
|
19
|
+
fetch: globalThis.fetch,
|
|
20
|
+
transform
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
proxies.set(images, made);
|
|
24
|
+
return made;
|
|
25
|
+
};
|
|
26
|
+
/** `/_plitzi/img?url=…&w=…` — a remote picture at one of the widths the SDK asks for. See `SSRImagesConfig`. */
|
|
27
|
+
var imagesStage = async (ctx) => {
|
|
28
|
+
const { images } = ctx.config;
|
|
29
|
+
if (!images || imagesPathOf(ctx.config) === void 0 || ctx.req.path !== IMAGE_PATH) return false;
|
|
30
|
+
if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") {
|
|
31
|
+
ctx.res.setStatus(405);
|
|
32
|
+
ctx.res.setHeader("Allow", "GET, HEAD");
|
|
33
|
+
ctx.res.send("Method not allowed");
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const answer = await (await proxyFor(images))({
|
|
37
|
+
url: ctx.req.query.url,
|
|
38
|
+
width: ctx.req.query.w,
|
|
39
|
+
accept: ctx.req.headers.accept ?? ""
|
|
40
|
+
});
|
|
41
|
+
ctx.res.setStatus(answer.status);
|
|
42
|
+
for (const [name, value] of Object.entries(answer.headers)) ctx.res.setHeader(name, value);
|
|
43
|
+
ctx.res.send(answer.body);
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { imagesPathOf, imagesStage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/core/server/fleet/channel.ts
|
|
2
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
3
3
|
/** The channel is shared with `node:cluster` and anything else the deployment sends: only these are ours. */
|
|
4
4
|
var isFleetMessage = (value) => {
|
|
5
5
|
if (!isRecord(value)) return false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
2
|
import { broadcast, onBroadcast } from "./link.js";
|
|
3
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
3
4
|
//#region src/core/server/fleet/relay.ts
|
|
4
5
|
/**
|
|
5
6
|
* `server.cache` and `server.plugins` in a fleet, whose workers each keep their own: what one process is told, every
|
|
@@ -10,7 +11,6 @@ import { broadcast, onBroadcast } from "./link.js";
|
|
|
10
11
|
* Code that runs in every process (a pub/sub subscription made where the server is created) is told N times; what
|
|
11
12
|
* is relayed is safe to apply again, and cheap: it only drops entries from memory.
|
|
12
13
|
*/
|
|
13
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
14
14
|
var PLUGIN_ACTIONS = [
|
|
15
15
|
"copy",
|
|
16
16
|
"compile",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectorLookupsOf } from "../configSeam.js";
|
|
1
2
|
import { handleActionCall } from "../../modules/actions/transport/callHandler.js";
|
|
2
3
|
import { handleActionCatalog } from "../../modules/actions/transport/catalogHandler.js";
|
|
3
4
|
import { handleActionWebhook } from "../../modules/actions/transport/webhookHandler.js";
|
|
@@ -38,8 +39,8 @@ var lineageOf = (req) => {
|
|
|
38
39
|
var actionStage = async (ctx) => {
|
|
39
40
|
const { config, req, actions } = ctx;
|
|
40
41
|
const actionPath = config.action?.path ?? "/_action";
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
42
|
+
const { connectors } = config;
|
|
43
|
+
if (!connectors && !actions) return false;
|
|
43
44
|
if (actions && req.method === "GET" && req.path === `${actionPath}/catalog`) {
|
|
44
45
|
ctx.operation = "action:catalog";
|
|
45
46
|
if (!req.ctx.user) {
|
|
@@ -104,7 +105,7 @@ var actionStage = async (ctx) => {
|
|
|
104
105
|
});
|
|
105
106
|
return true;
|
|
106
107
|
}
|
|
107
|
-
if (addressesAction || !
|
|
108
|
+
if (addressesAction || !connectors) {
|
|
108
109
|
ctx.operation = "action";
|
|
109
110
|
ctx.res.setStatus(404);
|
|
110
111
|
ctx.res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
@@ -115,7 +116,7 @@ var actionStage = async (ctx) => {
|
|
|
115
116
|
return true;
|
|
116
117
|
}
|
|
117
118
|
ctx.operation = "action";
|
|
118
|
-
await handleAction(req, ctx.res, config,
|
|
119
|
+
await handleAction(req, ctx.res, config, connectorLookupsOf(connectors));
|
|
119
120
|
return true;
|
|
120
121
|
};
|
|
121
122
|
//#endregion
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SSRContext, Stage } from '../http/types';
|
|
2
2
|
/**
|
|
3
|
-
* The space's functions answering HTTP under `/
|
|
3
|
+
* The space's functions answering HTTP under `/fn/`: a request there that a declared route answers is run
|
|
4
4
|
* by it — the deployment's own routes first, then the space's, in the sandbox — and anything else goes on to the page
|
|
5
|
-
* server, which answers as it would have. `/
|
|
5
|
+
* server, which answers as it would have. `/fn` is never a page: its slug is refused, so the path is the functions'.
|
|
6
6
|
*
|
|
7
7
|
* After the auth chain, so `ctx.user` is who the session says; the session itself never reaches the code.
|
|
8
8
|
*/
|
|
@@ -2,9 +2,9 @@ import { clientIp, readRawBytes, requestOrigin } from "../requestParser.js";
|
|
|
2
2
|
import { FUNCTION_ROUTES_PREFIX } from "@plitzi/sdk-shared/actions";
|
|
3
3
|
//#region src/core/services/functionRoutes.ts
|
|
4
4
|
/**
|
|
5
|
-
* The space's functions answering HTTP under `/
|
|
5
|
+
* The space's functions answering HTTP under `/fn/`: a request there that a declared route answers is run
|
|
6
6
|
* by it — the deployment's own routes first, then the space's, in the sandbox — and anything else goes on to the page
|
|
7
|
-
* server, which answers as it would have. `/
|
|
7
|
+
* server, which answers as it would have. `/fn` is never a page: its slug is refused, so the path is the functions'.
|
|
8
8
|
*
|
|
9
9
|
* After the auth chain, so `ctx.user` is who the session says; the session itself never reaches the code.
|
|
10
10
|
*/
|
|
@@ -2,6 +2,7 @@ import { actionStage } from "./action.js";
|
|
|
2
2
|
import { functionRoutesStage } from "./functionRoutes.js";
|
|
3
3
|
import { realtimeStage } from "./realtime.js";
|
|
4
4
|
import { rscStage } from "./rsc.js";
|
|
5
|
+
import { imagesStage } from "../http/stages/images.js";
|
|
5
6
|
import { notFoundStage, ssrStage } from "./ssr.js";
|
|
6
7
|
import { authRoutesStages } from "../http/stages/authRoutes.js";
|
|
7
8
|
import { fontAssetsStage } from "../http/stages/fontAssets.js";
|
|
@@ -17,6 +18,7 @@ var buildPagePipeline = (services, extensions = {}) => {
|
|
|
17
18
|
wellKnownStage,
|
|
18
19
|
pluginAssetsStage,
|
|
19
20
|
fontAssetsStage,
|
|
21
|
+
imagesStage,
|
|
20
22
|
...authRoutesStages,
|
|
21
23
|
configStaticStage,
|
|
22
24
|
sdkAssetsStage
|
package/dist/functions-api.d.ts
CHANGED
|
@@ -118,7 +118,7 @@ export declare type FunctionFetchInit = {
|
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* An HTTP endpoint of the space, under `/
|
|
121
|
+
* An HTTP endpoint of the space, under `/fn/`: a web-standard handler, the shape every edge runtime runs. The request
|
|
122
122
|
* carries no credentials — `ctx.user` says who is asking — and the response sets no cookies: the host's are the
|
|
123
123
|
* platform's.
|
|
124
124
|
*/
|
|
@@ -136,8 +136,10 @@ export declare type FunctionsDefinition = {
|
|
|
136
136
|
hosts?: string[];
|
|
137
137
|
};
|
|
138
138
|
tasks?: FunctionTask<never>[];
|
|
139
|
-
/** `'GET /board-assets/:board/:asset'` → a handler, served at `/
|
|
139
|
+
/** `'GET /board-assets/:board/:asset'` → a handler, served at `/fn/board-assets/…`. */
|
|
140
140
|
routes?: Record<string, FunctionRoute>;
|
|
141
|
+
/** More CPU or time than an invocation gets by default, for every task and route — a task may ask for its own. */
|
|
142
|
+
limits?: FunctionTimeLimits;
|
|
141
143
|
};
|
|
142
144
|
|
|
143
145
|
/**
|
|
@@ -151,9 +153,28 @@ export declare type FunctionTask<T extends Record<string, unknown> = Record<stri
|
|
|
151
153
|
title: string;
|
|
152
154
|
description?: string;
|
|
153
155
|
params: Record<keyof T, InteractionCallbackParam<T>>;
|
|
156
|
+
/**
|
|
157
|
+
* More CPU or time than an invocation gets by default — `{ cpuMs: 1000 }` for a task that reshapes a big feed — up to
|
|
158
|
+
* what the server allows. Over the functions' own `limits`, for this task.
|
|
159
|
+
*/
|
|
160
|
+
limits?: FunctionTimeLimits;
|
|
154
161
|
run: (params: T, ctx: FunctionContext) => unknown;
|
|
155
162
|
};
|
|
156
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A space's own functions, as everything outside the runner sees them: the builder, the CLI, the MCP and the
|
|
166
|
+
* platform that stores them. The contract the code itself is written against is `@plitzi/sdk-server/functions`.
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* What a task — or every task and route, declared once for all of them — asks to be given beyond an invocation's
|
|
170
|
+
* default: CPU time, awaits not counted, and wall time, from the moment it starts. Never more than the server allows;
|
|
171
|
+
* asking for more is a problem when the functions are saved.
|
|
172
|
+
*/
|
|
173
|
+
declare type FunctionTimeLimits = {
|
|
174
|
+
cpuMs?: number;
|
|
175
|
+
wallMs?: number;
|
|
176
|
+
};
|
|
177
|
+
|
|
157
178
|
/**
|
|
158
179
|
* Who started the run, as a space's own code may know them: everything a function decides by — who, their roles and
|
|
159
180
|
* permissions — and never their session. A token inside the sandbox is a token a function could send anywhere.
|
package/dist/functions.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export { ActionRefusal, defineFunctions } from './modules/functions/contract';
|
|
7
7
|
export type { FunctionContext, FunctionFetch, FunctionFetchInit, FunctionRoute, FunctionRouteContext, FunctionsDefinition, FunctionTask, FunctionUser } from './modules/functions/contract';
|
|
8
8
|
export type { FunctionsConfig, FunctionUsageRecord } from './modules/functions/config';
|
|
9
|
-
export { DEFAULT_FUNCTION_LIMITS } from './modules/functions/config';
|
|
9
|
+
export { DEFAULT_FUNCTION_CEILINGS, DEFAULT_FUNCTION_LIMITS } from './modules/functions/config';
|
|
10
10
|
/**
|
|
11
11
|
* What the platform keeps of a space's functions, and what it hands a runner: the built bundle and what it declared
|
|
12
12
|
* when it was saved (`prepareFunctions` on the actions module is the one way to make one).
|
package/dist/functions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionRefusal } from "./modules/actions/runtime/errors.js";
|
|
2
|
-
import { DEFAULT_FUNCTION_LIMITS } from "./modules/functions/config.js";
|
|
2
|
+
import { DEFAULT_FUNCTION_CEILINGS, DEFAULT_FUNCTION_LIMITS } from "./modules/functions/config.js";
|
|
3
3
|
import { defineFunctions } from "./modules/functions/contract.js";
|
|
4
4
|
import { TRY_STEP, functionTryEntry } from "./modules/functions/tryEntry.js";
|
|
5
|
-
export { ActionRefusal, DEFAULT_FUNCTION_LIMITS, TRY_STEP, defineFunctions, functionTryEntry };
|
|
5
|
+
export { ActionRefusal, DEFAULT_FUNCTION_CEILINGS, DEFAULT_FUNCTION_LIMITS, TRY_STEP, defineFunctions, functionTryEntry };
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** What a cache key reads off a request: where it is, and — from the resolved deployment — what the flags are at. */
|
|
2
|
+
type CacheKeyRequest = {
|
|
3
|
+
hostname: string;
|
|
4
|
+
path: string;
|
|
5
|
+
search: string;
|
|
6
|
+
ctx?: {
|
|
7
|
+
spaceDeployment?: {
|
|
8
|
+
flagsVersion?: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
};
|
|
1
12
|
/**
|
|
2
13
|
* The key a rendered document is cached under.
|
|
3
14
|
*
|
|
@@ -14,13 +25,17 @@
|
|
|
14
25
|
* values, so a page drawn with one visitor's toolbar must not be served to the next. Keyed by its raw value — this
|
|
15
26
|
* key does not know which keys a space declares, and a space that declares none has no such cookie to split on.
|
|
16
27
|
*
|
|
28
|
+
* So are the feature flags a tester forced from the dev tools, on a page that authorizes debugging: drawn with them,
|
|
29
|
+
* the page is not everybody's, and a tester holding the cookie must never be handed a page drawn without it. Keyed
|
|
30
|
+
* by its raw value, like the kept state.
|
|
31
|
+
*
|
|
32
|
+
* So is what the space's flags are at (`flagsVersion` on the resolved deployment), for a deployment that changes them
|
|
33
|
+
* apart from its revisions: a flag turned in production must reach the next visitor, while the revision is the same.
|
|
34
|
+
*
|
|
17
35
|
* Read from the request here rather than handed in, so a call site cannot key the page without it. Only those
|
|
18
36
|
* cookies are read: keying the whole header would split the cache on every analytics cookie a visitor carries.
|
|
19
37
|
*/
|
|
20
|
-
export declare const buildHtmlCacheKey: (accessToken: string | undefined, spaceId: number | string | null, environment: string, revision: number, req: {
|
|
21
|
-
hostname: string;
|
|
22
|
-
path: string;
|
|
23
|
-
search: string;
|
|
38
|
+
export declare const buildHtmlCacheKey: (accessToken: string | undefined, spaceId: number | string | null, environment: string, revision: number, req: CacheKeyRequest & {
|
|
24
39
|
headers: {
|
|
25
40
|
cookie?: string;
|
|
26
41
|
host?: string;
|
|
@@ -36,9 +51,7 @@ export declare const readHtmlCacheKey: (key: string) => {
|
|
|
36
51
|
environment: string;
|
|
37
52
|
hostname: string;
|
|
38
53
|
};
|
|
39
|
-
|
|
40
|
-
export declare const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
search: string;
|
|
44
|
-
}) => string;
|
|
54
|
+
/** The space as read for a render: its revision, and what its flags are at when they change apart from it. */
|
|
55
|
+
export declare const buildOfflineDataCacheKey: (spaceId: number, environment: string, revision: number, flagsVersion: string | undefined) => string;
|
|
56
|
+
export declare const buildRscCacheKey: (spaceId: number, environment: string, revision: number, userId: string | number | undefined, idsParam: string | undefined, req: CacheKeyRequest) => string;
|
|
57
|
+
export {};
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { ssrPaintedCookieName } from "../paintedCookie.js";
|
|
2
2
|
import { readCookie } from "../readCookie.js";
|
|
3
|
+
import { flagsCookieName } from "@plitzi/sdk-shared/flags";
|
|
3
4
|
import { debugCookieName } from "@plitzi/sdk-shared/devTools";
|
|
4
5
|
import { themeFromCookies } from "@plitzi/sdk-shared/theme";
|
|
5
6
|
//#region src/helpers/cache/keys.ts
|
|
7
|
+
var flagsVersionOf = (req) => req.ctx?.spaceDeployment?.flagsVersion ?? "";
|
|
6
8
|
/** The fields of an HTML key, in the order they are joined. */
|
|
7
9
|
var HTML_KEY_FIELDS = [
|
|
8
10
|
"accessToken",
|
|
9
11
|
"spaceId",
|
|
10
12
|
"environment",
|
|
11
13
|
"revision",
|
|
14
|
+
"flags",
|
|
12
15
|
"theme",
|
|
13
16
|
"painted",
|
|
14
17
|
"debugHidden",
|
|
18
|
+
"forcedFlags",
|
|
15
19
|
"hostname",
|
|
16
20
|
"path",
|
|
17
21
|
"search"
|
|
@@ -32,6 +36,13 @@ var HTML_KEY_FIELDS = [
|
|
|
32
36
|
* values, so a page drawn with one visitor's toolbar must not be served to the next. Keyed by its raw value — this
|
|
33
37
|
* key does not know which keys a space declares, and a space that declares none has no such cookie to split on.
|
|
34
38
|
*
|
|
39
|
+
* So are the feature flags a tester forced from the dev tools, on a page that authorizes debugging: drawn with them,
|
|
40
|
+
* the page is not everybody's, and a tester holding the cookie must never be handed a page drawn without it. Keyed
|
|
41
|
+
* by its raw value, like the kept state.
|
|
42
|
+
*
|
|
43
|
+
* So is what the space's flags are at (`flagsVersion` on the resolved deployment), for a deployment that changes them
|
|
44
|
+
* apart from its revisions: a flag turned in production must reach the next visitor, while the revision is the same.
|
|
45
|
+
*
|
|
35
46
|
* Read from the request here rather than handed in, so a call site cannot key the page without it. Only those
|
|
36
47
|
* cookies are read: keying the whole header would split the cache on every analytics cookie a visitor carries.
|
|
37
48
|
*/
|
|
@@ -41,9 +52,11 @@ var buildHtmlCacheKey = (accessToken = "anonymous", spaceId, environment, revisi
|
|
|
41
52
|
spaceId: String(spaceId ?? 1),
|
|
42
53
|
environment,
|
|
43
54
|
revision: String(revision),
|
|
55
|
+
flags: flagsVersionOf(req),
|
|
44
56
|
theme: themeFromCookies(req.headers.cookie) ?? "",
|
|
45
57
|
painted: readCookie(req.headers.cookie, ssrPaintedCookieName(req.headers.host)) ?? "",
|
|
46
58
|
debugHidden: readCookie(req.headers.cookie, debugCookieName(req.headers.host)) === "false" ? "debug-off" : "",
|
|
59
|
+
forcedFlags: readCookie(req.headers.cookie, flagsCookieName(req.headers.host)) ?? "",
|
|
47
60
|
hostname: req.hostname,
|
|
48
61
|
path: req.path,
|
|
49
62
|
search: req.search
|
|
@@ -64,7 +77,8 @@ var readHtmlCacheKey = (key) => {
|
|
|
64
77
|
hostname: at("hostname")
|
|
65
78
|
};
|
|
66
79
|
};
|
|
67
|
-
|
|
68
|
-
var
|
|
80
|
+
/** The space as read for a render: its revision, and what its flags are at when they change apart from it. */
|
|
81
|
+
var buildOfflineDataCacheKey = (spaceId, environment, revision, flagsVersion) => `${spaceId}|${environment}|${revision}|${flagsVersion ?? ""}`;
|
|
82
|
+
var buildRscCacheKey = (spaceId, environment, revision, userId, idsParam, req) => `${spaceId}\0${environment}\0${revision}\0${flagsVersionOf(req)}\0${userId ?? "anon"}\0${idsParam ?? ""}\0${req.hostname}\0${req.path}\0${req.search}`;
|
|
69
83
|
//#endregion
|
|
70
84
|
export { buildHtmlCacheKey, buildOfflineDataCacheKey, buildRscCacheKey, readHtmlCacheKey };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Environment, FlagOverrides, SSRServerConfig } from '@plitzi/sdk-shared';
|
|
2
|
+
/** The deployment's own layer for one space: `config.flags`, asked per space when it is a function. */
|
|
3
|
+
export declare const serverFlagsFor: (config: Pick<SSRServerConfig, "flags">, space: {
|
|
4
|
+
spaceId: number;
|
|
5
|
+
environment: Environment;
|
|
6
|
+
}) => Record<string, boolean> | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The flags a tester forced from the dev tools, for a request allowed to debug — undefined for everybody else, and for
|
|
9
|
+
* a tester who forced nothing. Authorization is asked only when there is a cookie to honour, because answering it can
|
|
10
|
+
* mean reading the space.
|
|
11
|
+
*/
|
|
12
|
+
export declare const forcedFlagsFor: (req: {
|
|
13
|
+
headers: {
|
|
14
|
+
cookie?: string;
|
|
15
|
+
host?: string;
|
|
16
|
+
};
|
|
17
|
+
}, authorized: boolean | (() => Promise<boolean>)) => Promise<Record<string, boolean> | undefined>;
|
|
18
|
+
/** Both layers this server answers for, for one request. */
|
|
19
|
+
export declare const requestFlagOverrides: (config: Pick<SSRServerConfig, "flags">, req: {
|
|
20
|
+
headers: {
|
|
21
|
+
cookie?: string;
|
|
22
|
+
host?: string;
|
|
23
|
+
};
|
|
24
|
+
}, space: {
|
|
25
|
+
spaceId: number;
|
|
26
|
+
environment: Environment;
|
|
27
|
+
}, authorized: boolean | (() => Promise<boolean>)) => Promise<FlagOverrides>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { forcedFlagsFromCookies } from "@plitzi/sdk-shared/flags";
|
|
2
|
+
//#region src/helpers/flagOverrides.ts
|
|
3
|
+
/** The deployment's own layer for one space: `config.flags`, asked per space when it is a function. */
|
|
4
|
+
var serverFlagsFor = (config, space) => typeof config.flags === "function" ? config.flags(space) : config.flags;
|
|
5
|
+
/**
|
|
6
|
+
* The flags a tester forced from the dev tools, for a request allowed to debug — undefined for everybody else, and for
|
|
7
|
+
* a tester who forced nothing. Authorization is asked only when there is a cookie to honour, because answering it can
|
|
8
|
+
* mean reading the space.
|
|
9
|
+
*/
|
|
10
|
+
var forcedFlagsFor = async (req, authorized) => {
|
|
11
|
+
const forced = forcedFlagsFromCookies(req.headers.cookie, req.headers.host);
|
|
12
|
+
if (Object.keys(forced).length === 0) return;
|
|
13
|
+
return (typeof authorized === "boolean" ? authorized : await authorized()) ? forced : void 0;
|
|
14
|
+
};
|
|
15
|
+
/** Both layers this server answers for, for one request. */
|
|
16
|
+
var requestFlagOverrides = async (config, req, space, authorized) => {
|
|
17
|
+
const server = serverFlagsFor(config, space);
|
|
18
|
+
const qa = await forcedFlagsFor(req, authorized);
|
|
19
|
+
return {
|
|
20
|
+
...server ? { server } : {},
|
|
21
|
+
...qa ? { qa } : {}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { forcedFlagsFor, requestFlagOverrides, serverFlagsFor };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { createServer, resolveServices } from './core/createServer';
|
|
2
2
|
export { closeOnSignals } from './core/server/closeOnSignals';
|
|
3
3
|
export { loadFunctions, readFunctionsDir } from './modules/functions/load';
|
|
4
|
+
export { freePort } from './core/freePort';
|
|
4
5
|
export { registerHealthCheck, buildHealthPayload } from './core/health';
|
|
5
6
|
export { consoleLogger, isLogged, logLevelOf, renderLogEvent, serverLog } from './helpers/serverLog';
|
|
6
7
|
export type { HealthCheckApp, HealthIdentity } from './core/health';
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { buildHealthPayload, registerHealthCheck } from "./core/health.js";
|
|
|
10
10
|
import { createServer } from "./core/createServer.js";
|
|
11
11
|
import { closeOnSignals } from "./core/server/closeOnSignals.js";
|
|
12
12
|
import { loadFunctions, readFunctionsDir } from "./modules/functions/load.js";
|
|
13
|
+
import { freePort } from "./core/freePort.js";
|
|
13
14
|
import { createCloudAdapters } from "./adapters/cloudAdapters.js";
|
|
14
15
|
import { createJsonAdapters } from "./adapters/jsonAdapters.js";
|
|
15
16
|
import { createAuthAdapters } from "./adapters/authAdapters.js";
|
|
@@ -18,4 +19,4 @@ import { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildc
|
|
|
18
19
|
import { createSpaceAdapters } from "./adapters/space/index.js";
|
|
19
20
|
import { createLocalFontStore, hostedPathsOf } from "./core/fontStore.js";
|
|
20
21
|
import { createRedisPubSub } from "./modules/realtime/redisPubSub.js";
|
|
21
|
-
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, closeOnSignals, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createMemoryPubSub, createRedisPubSub, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, hostedPathsOf, isLogged, loadFunctions, logLevelOf, presentsCredential, readFunctionsDir, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, serverLog, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
22
|
+
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, closeOnSignals, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createMemoryPubSub, createRedisPubSub, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, freePort, hostedPathsOf, isLogged, loadFunctions, logLevelOf, presentsCredential, readFunctionsDir, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, serverLog, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
@@ -66,4 +66,4 @@ export type { ActionJobs, JobWorker, JobWorkerOptions, Scheduler, SchedulerOptio
|
|
|
66
66
|
export type { ActiveRun, RunGuards } from './runtime/guards';
|
|
67
67
|
export type { ActionTaskDescriptor } from './taskCatalog';
|
|
68
68
|
export type { TaskRegistryOptions } from './tasks/registry';
|
|
69
|
-
export type { ActionCredential, ActionDbDriver, ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionJobsConfig, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './types';
|
|
69
|
+
export type { ActionCredential, ActionDbDriver, ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionJobsConfig, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskOrigin, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './types';
|
|
@@ -12,6 +12,7 @@ import { namespaceKv, spaceKvPatterns } from "./runtime/namespaceKv.js";
|
|
|
12
12
|
import "./runtime/precheck.js";
|
|
13
13
|
import { createActionRunner } from "./runtime/runAction.js";
|
|
14
14
|
import { createTaskRegistry } from "./tasks/registry.js";
|
|
15
|
+
import { functionCeilings } from "../functions/config.js";
|
|
15
16
|
import { readManifest } from "../functions/manifest.js";
|
|
16
17
|
import { createRoutes } from "../functions/routes.js";
|
|
17
18
|
import { createSpaceRegistries, prepareFunctions } from "../functions/space.js";
|
|
@@ -72,12 +73,12 @@ var createActionsModule = (given) => {
|
|
|
72
73
|
routeFor: routes.routeFor,
|
|
73
74
|
prepareFunctions: (source) => {
|
|
74
75
|
const runner = config.functions?.runner;
|
|
75
|
-
return runner ? prepareFunctions(source, runner, reserved) : Promise.resolve({
|
|
76
|
+
return runner ? prepareFunctions(source, runner, reserved, functionCeilings(config.functions?.limits)) : Promise.resolve({
|
|
76
77
|
ok: false,
|
|
77
78
|
problems: [{ message: "This server runs no space functions" }]
|
|
78
79
|
});
|
|
79
80
|
},
|
|
80
|
-
readManifest: (declared) => readManifest(declared, reserved),
|
|
81
|
+
readManifest: (declared) => readManifest(declared, reserved, functionCeilings(config.functions?.limits)),
|
|
81
82
|
guards,
|
|
82
83
|
kv: (spaceId) => namespaceKv(kv, spaceId),
|
|
83
84
|
limitsFor
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { actionLookupsOf, dbDriversOf, functionsConfigOf } from "../../core/configSeam.js";
|
|
1
2
|
import { createActionsModule } from "./index.js";
|
|
2
3
|
import { realtimeModuleFor } from "../realtime/index.js";
|
|
3
4
|
//#region src/modules/actions/moduleFor.ts
|
|
@@ -19,8 +20,9 @@ var actionsModuleFor = (config) => {
|
|
|
19
20
|
const existing = modules.get(config);
|
|
20
21
|
if (existing) return existing;
|
|
21
22
|
const module = createActionsModule({
|
|
22
|
-
lookups,
|
|
23
|
-
|
|
23
|
+
lookups: actionLookupsOf(lookups),
|
|
24
|
+
flags: config.flags,
|
|
25
|
+
functions: functionsConfigOf(config.functions),
|
|
24
26
|
limits: config.action?.limits,
|
|
25
27
|
concurrency: config.action?.concurrency,
|
|
26
28
|
kv: config.action?.kv,
|
|
@@ -28,7 +30,7 @@ var actionsModuleFor = (config) => {
|
|
|
28
30
|
rateLimit: config.action?.rateLimit,
|
|
29
31
|
idempotency: config.action?.idempotency,
|
|
30
32
|
signingSecret: config.action?.signingSecret,
|
|
31
|
-
dbDrivers: config.action?.dbDrivers,
|
|
33
|
+
dbDrivers: dbDriversOf(config.action?.dbDrivers),
|
|
32
34
|
email: config.action?.email,
|
|
33
35
|
onRun: config.action?.onRun,
|
|
34
36
|
realtime: realtimeModuleFor(config)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ActionRunRequest, ActionsConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a flow names `flags` anywhere — a template, a `when` rule's field. Only those are worth a lookup: most flows
|
|
4
|
+
* read no flag, and asking for the space's flags on every run of every one of them would be a read per click.
|
|
5
|
+
*
|
|
6
|
+
* Over the serialized document, so a template in any step's params counts without knowing every step's shape. A word
|
|
7
|
+
* `flags` that is not the source costs one lookup that was not needed, never a flow without its flags. Kept per
|
|
8
|
+
* document, which the lookups hand out once per version.
|
|
9
|
+
*/
|
|
10
|
+
export declare const readsFlags: (document: object) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The space's flags as this run sees them: what the space declares at the run's revision, matched against the run's
|
|
13
|
+
* environment and visitor, then the server's own layer and a tester's forced one — the same order a page resolves
|
|
14
|
+
* them in. A run has no URL, so a rule on the host or the route does not match here.
|
|
15
|
+
*/
|
|
16
|
+
export declare const flagsForRun: (config: Pick<ActionsConfig, "lookups" | "flags">, request: Pick<ActionRunRequest, "entry" | "spaceId" | "environment" | "at" | "user" | "forcedFlags">) => Promise<Record<string, boolean>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
|
+
import { serverFlagsFor } from "../../../helpers/flagOverrides.js";
|
|
3
|
+
import { flagUserFromSSR, flagValues, resolveFlags, undeclaredFlagOverrides } from "@plitzi/sdk-shared/flags";
|
|
4
|
+
//#region src/modules/actions/runtime/flags.ts
|
|
5
|
+
var reading = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
/**
|
|
7
|
+
* Whether a flow names `flags` anywhere — a template, a `when` rule's field. Only those are worth a lookup: most flows
|
|
8
|
+
* read no flag, and asking for the space's flags on every run of every one of them would be a read per click.
|
|
9
|
+
*
|
|
10
|
+
* Over the serialized document, so a template in any step's params counts without knowing every step's shape. A word
|
|
11
|
+
* `flags` that is not the source costs one lookup that was not needed, never a flow without its flags. Kept per
|
|
12
|
+
* document, which the lookups hand out once per version.
|
|
13
|
+
*/
|
|
14
|
+
var readsFlags = (document) => {
|
|
15
|
+
const known = reading.get(document);
|
|
16
|
+
if (known !== void 0) return known;
|
|
17
|
+
const reads = /\bflags\b/.test(JSON.stringify(document));
|
|
18
|
+
reading.set(document, reads);
|
|
19
|
+
return reads;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The space's flags as this run sees them: what the space declares at the run's revision, matched against the run's
|
|
23
|
+
* environment and visitor, then the server's own layer and a tester's forced one — the same order a page resolves
|
|
24
|
+
* them in. A run has no URL, so a rule on the host or the route does not match here.
|
|
25
|
+
*/
|
|
26
|
+
var flagsForRun = async (config, request) => {
|
|
27
|
+
if (!readsFlags(request.entry.document)) return {};
|
|
28
|
+
const { getFlags } = config.lookups;
|
|
29
|
+
if (!getFlags) {
|
|
30
|
+
serverLog.warn("Actions", `Action "${request.entry.id}" reads flags, but this deployment's action lookups have no getFlags: it sees none.`);
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const declared = await getFlags(request.spaceId, request.at);
|
|
34
|
+
const { spaceId, environment, user } = request;
|
|
35
|
+
const overrides = {
|
|
36
|
+
server: serverFlagsFor(config, {
|
|
37
|
+
spaceId,
|
|
38
|
+
environment
|
|
39
|
+
}),
|
|
40
|
+
qa: request.forcedFlags
|
|
41
|
+
};
|
|
42
|
+
undeclaredFlagOverrides(declared, { server: overrides.server }).forEach(({ name }) => serverLog.warn("Actions", `The server sets the flag "${name}", which space ${spaceId} does not declare.`));
|
|
43
|
+
return flagValues(resolveFlags(declared, {
|
|
44
|
+
environment,
|
|
45
|
+
user: flagUserFromSSR(user)
|
|
46
|
+
}, overrides));
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { flagsForRun, readsFlags };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/modules/actions/runtime/kvList.ts
|
|
2
3
|
/**
|
|
3
4
|
* The most a list holds. A list is one value of the store, read and written whole, so it is for what a page shows at
|
|
@@ -11,7 +12,6 @@ var MAX_LIST_ENTRIES = 500;
|
|
|
11
12
|
var MAX_LIST_BYTES = 128e3;
|
|
12
13
|
var ID = /^[A-Za-z0-9:_.@-]{1,128}$/;
|
|
13
14
|
var listIdProblem = (id) => typeof id === "string" && ID.test(id) ? void 0 : "A list entry’s id is 1-128 of A-Z a-z 0-9 : _ . @ -";
|
|
14
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
15
15
|
var isEntry = (value) => isRecord(value) && typeof value.id === "string" && typeof value.score === "number";
|
|
16
16
|
/** The entries a stored list holds — none for a key that holds nothing, or something that is not a list. */
|
|
17
17
|
var parseList = (raw) => {
|
|
@@ -72,6 +72,7 @@ var createActionResolver = (lookups, module) => {
|
|
|
72
72
|
values
|
|
73
73
|
]);
|
|
74
74
|
const callerId = user ? `user:${user.id}` : "render";
|
|
75
|
+
let ownRunId;
|
|
75
76
|
const startRun = async () => {
|
|
76
77
|
/**
|
|
77
78
|
* A key of its own per render, so two visitors are never each other's duplicate.
|
|
@@ -90,6 +91,7 @@ var createActionResolver = (lookups, module) => {
|
|
|
90
91
|
kind: "render",
|
|
91
92
|
ttlMs: module.limitsFor(entry.document).timeoutMs
|
|
92
93
|
});
|
|
94
|
+
ownRunId = run.runId;
|
|
93
95
|
/**
|
|
94
96
|
* The render giving up ends the run, not just the wait for it.
|
|
95
97
|
*
|
|
@@ -157,13 +159,35 @@ var createActionResolver = (lookups, module) => {
|
|
|
157
159
|
await module.guards.end(run);
|
|
158
160
|
}
|
|
159
161
|
};
|
|
162
|
+
let told = false;
|
|
160
163
|
const record = (summary) => {
|
|
161
|
-
(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
if (!told) {
|
|
165
|
+
told = true;
|
|
166
|
+
(req.ctx.actionRuns ??= []).push({
|
|
167
|
+
...summary,
|
|
168
|
+
elementId: element.id
|
|
169
|
+
});
|
|
170
|
+
}
|
|
165
171
|
};
|
|
166
172
|
/**
|
|
173
|
+
* The page stopped waiting — the section's budget ran out — and is answered without it: said NOW, while the page
|
|
174
|
+
* still has the request's runs to send. The run itself ends a moment later, aborted, when the page has gone; told
|
|
175
|
+
* then, a debugger would never hear of the one run it needed to — the slow one.
|
|
176
|
+
*/
|
|
177
|
+
const askedAt = Date.now();
|
|
178
|
+
const stopWaiting = onAbort(signal, () => {
|
|
179
|
+
record({
|
|
180
|
+
actionId: entry.id,
|
|
181
|
+
runId: ownRunId ?? `render:${randomUUID()}`,
|
|
182
|
+
trigger: "render",
|
|
183
|
+
status: "aborted",
|
|
184
|
+
startedAt: askedAt,
|
|
185
|
+
endedAt: Date.now(),
|
|
186
|
+
steps: [],
|
|
187
|
+
error: "Still running when the page stopped waiting for it: it took longer than a section is given"
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
/**
|
|
167
191
|
* Everyone asking the same question at once gets one run, and its answer.
|
|
168
192
|
*
|
|
169
193
|
* Not an optimisation — it is what makes a page survive being read. Without it, a thousand visitors of one URL
|
|
@@ -177,6 +201,8 @@ var createActionResolver = (lookups, module) => {
|
|
|
177
201
|
} catch (error) {
|
|
178
202
|
if (error instanceof RenderRunFailure) record(error.summary);
|
|
179
203
|
throw error;
|
|
204
|
+
} finally {
|
|
205
|
+
stopWaiting();
|
|
180
206
|
}
|
|
181
207
|
};
|
|
182
208
|
};
|
|
@@ -5,6 +5,7 @@ import { createKvStore } from "./kvStore.js";
|
|
|
5
5
|
import { resolveLimits } from "./limits.js";
|
|
6
6
|
import { createMemoryKv } from "./memoryKv.js";
|
|
7
7
|
import { createEmailSender } from "./email.js";
|
|
8
|
+
import { flagsForRun } from "./flags.js";
|
|
8
9
|
import { createRedactor, projectUser } from "./scope.js";
|
|
9
10
|
import { precheckRun } from "./precheck.js";
|
|
10
11
|
import { createSigning } from "./signing.js";
|
|
@@ -240,6 +241,7 @@ var createActionRunner = (config, registryFor, baseFetch = fetch) => {
|
|
|
240
241
|
const scope = {
|
|
241
242
|
input: values,
|
|
242
243
|
user: projectUser(request.user),
|
|
244
|
+
flags: await flagsForRun(config, request),
|
|
243
245
|
spaceId: request.spaceId,
|
|
244
246
|
environment: request.environment,
|
|
245
247
|
trigger: request.trigger,
|