@plitzi/sdk-server 0.37.7 → 0.37.9
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 +255 -0
- package/README.md +7 -2
- package/dist/actions.d.ts +1 -1
- package/dist/adapters/cloudAdapters.js +4 -30
- package/dist/core/services/functionRoutes.d.ts +2 -2
- package/dist/core/services/functionRoutes.js +2 -2
- package/dist/functions-api.d.ts +23 -2
- package/dist/functions.d.ts +1 -1
- package/dist/functions.js +2 -2
- package/dist/modules/actions/index.d.ts +1 -1
- package/dist/modules/actions/index.js +3 -2
- package/dist/modules/actions/runtime/renderResolver.js +30 -4
- 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/types.d.ts +6 -0
- 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/manifest.d.ts +5 -3
- package/dist/modules/functions/manifest.js +36 -5
- package/dist/modules/functions/routes.d.ts +1 -1
- package/dist/modules/functions/routes.js +3 -3
- package/dist/modules/functions/space.d.ts +7 -5
- package/dist/modules/functions/space.js +17 -10
- 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/prepareRender.js +2 -2
- package/dist/modules/ssr/registerExternalPlugins.d.ts +5 -1
- package/dist/modules/ssr/registerExternalPlugins.js +6 -2
- package/package.json +3 -3
- package/skills/plitzi-authoring/SKILL.md +11 -8
- package/skills/plitzi-authoring/reference/components.md +75 -0
- package/skills/plitzi-authoring/reference/flows.md +1 -1
- package/skills/plitzi-authoring/reference/plugins.md +12 -0
- package/skills/plitzi-authoring/reference/templates-and-export.md +6 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FunctionsDefinition } from './contract';
|
|
2
2
|
import { FunctionLimits, FunctionRunner, FunctionUsage } from './protocol';
|
|
3
|
+
import { FunctionTimeLimits } from '@plitzi/sdk-shared';
|
|
3
4
|
/** One invocation of a space's task, as metering sees it. */
|
|
4
5
|
export type FunctionUsageRecord = {
|
|
5
6
|
spaceId: number;
|
|
@@ -19,7 +20,11 @@ export type FunctionsConfig = {
|
|
|
19
20
|
* functions here — their tasks are not offered and none of their code runs.
|
|
20
21
|
*/
|
|
21
22
|
runner?: FunctionRunner;
|
|
22
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The most one invocation may spend on this deployment ({@link DEFAULT_FUNCTION_CEILINGS} for each unset): what a
|
|
25
|
+
* task may ask for in CPU and time, and what every invocation gets of the rest. A space's plan may allow less, never
|
|
26
|
+
* more.
|
|
27
|
+
*/
|
|
23
28
|
limits?: Partial<FunctionLimits>;
|
|
24
29
|
/**
|
|
25
30
|
* Asked before each invocation: why this space may not run code right now — its account spent its CPU for the minute,
|
|
@@ -29,6 +34,17 @@ export type FunctionsConfig = {
|
|
|
29
34
|
/** Told what each invocation spent, however it ended: what `admit` budgets by, and what a deployment meters. */
|
|
30
35
|
onUsage?: (record: FunctionUsageRecord) => void;
|
|
31
36
|
};
|
|
37
|
+
/** What an invocation gets unless its task asks for more CPU or time. */
|
|
32
38
|
export declare const DEFAULT_FUNCTION_LIMITS: FunctionLimits;
|
|
33
|
-
/**
|
|
34
|
-
|
|
39
|
+
/**
|
|
40
|
+
* The most a deployment lets one invocation have, unless it says otherwise: two seconds of CPU and half a minute of
|
|
41
|
+
* time for a task that asks; the rest as every invocation gets it.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DEFAULT_FUNCTION_CEILINGS: FunctionLimits;
|
|
44
|
+
/** A deployment's ceilings, each unset one the default's. */
|
|
45
|
+
export declare const functionCeilings: (ceilings?: Partial<FunctionLimits>) => FunctionLimits;
|
|
46
|
+
/**
|
|
47
|
+
* What one invocation of a space may spend: the CPU and time its task asked for — the default's when it asked for none
|
|
48
|
+
* — and the rest as the deployment gives it; never above the space's plan, nor above the deployment's ceilings.
|
|
49
|
+
*/
|
|
50
|
+
export declare const functionLimitsFor: (ceilings?: Partial<FunctionLimits>, space?: Partial<FunctionLimits>, asked?: FunctionTimeLimits) => FunctionLimits;
|
|
@@ -1,24 +1,40 @@
|
|
|
1
|
+
import { DEFAULT_FUNCTION_TIME_LIMITS } from "@plitzi/sdk-shared/actions";
|
|
1
2
|
//#region src/modules/functions/config.ts
|
|
3
|
+
/** What an invocation gets unless its task asks for more CPU or time. */
|
|
2
4
|
var DEFAULT_FUNCTION_LIMITS = {
|
|
3
|
-
|
|
4
|
-
wallMs: 1e4,
|
|
5
|
+
...DEFAULT_FUNCTION_TIME_LIMITS,
|
|
5
6
|
memoryMb: 64,
|
|
6
7
|
outputBytes: 1e6,
|
|
7
8
|
calls: 100
|
|
8
9
|
};
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The most a deployment lets one invocation have, unless it says otherwise: two seconds of CPU and half a minute of
|
|
12
|
+
* time for a task that asks; the rest as every invocation gets it.
|
|
13
|
+
*/
|
|
14
|
+
var DEFAULT_FUNCTION_CEILINGS = {
|
|
15
|
+
...DEFAULT_FUNCTION_LIMITS,
|
|
16
|
+
cpuMs: 2e3,
|
|
17
|
+
wallMs: 3e4
|
|
18
|
+
};
|
|
19
|
+
/** A deployment's ceilings, each unset one the default's. */
|
|
20
|
+
var functionCeilings = (ceilings = {}) => ({
|
|
21
|
+
...DEFAULT_FUNCTION_CEILINGS,
|
|
22
|
+
...ceilings
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* What one invocation of a space may spend: the CPU and time its task asked for — the default's when it asked for none
|
|
26
|
+
* — and the rest as the deployment gives it; never above the space's plan, nor above the deployment's ceilings.
|
|
27
|
+
*/
|
|
28
|
+
var functionLimitsFor = (ceilings = {}, space = {}, asked = {}) => {
|
|
29
|
+
const ceiling = functionCeilings(ceilings);
|
|
30
|
+
const capOf = (key) => Math.min(space[key] ?? ceiling[key], ceiling[key]);
|
|
15
31
|
return {
|
|
16
|
-
cpuMs: Math.min(
|
|
17
|
-
wallMs: Math.min(
|
|
18
|
-
memoryMb:
|
|
19
|
-
outputBytes:
|
|
20
|
-
calls:
|
|
32
|
+
cpuMs: Math.min(asked.cpuMs ?? DEFAULT_FUNCTION_LIMITS.cpuMs, capOf("cpuMs")),
|
|
33
|
+
wallMs: Math.min(asked.wallMs ?? DEFAULT_FUNCTION_LIMITS.wallMs, capOf("wallMs")),
|
|
34
|
+
memoryMb: capOf("memoryMb"),
|
|
35
|
+
outputBytes: capOf("outputBytes"),
|
|
36
|
+
calls: capOf("calls")
|
|
21
37
|
};
|
|
22
38
|
};
|
|
23
39
|
//#endregion
|
|
24
|
-
export { DEFAULT_FUNCTION_LIMITS, functionLimitsFor };
|
|
40
|
+
export { DEFAULT_FUNCTION_CEILINGS, DEFAULT_FUNCTION_LIMITS, functionCeilings, functionLimitsFor };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RateCount, RateLimit } from '../actions/runtime/rateLimit';
|
|
2
2
|
import { ActionKvStore } from '../actions/types';
|
|
3
|
-
import { Environment, InteractionCallbackParam } from '@plitzi/sdk-shared';
|
|
3
|
+
import { Environment, FunctionTimeLimits, InteractionCallbackParam } from '@plitzi/sdk-shared';
|
|
4
4
|
/**
|
|
5
5
|
* Who started the run, as a space's own code may know them: everything a function decides by — who, their roles and
|
|
6
6
|
* permissions — and never their session. A token inside the sandbox is a token a function could send anywhere.
|
|
@@ -85,6 +85,11 @@ export type FunctionTask<T extends Record<string, unknown> = Record<string, unkn
|
|
|
85
85
|
title: string;
|
|
86
86
|
description?: string;
|
|
87
87
|
params: Record<keyof T, InteractionCallbackParam<T>>;
|
|
88
|
+
/**
|
|
89
|
+
* More CPU or time than an invocation gets by default — `{ cpuMs: 1000 }` for a task that reshapes a big feed — up to
|
|
90
|
+
* what the server allows. Over the functions' own `limits`, for this task.
|
|
91
|
+
*/
|
|
92
|
+
limits?: FunctionTimeLimits;
|
|
88
93
|
run: (params: T, ctx: FunctionContext) => unknown;
|
|
89
94
|
};
|
|
90
95
|
/** What a route's handler gets besides the request: the context, and the `:params` its key named, by name. */
|
|
@@ -92,7 +97,7 @@ export type FunctionRouteContext = FunctionContext & {
|
|
|
92
97
|
params: Record<string, string>;
|
|
93
98
|
};
|
|
94
99
|
/**
|
|
95
|
-
* An HTTP endpoint of the space, under `/
|
|
100
|
+
* An HTTP endpoint of the space, under `/fn/`: a web-standard handler, the shape every edge runtime runs. The request
|
|
96
101
|
* carries no credentials — `ctx.user` says who is asking — and the response sets no cookies: the host's are the
|
|
97
102
|
* platform's.
|
|
98
103
|
*/
|
|
@@ -104,8 +109,10 @@ export type FunctionsDefinition = {
|
|
|
104
109
|
hosts?: string[];
|
|
105
110
|
};
|
|
106
111
|
tasks?: FunctionTask<never>[];
|
|
107
|
-
/** `'GET /board-assets/:board/:asset'` → a handler, served at `/
|
|
112
|
+
/** `'GET /board-assets/:board/:asset'` → a handler, served at `/fn/board-assets/…`. */
|
|
108
113
|
routes?: Record<string, FunctionRoute>;
|
|
114
|
+
/** More CPU or time than an invocation gets by default, for every task and route — a task may ask for its own. */
|
|
115
|
+
limits?: FunctionTimeLimits;
|
|
109
116
|
};
|
|
110
117
|
/**
|
|
111
118
|
* What a function throws to refuse, with a reason written for whoever asked — a wrong password, a board that is
|
|
@@ -14,9 +14,11 @@ var describeFunctions = (definition) => {
|
|
|
14
14
|
action: task.action,
|
|
15
15
|
title: task.title,
|
|
16
16
|
description: task.description,
|
|
17
|
-
params: task.params
|
|
17
|
+
params: task.params,
|
|
18
|
+
limits: task.limits
|
|
18
19
|
} : {}),
|
|
19
|
-
routes: isRecord(found.routes) ? Object.keys(found.routes) : []
|
|
20
|
+
routes: isRecord(found.routes) ? Object.keys(found.routes) : [],
|
|
21
|
+
limits: found.limits
|
|
20
22
|
};
|
|
21
23
|
};
|
|
22
24
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FunctionLimits } from './protocol';
|
|
1
2
|
import { FunctionsManifest } from '@plitzi/sdk-shared';
|
|
2
3
|
export declare const ROUTE_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3
4
|
export type RouteMethod = (typeof ROUTE_METHODS)[number];
|
|
@@ -6,7 +7,7 @@ export type RouteKey = {
|
|
|
6
7
|
segments: string[];
|
|
7
8
|
};
|
|
8
9
|
/**
|
|
9
|
-
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/
|
|
10
|
+
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/fn/`. Anything else — a query, a
|
|
10
11
|
* wildcard, `..` — is not a key, so what a space may answer is only ever a path it spelled out.
|
|
11
12
|
*/
|
|
12
13
|
export declare const parseRouteKey: (key: string) => RouteKey | undefined;
|
|
@@ -19,6 +20,7 @@ export type ManifestReading = {
|
|
|
19
20
|
* afterwards, because what is stored is only ever what passed it.
|
|
20
21
|
*
|
|
21
22
|
* `reserved` is every namespace this deployment's own tasks use: a space's task may take none of them, so a step that
|
|
22
|
-
* names a platform task always runs the platform's.
|
|
23
|
+
* names a platform task always runs the platform's. `ceilings` is the most this deployment gives one invocation: what a
|
|
24
|
+
* task may ask for.
|
|
23
25
|
*/
|
|
24
|
-
export declare const readManifest: (value: unknown, reserved: ReadonlySet<string
|
|
26
|
+
export declare const readManifest: (value: unknown, reserved: ReadonlySet<string>, ceilings: FunctionLimits) => ManifestReading;
|
|
@@ -10,7 +10,7 @@ var ROUTE_METHODS = [
|
|
|
10
10
|
var SEGMENT = /^(:[a-zA-Z][a-zA-Z0-9]*|[A-Za-z0-9._~-]+)$/;
|
|
11
11
|
var isRouteMethod = (method) => ROUTE_METHODS.some((known) => known === method);
|
|
12
12
|
/**
|
|
13
|
-
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/
|
|
13
|
+
* A route key, `'<METHOD> /<path>'`: literal segments and `:params`, served under `/fn/`. Anything else — a query, a
|
|
14
14
|
* wildcard, `..` — is not a key, so what a space may answer is only ever a path it spelled out.
|
|
15
15
|
*/
|
|
16
16
|
var parseRouteKey = (key) => {
|
|
@@ -83,14 +83,41 @@ var paramOf = (value) => {
|
|
|
83
83
|
default: return `has a type the builder cannot draw ("${String(value.type)}"): text, textarea, codemirror-text, codemirror-json, boolean, elements or select`;
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
|
+
/** What each asked limit is, in words a problem is written in. */
|
|
87
|
+
var LIMIT_WORDS = {
|
|
88
|
+
cpuMs: "of CPU",
|
|
89
|
+
wallMs: "to finish"
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* What `where` asks for beyond the default, read: whole milliseconds above nothing — and never more than `ceilings`, which
|
|
93
|
+
* is said rather than quietly cut down, so the author knows what the code will get.
|
|
94
|
+
*/
|
|
95
|
+
var limitsOf = (value, where, ceilings, problems) => {
|
|
96
|
+
if (value === void 0) return;
|
|
97
|
+
if (!isRecord(value)) {
|
|
98
|
+
problems.push(`${where}: limits is not an object like { cpuMs: 1000, wallMs: 20000 }`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const limits = {};
|
|
102
|
+
["cpuMs", "wallMs"].forEach((key) => {
|
|
103
|
+
const asked = value[key];
|
|
104
|
+
if (asked === void 0) return;
|
|
105
|
+
if (typeof asked !== "number" || !Number.isInteger(asked) || asked <= 0) problems.push(`${where}: limits.${key} is not a whole number of milliseconds above 0`);
|
|
106
|
+
else if (asked > ceilings[key]) problems.push(`${where}: asks for ${String(asked)} ms ${LIMIT_WORDS[key]}, and this server allows at most ${String(ceilings[key])} ms`);
|
|
107
|
+
else limits[key] = asked;
|
|
108
|
+
});
|
|
109
|
+
Object.keys(value).filter((key) => key !== "cpuMs" && key !== "wallMs").forEach((key) => problems.push(`${where}: limits.${key} is not a limit a function asks for (cpuMs, wallMs)`));
|
|
110
|
+
return Object.keys(limits).length ? limits : void 0;
|
|
111
|
+
};
|
|
86
112
|
/**
|
|
87
113
|
* Reads what a bundle said it declares, and every rule it must meet — the one validator, at save and nowhere else
|
|
88
114
|
* afterwards, because what is stored is only ever what passed it.
|
|
89
115
|
*
|
|
90
116
|
* `reserved` is every namespace this deployment's own tasks use: a space's task may take none of them, so a step that
|
|
91
|
-
* names a platform task always runs the platform's.
|
|
117
|
+
* names a platform task always runs the platform's. `ceilings` is the most this deployment gives one invocation: what a
|
|
118
|
+
* task may ask for.
|
|
92
119
|
*/
|
|
93
|
-
var readManifest = (value, reserved) => {
|
|
120
|
+
var readManifest = (value, reserved, ceilings) => {
|
|
94
121
|
const problems = [];
|
|
95
122
|
const raw = isRecord(value) ? value : {};
|
|
96
123
|
const hosts = Array.isArray(raw.hosts) ? raw.hosts.filter((host) => typeof host === "string") : [];
|
|
@@ -130,12 +157,14 @@ var readManifest = (value, reserved) => {
|
|
|
130
157
|
else params[key] = read;
|
|
131
158
|
});
|
|
132
159
|
const description = stringOf(task.description);
|
|
160
|
+
const limits = limitsOf(task.limits, where, ceilings, problems);
|
|
133
161
|
tasks.push({
|
|
134
162
|
namespace,
|
|
135
163
|
action,
|
|
136
164
|
title,
|
|
137
165
|
...description ? { description } : {},
|
|
138
|
-
params
|
|
166
|
+
params,
|
|
167
|
+
...limits ? { limits } : {}
|
|
139
168
|
});
|
|
140
169
|
});
|
|
141
170
|
const routes = Array.isArray(raw.routes) ? raw.routes.filter((route) => typeof route === "string") : [];
|
|
@@ -150,11 +179,13 @@ var readManifest = (value, reserved) => {
|
|
|
150
179
|
if (routed.has(shape)) problems.push(`Route "${route}" answers the same requests as another one`);
|
|
151
180
|
routed.add(shape);
|
|
152
181
|
});
|
|
182
|
+
const limits = limitsOf(raw.limits, "The functions", ceilings, problems);
|
|
153
183
|
return {
|
|
154
184
|
manifest: {
|
|
155
185
|
hosts,
|
|
156
186
|
tasks,
|
|
157
|
-
routes
|
|
187
|
+
routes,
|
|
188
|
+
...limits ? { limits } : {}
|
|
158
189
|
},
|
|
159
190
|
problems
|
|
160
191
|
};
|
|
@@ -31,6 +31,6 @@ export type RouteSources = {
|
|
|
31
31
|
* each run with the same context a task of theirs gets, `trigger: 'route'`.
|
|
32
32
|
*/
|
|
33
33
|
export declare const createRoutes: ({ config, getFunctions, taskContext }: RouteSources) => {
|
|
34
|
-
/** The route that answers `method path` (the path after `/
|
|
34
|
+
/** The route that answers `method path` (the path after `/fn`), for this visit — or none. */
|
|
35
35
|
routeFor: (visit: RouteVisit, method: string, path: string) => Promise<RouteHandler | undefined>;
|
|
36
36
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { serverLog } from "../../helpers/serverLog.js";
|
|
2
2
|
import { ActionRefusal } from "../actions/runtime/errors.js";
|
|
3
3
|
import { functionContextFor } from "./context.js";
|
|
4
|
+
import { functionLimitsFor } from "./config.js";
|
|
4
5
|
import { parseRouteKey } from "./manifest.js";
|
|
5
6
|
import { answerCall, readCall, responseOf, wireRequestOf, wireResponseFrom } from "./capabilities.js";
|
|
6
|
-
import { functionLimitsFor } from "./config.js";
|
|
7
7
|
import { FunctionFailure } from "./protocol.js";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
//#region src/modules/functions/routes.ts
|
|
@@ -151,7 +151,7 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
|
|
|
151
151
|
...fnCtx.user ? { user: fnCtx.user } : {}
|
|
152
152
|
}
|
|
153
153
|
},
|
|
154
|
-
limits: functionLimitsFor(config.limits, functions.limits),
|
|
154
|
+
limits: functionLimitsFor(config.limits, functions.limits, functions.manifest.limits),
|
|
155
155
|
answer: (call) => answerCall(fnCtx, readCall(call)),
|
|
156
156
|
signal: visit.signal,
|
|
157
157
|
onUsage: (usage) => {
|
|
@@ -174,7 +174,7 @@ var createRoutes = ({ config, getFunctions, taskContext }) => {
|
|
|
174
174
|
};
|
|
175
175
|
};
|
|
176
176
|
return {
|
|
177
|
-
/** The route that answers `method path` (the path after `/
|
|
177
|
+
/** The route that answers `method path` (the path after `/fn`), for this visit — or none. */
|
|
178
178
|
routeFor: async (visit, method, path) => nativeRoute(visit, method, path) ?? await spaceRoute(visit, method, path) };
|
|
179
179
|
};
|
|
180
180
|
//#endregion
|
|
@@ -2,12 +2,13 @@ import { FunctionsSource } from './build';
|
|
|
2
2
|
import { FunctionsConfig } from './config';
|
|
3
3
|
import { FunctionsBundle, FunctionLimits, FunctionRunner, SpaceFunctions } from './protocol';
|
|
4
4
|
import { ActionTask, ActionTaskRegistry } from '../actions/types';
|
|
5
|
-
import { FunctionsManifest, FunctionsProblem } from '@plitzi/sdk-shared';
|
|
5
|
+
import { FunctionsManifest, FunctionsProblem, FunctionTimeLimits } from '@plitzi/sdk-shared';
|
|
6
6
|
/**
|
|
7
7
|
* A space's tasks as the registry holds them — each run by the runner, answering the code's calls with the run's own
|
|
8
|
-
* {@link FunctionContext}: the same one native functions get, built by the same function.
|
|
8
|
+
* {@link FunctionContext}: the same one native functions get, built by the same function. Each is given what
|
|
9
|
+
* `limitsFor` answers for what it asked: its own limits, over its functions'.
|
|
9
10
|
*/
|
|
10
|
-
export declare const spaceTasks: (functions: SpaceFunctions, runner: FunctionRunner,
|
|
11
|
+
export declare const spaceTasks: (functions: SpaceFunctions, runner: FunctionRunner, limitsFor: (asked: FunctionTimeLimits) => FunctionLimits, { admit, onUsage }?: Pick<FunctionsConfig, "admit" | "onUsage">) => ActionTask<Record<string, unknown>>[];
|
|
11
12
|
export type SpaceRegistries = {
|
|
12
13
|
/** The tasks one run of that space can use: the deployment's, and its own functions' when it has any. */
|
|
13
14
|
registryFor: (functions: SpaceFunctions | undefined) => ActionTaskRegistry;
|
|
@@ -35,6 +36,7 @@ export type PreparedFunctions = {
|
|
|
35
36
|
};
|
|
36
37
|
/**
|
|
37
38
|
* A space's source made ready to save: built, read by the runner, and checked — the only way a space's functions are
|
|
38
|
-
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use
|
|
39
|
+
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use;
|
|
40
|
+
* `ceilings`, the most it gives one invocation — what a task may ask for.
|
|
39
41
|
*/
|
|
40
|
-
export declare const prepareFunctions: (source: FunctionsSource, runner: FunctionRunner, reserved: ReadonlySet<string
|
|
42
|
+
export declare const prepareFunctions: (source: FunctionsSource, runner: FunctionRunner, reserved: ReadonlySet<string>, ceilings: FunctionLimits) => Promise<PreparedFunctions>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ActionRefusal } from "../actions/runtime/errors.js";
|
|
2
2
|
import { functionContextFor } from "./context.js";
|
|
3
3
|
import { taskName } from "../actions/tasks/registry.js";
|
|
4
|
+
import { functionLimitsFor } from "./config.js";
|
|
4
5
|
import { readManifest } from "./manifest.js";
|
|
5
6
|
import { answerCall, readCall } from "./capabilities.js";
|
|
6
|
-
import { functionLimitsFor } from "./config.js";
|
|
7
7
|
import { FunctionFailure } from "./protocol.js";
|
|
8
8
|
import { FunctionsBuildError, buildFunctions } from "./build.js";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
@@ -18,10 +18,15 @@ var invocationContextOf = ({ spaceId, environment, runId, trigger, callerId, use
|
|
|
18
18
|
});
|
|
19
19
|
/**
|
|
20
20
|
* A space's tasks as the registry holds them — each run by the runner, answering the code's calls with the run's own
|
|
21
|
-
* {@link FunctionContext}: the same one native functions get, built by the same function.
|
|
21
|
+
* {@link FunctionContext}: the same one native functions get, built by the same function. Each is given what
|
|
22
|
+
* `limitsFor` answers for what it asked: its own limits, over its functions'.
|
|
22
23
|
*/
|
|
23
|
-
var spaceTasks = (functions, runner,
|
|
24
|
+
var spaceTasks = (functions, runner, limitsFor, { admit, onUsage } = {}) => functions.manifest.tasks.map((task) => {
|
|
24
25
|
const name = taskName(task);
|
|
26
|
+
const limits = limitsFor({
|
|
27
|
+
...functions.manifest.limits,
|
|
28
|
+
...task.limits
|
|
29
|
+
});
|
|
25
30
|
return {
|
|
26
31
|
namespace: task.namespace,
|
|
27
32
|
action: task.action,
|
|
@@ -73,7 +78,8 @@ var composeRegistry = (base, tasks) => {
|
|
|
73
78
|
const name = taskName(task);
|
|
74
79
|
if (!base.get(name)) own.set(name, {
|
|
75
80
|
...task,
|
|
76
|
-
name
|
|
81
|
+
name,
|
|
82
|
+
origin: "space"
|
|
77
83
|
});
|
|
78
84
|
});
|
|
79
85
|
return {
|
|
@@ -91,11 +97,11 @@ var createSpaceRegistries = (base, config = {}) => {
|
|
|
91
97
|
return { registryFor: (functions) => {
|
|
92
98
|
const runner = functions?.runner ?? config.runner;
|
|
93
99
|
if (!functions || !runner) return base;
|
|
94
|
-
const
|
|
95
|
-
const key = `${functions.bundle.id}:${JSON.stringify(limits)}`;
|
|
100
|
+
const key = `${functions.bundle.id}:${JSON.stringify(functions.limits ?? {})}`;
|
|
96
101
|
const found = cache.get(key);
|
|
97
102
|
if (found) return found;
|
|
98
|
-
const
|
|
103
|
+
const limitsFor = (asked) => functionLimitsFor(config.limits, functions.limits, asked);
|
|
104
|
+
const registry = composeRegistry(base, spaceTasks(functions, runner, limitsFor, config));
|
|
99
105
|
cache.set(key, registry);
|
|
100
106
|
if (cache.size > REGISTRY_CACHE) {
|
|
101
107
|
const oldest = cache.keys().next().value;
|
|
@@ -114,9 +120,10 @@ var functionsInHand = ({ bundle, manifest }) => ({
|
|
|
114
120
|
});
|
|
115
121
|
/**
|
|
116
122
|
* A space's source made ready to save: built, read by the runner, and checked — the only way a space's functions are
|
|
117
|
-
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use
|
|
123
|
+
* stored, so what is stored always passed every rule. `reserved` is every namespace the deployment's own tasks use;
|
|
124
|
+
* `ceilings`, the most it gives one invocation — what a task may ask for.
|
|
118
125
|
*/
|
|
119
|
-
var prepareFunctions = async (source, runner, reserved) => {
|
|
126
|
+
var prepareFunctions = async (source, runner, reserved, ceilings) => {
|
|
120
127
|
let code;
|
|
121
128
|
try {
|
|
122
129
|
({code} = await buildFunctions(source));
|
|
@@ -143,7 +150,7 @@ var prepareFunctions = async (source, runner, reserved) => {
|
|
|
143
150
|
}]
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
|
-
const { manifest, problems } = readManifest(declared, reserved);
|
|
153
|
+
const { manifest, problems } = readManifest(declared, reserved, ceilings);
|
|
147
154
|
return problems.length ? {
|
|
148
155
|
ok: false,
|
|
149
156
|
problems: problems.map((message) => ({
|
|
@@ -14,7 +14,7 @@ export type SpaceRuntimeContext = {
|
|
|
14
14
|
export type SpaceRuntimeEndpoint = (request: Request) => Response | Promise<Response>;
|
|
15
15
|
/** What a started runtime is made of. */
|
|
16
16
|
export type SpaceRuntimeParts = {
|
|
17
|
-
/** Its tasks and `/
|
|
17
|
+
/** Its tasks and `/fn/` routes, run with a `ctx` that is the platform's — the space's `kv`, its channels, its key. */
|
|
18
18
|
functions?: FunctionsDefinition;
|
|
19
19
|
/** Paths it answers itself, each with everything beneath it (`/mcp` answers `/mcp/…`) — see {@link endpointProblem}. */
|
|
20
20
|
endpoints?: Record<string, SpaceRuntimeEndpoint>;
|
|
@@ -45,6 +45,6 @@ export type RuntimeProxyConfig = {
|
|
|
45
45
|
* The page server forwarding a space's endpoints to its runtime: the request as it came, streamed both ways — an agent's
|
|
46
46
|
* event stream included — without the visitor's cookies or `Authorization`, and with who asked in `X-Forwarded-*`.
|
|
47
47
|
*
|
|
48
|
-
* A data stage: after the auth chain, so the space is known, and before the space's `/
|
|
48
|
+
* A data stage: after the auth chain, so the space is known, and before the space's `/fn/` routes and its pages.
|
|
49
49
|
*/
|
|
50
50
|
export declare const createRuntimeProxyStage: ({ lookup, onForward }: RuntimeProxyConfig) => Stage<SSRContext>;
|
|
@@ -55,7 +55,7 @@ var NOT_ANSWERED = /* @__PURE__ */ new Set([...HOP_BY_HOP, "set-cookie"]);
|
|
|
55
55
|
* The page server forwarding a space's endpoints to its runtime: the request as it came, streamed both ways — an agent's
|
|
56
56
|
* event stream included — without the visitor's cookies or `Authorization`, and with who asked in `X-Forwarded-*`.
|
|
57
57
|
*
|
|
58
|
-
* A data stage: after the auth chain, so the space is known, and before the space's `/
|
|
58
|
+
* A data stage: after the auth chain, so the space is known, and before the space's `/fn/` routes and its pages.
|
|
59
59
|
*/
|
|
60
60
|
var createRuntimeProxyStage = ({ lookup, onForward }) => async (ctx) => {
|
|
61
61
|
const { spaceId, environment = "main" } = ctx.req.ctx.spaceDeployment ?? {};
|
|
@@ -143,11 +143,11 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
143
143
|
const key = pluginManager.ensure(pluginName, pluginSource);
|
|
144
144
|
if (!pluginBaseNames.has(pluginName)) dynamicNames.push(key);
|
|
145
145
|
}
|
|
146
|
-
const
|
|
146
|
+
const externalNames = config.autoLoadSchemaPlugins !== false ? await m("extPlugins", () => registerExternalPlugins(pluginManager, offlineData, { allowPrivateHosts: config.allowPrivatePluginHosts === true }, pluginBaseNames)) : [];
|
|
147
147
|
const allPluginNames = [
|
|
148
148
|
...pluginNames,
|
|
149
149
|
...dynamicNames,
|
|
150
|
-
...
|
|
150
|
+
...externalNames
|
|
151
151
|
];
|
|
152
152
|
const entries = allPluginNames.length > 0 ? await pluginManager.getEntries(allPluginNames) : [];
|
|
153
153
|
const pluginComponents = await m("plugins", () => loadPluginComponents(entries, pluginManager.getComponents()));
|
|
@@ -7,6 +7,10 @@ type ManifestReach = {
|
|
|
7
7
|
/**
|
|
8
8
|
* Reads plugins listed in offlineData.plugins, fetches their manifests, downloads and caches
|
|
9
9
|
* JS/CSS via the PluginManager, and returns the effective plugin keys to pass to getEntries().
|
|
10
|
+
*
|
|
11
|
+
* `registered`: the types this deployment registers itself — a project's own build of a plugin the space also lists
|
|
12
|
+
* on its CDN. Those are never looked for elsewhere: the deployment's copy is the one rendered, and asking the CDN for a
|
|
13
|
+
* manifest nobody will use is a request, and a warning, for nothing.
|
|
10
14
|
*/
|
|
11
|
-
export declare const registerExternalPlugins: (pluginManager: PluginManager, offlineData: OfflineDataRaw | undefined, reach?: ManifestReach) => Promise<string[]>;
|
|
15
|
+
export declare const registerExternalPlugins: (pluginManager: PluginManager, offlineData: OfflineDataRaw | undefined, reach?: ManifestReach, registered?: ReadonlySet<string>) => Promise<string[]>;
|
|
12
16
|
export {};
|
|
@@ -97,9 +97,13 @@ var registerPlugin = async (pluginManager, plugin, reach) => {
|
|
|
97
97
|
/**
|
|
98
98
|
* Reads plugins listed in offlineData.plugins, fetches their manifests, downloads and caches
|
|
99
99
|
* JS/CSS via the PluginManager, and returns the effective plugin keys to pass to getEntries().
|
|
100
|
+
*
|
|
101
|
+
* `registered`: the types this deployment registers itself — a project's own build of a plugin the space also lists
|
|
102
|
+
* on its CDN. Those are never looked for elsewhere: the deployment's copy is the one rendered, and asking the CDN for a
|
|
103
|
+
* manifest nobody will use is a request, and a warning, for nothing.
|
|
100
104
|
*/
|
|
101
|
-
var registerExternalPlugins = async (pluginManager, offlineData, reach = { allowPrivateHosts: false }) => {
|
|
102
|
-
const plugins = offlineData?.plugins;
|
|
105
|
+
var registerExternalPlugins = async (pluginManager, offlineData, reach = { allowPrivateHosts: false }, registered = /* @__PURE__ */ new Set()) => {
|
|
106
|
+
const plugins = offlineData?.plugins?.filter((plugin) => !registered.has(plugin.type));
|
|
103
107
|
if (!plugins || plugins.length === 0) return [];
|
|
104
108
|
return (await Promise.all(plugins.map((p) => registerPlugin(pluginManager, p, reach)))).filter((k) => k !== null);
|
|
105
109
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plitzi/sdk-server",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.9",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"type": "module",
|
|
80
80
|
"sideEffects": false,
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@plitzi/plitzi-sdk": "0.37.
|
|
83
|
-
"@plitzi/sdk-shared": "0.37.
|
|
82
|
+
"@plitzi/plitzi-sdk": "0.37.9",
|
|
83
|
+
"@plitzi/sdk-shared": "0.37.9",
|
|
84
84
|
"ejs": "^6.0.1",
|
|
85
85
|
"esbuild": "^0.28.2",
|
|
86
86
|
"jsonwebtoken": "^9.0.3",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plitzi-authoring
|
|
3
3
|
description: >-
|
|
4
|
-
Write or change a Plitzi space or template in TypeScript with @plitzi/sdk-authoring — pages, layouts,
|
|
5
|
-
CSS classes, data bindings, providers and flows — instead of hand-writing schema JSON. Use whenever the task is to
|
|
4
|
+
Write or change a Plitzi space or template in TypeScript with @plitzi/sdk-authoring — pages, layouts, components,
|
|
5
|
+
elements, CSS classes, data bindings, providers and flows — instead of hand-writing schema JSON. Use whenever the task is to
|
|
6
6
|
create, extend, restyle or fix a space: adding a page or a section, sharing a header across pages, binding an
|
|
7
7
|
element to data, showing or hiding something, wiring what happens on click, or turning an exported JSON into code.
|
|
8
8
|
---
|
|
@@ -52,10 +52,12 @@ type, the exported catalogues answer faster (`elementDefaultAttributes`, `elemen
|
|
|
52
52
|
1. **Name what is referred to.** Give an `id` to every element a binding, a flow or a test addresses. Ids are ONE
|
|
53
53
|
namespace for the whole space — layouts and every page share it — so an element built by a helper that runs more
|
|
54
54
|
than once gets its id prefixed by what it is for: `` `${pageId}-foot` ``.
|
|
55
|
-
2. **Share with classes and
|
|
56
|
-
several pages — a header, a sidebar, a footer — is a **layout** the pages name, written once. A
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
2. **Share with classes, layouts and components, never with copies.** A look used twice is a `styles()` class.
|
|
56
|
+
Chrome shown on several pages — a header, a sidebar, a footer — is a **layout** the pages name, written once. A block
|
|
57
|
+
placed many times with different content — a product card, a testimonial — is a **component** placed with
|
|
58
|
+
`component(id, { props })`. A menu, a card grid, a list of steps is DATA mapped to elements
|
|
59
|
+
(`entries.map(entry => link(…))`), not a block pasted per item. See [layouts and duplication](reference/layouts.md)
|
|
60
|
+
and [components](reference/components.md).
|
|
59
61
|
3. **One element, one base selector.** An element takes a shared `class` OR its own `css`, never both — authoring
|
|
60
62
|
refuses the pair. A look that never changes is its own class.
|
|
61
63
|
4. **Visible by default; decide which way the logic flips.** An element is on screen unless something hides it. When
|
|
@@ -158,16 +160,17 @@ link({ href: 'about' }); link({ href: '/games/nebula', mode: 'internal' }); link
|
|
|
158
160
|
| --- | --- |
|
|
159
161
|
| [elements-and-styles.md](reference/elements-and-styles.md) | Any element or CSS: factories, fields, classes, states, variants, tokens, fonts, lists, links |
|
|
160
162
|
| [layouts.md](reference/layouts.md) | Anything shown on more than one page; menus; reducing duplication of elements and styles |
|
|
163
|
+
| [components.md](reference/components.md) | One block placed many times — a card, a tier, a testimonial: props, slots, binding a row into one, why it is closed |
|
|
161
164
|
| [data-and-visibility.md](reference/data-and-visibility.md) | Bindings, providers, offline data, loading/empty/error states, live data, caching, showing and hiding, kept state |
|
|
162
165
|
| [lists.md](reference/lists.md) | Rendering rows, filtering and sorting them, a detail page for one record |
|
|
163
166
|
| [validation.md](reference/validation.md) | How `authorSpace` checks, the loop that wastes no attempts, and what it cannot see |
|
|
164
167
|
| [authoring-errors.md](reference/authoring-errors.md) | What `authorSpace` refuses or warns about, and what to write instead |
|
|
165
168
|
| [templates.md](reference/templates.md) | Any `{{ … }}` or `{% … %}`: where it runs, naming sources, filters, tests, dates |
|
|
166
169
|
| [flows.md](reference/flows.md) | Clicks, submits, page loads, server actions, realtime channels, modals, state |
|
|
167
|
-
| [plugins.md](reference/plugins.md) | A component of your own: props, binding them, writing state, channels, registering |
|
|
170
|
+
| [plugins.md](reference/plugins.md) | A component of your own: props, binding them, writing state, channels, registering, behaving in the builder |
|
|
168
171
|
| [structure.md](reference/structure.md) | A space bigger than one screen: files, helpers, naming, keeping it short |
|
|
169
172
|
| [testing.md](reference/testing.md) | Any test: `inspectPage` (one call, every problem), handles, fixtures, catching a flash from the first frame, shortcuts, counting renders |
|
|
170
173
|
| [performance.md](reference/performance.md) | A page with many elements, a busy flow, something that feels slow: what renders, what it costs, how to measure it |
|
|
171
|
-
| [templates-and-export.md](reference/templates-and-export.md) | Publishing a template; turning an exported JSON into code |
|
|
174
|
+
| [templates-and-export.md](reference/templates-and-export.md) | Publishing a template; turning an exported JSON — or a server action — into code |
|
|
172
175
|
| [accessibility.md](reference/accessibility.md) | Icon buttons, fields, images, clickable cards, toggles, headings, landmarks, a canvas — anything a screen reader or a browser agent has to use |
|
|
173
176
|
| [review-checklist.md](reference/review-checklist.md) | Before you say it is done |
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Components: a block written once, placed anywhere
|
|
2
|
+
|
|
3
|
+
A product card, a pricing tier, a testimonial: the same block on several pages, or several times on one, differing
|
|
4
|
+
only in what it says. A helper function that returns `ElementSpec` writes a COPY of the block wherever it is called —
|
|
5
|
+
the document holds every copy, and a person who later edits one in the builder edits that one. A **component** is the
|
|
6
|
+
block written ONCE: the document holds one tree, every placement is an instance of it, and an edit to it is an edit to
|
|
7
|
+
all of them, in code and in the builder alike.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { component, container, heading, text } from '@plitzi/sdk-authoring';
|
|
11
|
+
|
|
12
|
+
import type { ComponentSpec } from '@plitzi/sdk-authoring';
|
|
13
|
+
|
|
14
|
+
export const productCard: ComponentSpec = {
|
|
15
|
+
id: 'product-card',
|
|
16
|
+
label: 'Product card',
|
|
17
|
+
props: {
|
|
18
|
+
title: { type: 'text', description: 'The product name', required: true },
|
|
19
|
+
price: { type: 'number', description: 'In euros' },
|
|
20
|
+
featured: { type: 'boolean', description: 'Wears the accent border', default: false }
|
|
21
|
+
},
|
|
22
|
+
slots: ['product-card-actions'],
|
|
23
|
+
root: container({
|
|
24
|
+
id: 'product-card-root',
|
|
25
|
+
class: card,
|
|
26
|
+
children: [
|
|
27
|
+
heading({ id: 'product-card-title', bind: { content: 'props.title' } }),
|
|
28
|
+
text({ id: 'product-card-price', content: '{{ props.price }} €' }),
|
|
29
|
+
container({ id: 'product-card-actions' })
|
|
30
|
+
]
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// in the space
|
|
35
|
+
components: [productCard],
|
|
36
|
+
pages: [{ name: 'Shop', slug: 'shop', body: [
|
|
37
|
+
component('product-card', { props: { title: 'Lamp', price: 40 }, children: { 'product-card-actions': [button('Buy')] } })
|
|
38
|
+
] }]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **Props are attributes of the instance**, declared like a step's params (`type`, `description`, `required`,
|
|
42
|
+
`default`, `options`) and read inside as `{{ props.<name> }}` — in a binding's `source`, a template or an attribute.
|
|
43
|
+
A prop name is letters, digits and `_`, never one an instance already has (`referenceId`, `slot`, `className`…).
|
|
44
|
+
A prop an instance leaves out, with no `default`, is `null`: `{{ props.blurb }}` prints nothing and
|
|
45
|
+
`visible: 'props.featured'` keeps the element hidden.
|
|
46
|
+
- **A prop can be bound** like any attribute: `component('product-card', { bind: [{ to: 'title', source: 'list_products.item.name' }] })`.
|
|
47
|
+
That is how a list row hands its record to a card — bind the record, not each field, if the card reads several:
|
|
48
|
+
a `json` prop `item`, read as `{{ props.item.name }}`.
|
|
49
|
+
- **Slots** are elements of the component's tree that an instance fills. `children: [ … ]` fills a component with one
|
|
50
|
+
slot; `children: { '<slot>': [ … ] }` names the slot for each. What fills a slot is the instance's, not the
|
|
51
|
+
component's: it is authored on the page and sees the page's sources.
|
|
52
|
+
- **Closed.** Inside, nothing but `props` and the globals (`state`, `auth`, `navigation`, `theme`, `variables`,
|
|
53
|
+
`computed`). A binding inside a component onto a provider of the page is refused — "nothing in this component answers
|
|
54
|
+
to …" — because the component would render differently wherever it was placed. Put the provider inside the component,
|
|
55
|
+
or hand the value in as a prop.
|
|
56
|
+
- **Ids are shared** with the whole space, like a layout's: name a component's elements after it (`product-card-title`).
|
|
57
|
+
The same ids render once per instance, each with state of its own.
|
|
58
|
+
- **State of its own per instance.** A flow inside a component that names one of its elements —
|
|
59
|
+
`toggleElement({ category: 'state', key: 'visibility' }, 'product-card-details')` — acts on THAT instance's copy.
|
|
60
|
+
Global state is shared: `toggleInState({ key: 'favourites', value: '{{ props.product.id }}' })` from any card.
|
|
61
|
+
- **Components nest**: a component may place another. One that places itself, directly or through another, is refused.
|
|
62
|
+
- **Checked when written**: a component the space does not declare, a prop it does not declare, a required one left out,
|
|
63
|
+
a value of the wrong kind (`featured: 'true'` for a boolean) and a child for a slot it does not have each throw, by name.
|
|
64
|
+
|
|
65
|
+
## Component, layout or helper?
|
|
66
|
+
|
|
67
|
+
| You have | Write |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| Chrome a page renders INSIDE — top bar, sidebar, footer | a **layout** ([layouts.md](layouts.md)) |
|
|
70
|
+
| One block placed many times, differing in its content | a **component** |
|
|
71
|
+
| Many blocks of DIFFERENT structure built by the same logic | a **helper** returning `ElementSpec` |
|
|
72
|
+
| A list of the same block over data | a **list** whose row is the block — or places the component ([lists.md](lists.md)) |
|
|
73
|
+
|
|
74
|
+
A component is also what a person editing the space in the builder will find in its Components panel, open and edit
|
|
75
|
+
once; a helper's output is, to them, a pile of copies.
|
|
@@ -39,7 +39,7 @@ never a nested tree. Use the step builders — they fill in where a step runs an
|
|
|
39
39
|
|
|
40
40
|
## What a trigger hands the flow
|
|
41
41
|
|
|
42
|
-
Name the trigger (`named('changed', onChange
|
|
42
|
+
Name the trigger (`named('changed', on('onChange'))`) and read its payload as `{{ changed.<field> }}`:
|
|
43
43
|
|
|
44
44
|
| Trigger | Payload |
|
|
45
45
|
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
|