@plitzi/sdk-server 0.38.4 → 0.38.6
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 +350 -0
- package/README.md +126 -17
- package/dist/actions.d.ts +8 -5
- package/dist/actions.js +3 -1
- package/dist/adapters/jsonAdapters.d.ts +1 -1
- package/dist/adapters/jsonAdapters.js +16 -3
- package/dist/core/configSeam.js +20 -4
- package/dist/core/createServer.js +3 -2
- package/dist/core/handlers/socialRouteHandlers.js +6 -1
- package/dist/core/http/stages/devReload.d.ts +11 -0
- package/dist/core/http/stages/devReload.js +17 -0
- package/dist/core/server/baseServer.d.ts +2 -0
- package/dist/core/server/baseServer.js +3 -0
- package/dist/core/server/pageServer.js +18 -0
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/rsc.js +1 -1
- package/dist/core/services/spaceDocument.d.ts +2 -0
- package/dist/core/services/spaceDocument.js +11 -0
- package/dist/functions-api.d.ts +6 -1
- package/dist/functionsRunner.d.ts +5 -0
- package/dist/functionsRunner.js +2 -1
- package/dist/helpers/hydrationPayload.d.ts +25 -4
- package/dist/helpers/hydrationPayload.js +36 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/modules/actions/index.d.ts +11 -2
- package/dist/modules/actions/index.js +29 -7
- package/dist/modules/actions/runtime/fileKv.d.ts +28 -0
- package/dist/modules/actions/runtime/fileKv.js +95 -0
- package/dist/modules/actions/runtime/fileKv.test.d.ts +1 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +23 -0
- package/dist/modules/actions/runtime/memoryKv.js +39 -24
- package/dist/modules/actions/runtime/namespaceKv.d.ts +2 -0
- package/dist/modules/actions/runtime/namespaceKv.js +5 -3
- package/dist/modules/actions/runtime/renderResolver.d.ts +3 -2
- package/dist/modules/actions/runtime/renderResolver.js +13 -9
- package/dist/modules/actions/runtime/renderShare.d.ts +7 -24
- package/dist/modules/actions/runtime/renderShare.js +47 -13
- package/dist/modules/actions/runtime/runAction.js +15 -1
- package/dist/modules/actions/runtime/scope.js +16 -1
- package/dist/modules/actions/runtime/signing.js +4 -1
- package/dist/modules/actions/runtime/taskContext.d.ts +7 -1
- package/dist/modules/actions/runtime/taskContext.js +6 -2
- package/dist/modules/actions/tasks/registry.d.ts +3 -1
- package/dist/modules/actions/tasks/registry.js +15 -1
- package/dist/modules/actions/types.d.ts +15 -2
- package/dist/modules/functions/config.d.ts +6 -0
- package/dist/modules/functions/context.d.ts +4 -2
- package/dist/modules/functions/context.js +43 -21
- package/dist/modules/functions/load.d.ts +8 -1
- package/dist/modules/functions/load.js +8 -4
- package/dist/modules/functions/manifest.d.ts +4 -1
- package/dist/modules/functions/manifest.js +14 -2
- package/dist/modules/functions/native.d.ts +2 -1
- package/dist/modules/functions/native.js +2 -2
- package/dist/modules/functions/plugins.test.d.ts +1 -0
- package/dist/modules/functions/routes.d.ts +8 -3
- package/dist/modules/functions/routes.js +41 -21
- package/dist/modules/functions/scope.d.ts +30 -0
- package/dist/modules/functions/scope.js +28 -0
- package/dist/modules/functions/space.d.ts +10 -5
- package/dist/modules/functions/space.js +37 -15
- package/dist/modules/rsc/connectorRscData.d.ts +3 -1
- package/dist/modules/rsc/connectorRscData.js +8 -4
- package/dist/modules/rsc/handler.d.ts +3 -16
- package/dist/modules/rsc/handler.js +10 -4
- package/dist/modules/rsc/publicFileResolver.d.ts +15 -12
- package/dist/modules/rsc/publicFileResolver.js +56 -24
- package/dist/modules/rsc/resolveRscData.d.ts +3 -1
- package/dist/modules/rsc/resolveRscData.js +8 -5
- package/dist/modules/runtime/bundle.d.ts +5 -0
- package/dist/modules/runtime/bundle.js +20 -5
- package/dist/modules/sqlite/kv.d.ts +34 -0
- package/dist/modules/sqlite/kv.js +99 -0
- package/dist/modules/sqlite/kv.test.d.ts +1 -0
- package/dist/modules/ssr/Component.d.ts +2 -1
- package/dist/modules/ssr/Component.js +2 -1
- package/dist/modules/ssr/prepareRender.js +48 -6
- package/dist/modules/ssr/spaceDocument.d.ts +25 -0
- package/dist/modules/ssr/spaceDocument.js +72 -0
- package/dist/modules/ssr/spaceDocument.test.d.ts +1 -0
- package/dist/modules/ssr/testing/offlineData.d.ts +5 -0
- package/dist/modules/ssr/views/template.ejs +122 -20
- package/dist/plugins/compile.js +6 -21
- package/dist/plugins/compile.test.d.ts +1 -0
- package/dist/plugins/copy.d.ts +5 -0
- package/dist/plugins/copy.js +16 -1
- package/dist/plugins/copy.test.d.ts +1 -0
- package/dist/plugins/manager.d.ts +13 -0
- package/dist/plugins/manager.js +40 -11
- package/dist/plugins/watch.d.ts +12 -0
- package/dist/plugins/watch.js +79 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +2 -2
- package/dist/sqlite.d.ts +17 -0
- package/dist/sqlite.js +2 -0
- package/package.json +8 -3
- package/skills/plitzi-authoring/CHEATSHEET.md +4 -2
- package/skills/plitzi-authoring/SKILL.md +20 -19
- package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
- package/skills/plitzi-authoring/recipes/server-data.ts +3 -3
- package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
- package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
- package/skills/plitzi-authoring/reference/authoring-errors.md +8 -2
- package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -1
- package/skills/plitzi-authoring/reference/data-and-visibility.md +24 -22
- package/skills/plitzi-authoring/reference/drawing.md +24 -0
- package/skills/plitzi-authoring/reference/efficiency.md +19 -2
- package/skills/plitzi-authoring/reference/flows.md +3 -0
- package/skills/plitzi-authoring/reference/layouts.md +8 -23
- package/skills/plitzi-authoring/reference/performance.md +2 -2
- package/skills/plitzi-authoring/reference/plugins.md +51 -38
- package/skills/plitzi-authoring/reference/review-checklist.md +2 -0
- package/skills/plitzi-authoring/reference/structure.md +32 -1
- package/skills/plitzi-authoring/reference/templates.md +5 -2
- package/skills/plitzi-authoring/reference/testing.md +9 -2
- package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
- package/skills/plitzi-authoring/reference/validation.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createActionResolver } from "../actions/runtime/renderResolver.js";
|
|
2
2
|
import { createConnectorResolver } from "../connectors/resolver.js";
|
|
3
|
-
import { publicFileResolver } from "./publicFileResolver.js";
|
|
3
|
+
import { dataFileResolver, dataLookupResolver, publicFileResolver } from "./publicFileResolver.js";
|
|
4
4
|
import { resolveRscData } from "./resolveRscData.js";
|
|
5
5
|
//#region src/modules/rsc/connectorRscData.ts
|
|
6
6
|
/**
|
|
@@ -18,10 +18,12 @@ import { resolveRscData } from "./resolveRscData.js";
|
|
|
18
18
|
*
|
|
19
19
|
* A deployment that resolves server elements some other way still supplies its own `getRscData`, and that one wins.
|
|
20
20
|
*/
|
|
21
|
-
var connectorRscData = ({ connectors, actions, publicDir, elementTimeoutMs }) => {
|
|
21
|
+
var connectorRscData = ({ connectors, actions, publicDir, dataDir, elementTimeoutMs }) => {
|
|
22
22
|
const resolveConnector = connectors ? createConnectorResolver(connectors) : void 0;
|
|
23
23
|
const resolveAction = actions ? createActionResolver(actions.lookups, actions.module) : void 0;
|
|
24
24
|
const resolvePublicFile = publicDir ? publicFileResolver(publicDir) : void 0;
|
|
25
|
+
const dataLookup = actions?.lookups.getData;
|
|
26
|
+
const resolveDataFile = dataDir ? dataFileResolver(dataDir) : dataLookup ? dataLookupResolver((spaceId, at) => dataLookup(spaceId, at)) : void 0;
|
|
25
27
|
/**
|
|
26
28
|
* An element names ONE producer, and which one decides how its data is fetched.
|
|
27
29
|
*
|
|
@@ -33,9 +35,10 @@ var connectorRscData = ({ connectors, actions, publicDir, elementTimeoutMs }) =>
|
|
|
33
35
|
const attributes = context.element.attributes;
|
|
34
36
|
if (attributes.connector) return resolveConnector ? resolveConnector(context) : void 0;
|
|
35
37
|
if (attributes.action) return resolveAction ? resolveAction(context) : void 0;
|
|
36
|
-
|
|
38
|
+
const fromData = resolveDataFile ? await resolveDataFile(context) : void 0;
|
|
39
|
+
return fromData !== void 0 ? fromData : resolvePublicFile ? resolvePublicFile(context) : void 0;
|
|
37
40
|
};
|
|
38
|
-
return async ({ req, spaceId, environment, user, ids, loadOfflineData, flagOverrides }) => {
|
|
41
|
+
return async ({ req, spaceId, environment, user, ids, loadOfflineData, flagOverrides, signal }) => {
|
|
39
42
|
const offlineData = await loadOfflineData();
|
|
40
43
|
if (!offlineData || offlineData.schema.rsc?.enabled === false) return {};
|
|
41
44
|
return resolveRscData({
|
|
@@ -47,6 +50,7 @@ var connectorRscData = ({ connectors, actions, publicDir, elementTimeoutMs }) =>
|
|
|
47
50
|
ids,
|
|
48
51
|
resolveElement,
|
|
49
52
|
...flagOverrides ? { flagOverrides } : {},
|
|
53
|
+
...signal ? { signal } : {},
|
|
50
54
|
...elementTimeoutMs === void 0 ? {} : { timeoutMs: elementTimeoutMs }
|
|
51
55
|
});
|
|
52
56
|
};
|
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { TtlCache } from '../../helpers/cache';
|
|
2
2
|
import { PluginManager } from '../../plugins/manager';
|
|
3
3
|
import { SSRPageServerConfig, SSRRequest, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
4
|
-
/**
|
|
5
|
-
* Rewrites the request to the page the browser is actually on.
|
|
6
|
-
*
|
|
7
|
-
* A refresh is issued against `/_rsc`, not against `/blog/my-post`, so resolving straight from `req.path` matches
|
|
8
|
-
* nothing in `schema.pages` and every client-side refresh silently returns no data. The SDK sends the visitor's
|
|
9
|
-
* location as `?location=`, and route params plus the page parameter are read from that instead.
|
|
10
|
-
*
|
|
11
|
-
* What a refresh asks for besides — the next page of a "load more", a search a provider was reloaded with — rides on
|
|
12
|
-
* the query string beside `location`, and is read over the location's own query. It used to be dropped with the rest
|
|
13
|
-
* of the endpoint's query, so a provider paged in place was answered its first page every time.
|
|
14
|
-
*
|
|
15
|
-
* Reflecting both is safe by construction: the location is matched against this space's own page list, and the rest
|
|
16
|
-
* is what the same visitor could have put in the page's own address; the cache key is the whole request, so no answer
|
|
17
|
-
* is served for a query it was not made for.
|
|
18
|
-
*/
|
|
19
4
|
export declare const withPageLocation: (req: SSRRequest) => SSRRequest;
|
|
20
5
|
/**
|
|
21
6
|
* Handles GET /_rsc requests.
|
|
@@ -30,4 +15,6 @@ export declare const withPageLocation: (req: SSRRequest) => SSRRequest;
|
|
|
30
15
|
* - Authenticated requests: Cache-Control: private, max-age=<ttl>
|
|
31
16
|
* - Unauthenticated requests: Cache-Control: public, max-age=<ttl>
|
|
32
17
|
*/
|
|
33
|
-
export declare const handleRsc: (req: SSRRequest, res: SSRResponseHelpers, config: SSRPageServerConfig, _pluginManager: PluginManager, cache?: TtlCache<string
|
|
18
|
+
export declare const handleRsc: (req: SSRRequest, res: SSRResponseHelpers, config: SSRPageServerConfig, _pluginManager: PluginManager, cache?: TtlCache<string>,
|
|
19
|
+
/** The request's own: a browser that stops waiting stops the elements it asked about. */
|
|
20
|
+
signal?: AbortSignal) => Promise<void>;
|
|
@@ -39,12 +39,16 @@ var ENDPOINT_PARAMS = /* @__PURE__ */ new Set(["location", "ids"]);
|
|
|
39
39
|
* is what the same visitor could have put in the page's own address; the cache key is the whole request, so no answer
|
|
40
40
|
* is served for a query it was not made for.
|
|
41
41
|
*/
|
|
42
|
+
/** What a refresh asked for beside the endpoint's own parameters. */
|
|
43
|
+
var refreshParams = (req) => Object.fromEntries(Object.entries(req.query).filter(([key]) => !ENDPOINT_PARAMS.has(key)));
|
|
42
44
|
var withPageLocation = (req) => {
|
|
43
45
|
const location = req.query.location;
|
|
44
46
|
if (!location || !location.startsWith("/") || location.length > MAX_PAGE_LOCATION) return req;
|
|
45
47
|
const url = new URL(location, `${req.protocol}://${req.hostname}`);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
+
const query = {
|
|
49
|
+
...Object.fromEntries(url.searchParams.entries()),
|
|
50
|
+
...refreshParams(req)
|
|
51
|
+
};
|
|
48
52
|
const search = new URLSearchParams(query).toString();
|
|
49
53
|
return {
|
|
50
54
|
...req,
|
|
@@ -67,7 +71,7 @@ var withPageLocation = (req) => {
|
|
|
67
71
|
* - Authenticated requests: Cache-Control: private, max-age=<ttl>
|
|
68
72
|
* - Unauthenticated requests: Cache-Control: public, max-age=<ttl>
|
|
69
73
|
*/
|
|
70
|
-
var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
74
|
+
var handleRsc = async (req, res, config, _pluginManager, cache, signal) => {
|
|
71
75
|
if (!config.adapters.getRscData) {
|
|
72
76
|
res.setStatus(501);
|
|
73
77
|
res.send(JSON.stringify({ error: "getRscData adapter not configured" }));
|
|
@@ -83,6 +87,7 @@ var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
|
83
87
|
const ids = idsRaw ? idsRaw.split(",").filter(Boolean).slice(0, 50).map((id) => id.slice(0, 128)) : void 0;
|
|
84
88
|
const idsParam = ids?.join(",");
|
|
85
89
|
const pageRequest = withPageLocation(req);
|
|
90
|
+
req.ctx.rscParams = refreshParams(req);
|
|
86
91
|
/**
|
|
87
92
|
* Whether this refresh belongs to somebody looking at a draft.
|
|
88
93
|
*
|
|
@@ -130,7 +135,8 @@ var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
|
130
135
|
user: req.ctx.user,
|
|
131
136
|
ids,
|
|
132
137
|
loadOfflineData,
|
|
133
|
-
flagOverrides
|
|
138
|
+
flagOverrides,
|
|
139
|
+
...signal ? { signal } : {}
|
|
134
140
|
});
|
|
135
141
|
} catch (err) {
|
|
136
142
|
serverLog.error("RSC", "getRscData error", err);
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { RscElementResolver } from './resolveRscData';
|
|
2
|
+
import { SpaceRevision } from '@plitzi/sdk-shared';
|
|
2
3
|
/**
|
|
3
|
-
* A server provider whose `query` is a file this server serves from `publicDir` — `/
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* A project with no backend keeps its data in `public/data/*.json`, and an `apiContainer` fetching it from the browser
|
|
7
|
-
* leaves the server-rendered page without those sections: a flash of nothing, no content for a crawler, and no
|
|
8
|
-
* anchor in place for `/#section` to land on. Read here, it is the same bytes the browser would have fetched.
|
|
9
|
-
*
|
|
10
|
-
* Only a plain path inside `publicDir`: a URL, one that climbs out of the folder, or one with `{{tokens}}` (resolved
|
|
11
|
-
* in the browser against the visitor's route and state) is not this resolver's, and the element is left as it was.
|
|
12
|
-
*
|
|
13
|
-
* Answered in the shape a browser request publishes — `{ status, data }`, the body under `data` — so a provider reads
|
|
14
|
-
* `<source>.data.<field>` whichever runtime it has, and moving it to the server changes no binding.
|
|
4
|
+
* A server provider whose `query` is a file this server serves from `publicDir` — `/hours.json` — read from disk, so
|
|
5
|
+
* the page arrives with the section in it rather than fetching in the browser what the server already has. The same
|
|
6
|
+
* bytes the browser would have fetched.
|
|
15
7
|
*/
|
|
16
8
|
export declare const publicFileResolver: (publicDir: string) => RscElementResolver;
|
|
9
|
+
/**
|
|
10
|
+
* A server provider whose `query` is `/data/<file>`, read from `dataDir` — the project's own data, which is never
|
|
11
|
+
* served as a file: nobody downloads the folder. What the provider reads is in the page it renders.
|
|
12
|
+
*/
|
|
13
|
+
export declare const dataFileResolver: (dataDir: string) => RscElementResolver;
|
|
14
|
+
/**
|
|
15
|
+
* A server provider whose `query` is `/data/<file>`, answered from the space's own data as Plitzi keeps it — of the
|
|
16
|
+
* version being rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a
|
|
17
|
+
* published page reads the data it was published with. What `dataFileResolver` is for a folder, for a platform.
|
|
18
|
+
*/
|
|
19
|
+
export declare const dataLookupResolver: (getData: (spaceId: number, at: SpaceRevision) => Promise<Record<string, string> | undefined>) => RscElementResolver;
|
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { PROJECT_DATA_PREFIX } from "@plitzi/sdk-shared/server/rsc/projectData";
|
|
3
4
|
//#region src/modules/rsc/publicFileResolver.ts
|
|
5
|
+
/** Where a provider's `query` is a path: a URL, a protocol-relative one or one with `{{tokens}}` is not. */
|
|
6
|
+
var pathOf = (query) => {
|
|
7
|
+
if (typeof query !== "string" || !query.startsWith("/") || query.startsWith("//") || query.includes("{{")) return;
|
|
8
|
+
try {
|
|
9
|
+
return decodeURIComponent(query.split(/[?#]/)[0]);
|
|
10
|
+
} catch {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
4
14
|
/**
|
|
5
|
-
* A server provider whose `query`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* leaves the server-rendered page without those sections: a flash of nothing, no content for a crawler, and no
|
|
10
|
-
* anchor in place for `/#section` to land on. Read here, it is the same bytes the browser would have fetched.
|
|
11
|
-
*
|
|
12
|
-
* Only a plain path inside `publicDir`: a URL, one that climbs out of the folder, or one with `{{tokens}}` (resolved
|
|
13
|
-
* in the browser against the visitor's route and state) is not this resolver's, and the element is left as it was.
|
|
14
|
-
*
|
|
15
|
-
* Answered in the shape a browser request publishes — `{ status, data }`, the body under `data` — so a provider reads
|
|
16
|
-
* `<source>.data.<field>` whichever runtime it has, and moving it to the server changes no binding.
|
|
15
|
+
* A server provider whose `query` names a JSON file under `prefix`, read from the same path inside `folder` and handed
|
|
16
|
+
* to the render — answered in the shape a browser request publishes, `{ status, data }`, so a provider reads
|
|
17
|
+
* `<source>.data.<field>` whichever runtime it has. A path outside `prefix`, or one that climbs out of the folder, is
|
|
18
|
+
* not this resolver's, and the element is left as it was; a file not there, or not JSON, is the provider's error state.
|
|
17
19
|
*/
|
|
18
|
-
var
|
|
19
|
-
const root = path.resolve(
|
|
20
|
+
var fileResolver = (folder, prefix) => {
|
|
21
|
+
const root = path.resolve(folder);
|
|
20
22
|
return async ({ element, signal }) => {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
pathname = decodeURIComponent(query.split(/[?#]/)[0]);
|
|
26
|
-
} catch {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const file = path.resolve(root, `.${pathname}`);
|
|
23
|
+
const pathname = pathOf(element.attributes.query);
|
|
24
|
+
if (pathname === void 0 || !pathname.startsWith(prefix)) return;
|
|
25
|
+
const file = path.resolve(root, `.${pathname.slice(prefix.length - 1)}`);
|
|
30
26
|
if (!file.startsWith(`${root}${path.sep}`)) return;
|
|
31
27
|
try {
|
|
32
28
|
return {
|
|
@@ -41,5 +37,41 @@ var publicFileResolver = (publicDir) => {
|
|
|
41
37
|
}
|
|
42
38
|
};
|
|
43
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* A server provider whose `query` is a file this server serves from `publicDir` — `/hours.json` — read from disk, so
|
|
42
|
+
* the page arrives with the section in it rather than fetching in the browser what the server already has. The same
|
|
43
|
+
* bytes the browser would have fetched.
|
|
44
|
+
*/
|
|
45
|
+
var publicFileResolver = (publicDir) => fileResolver(publicDir, "/");
|
|
46
|
+
/**
|
|
47
|
+
* A server provider whose `query` is `/data/<file>`, read from `dataDir` — the project's own data, which is never
|
|
48
|
+
* served as a file: nobody downloads the folder. What the provider reads is in the page it renders.
|
|
49
|
+
*/
|
|
50
|
+
var dataFileResolver = (dataDir) => fileResolver(dataDir, PROJECT_DATA_PREFIX);
|
|
51
|
+
/**
|
|
52
|
+
* A server provider whose `query` is `/data/<file>`, answered from the space's own data as Plitzi keeps it — of the
|
|
53
|
+
* version being rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a
|
|
54
|
+
* published page reads the data it was published with. What `dataFileResolver` is for a folder, for a platform.
|
|
55
|
+
*/
|
|
56
|
+
var dataLookupResolver = (getData) => {
|
|
57
|
+
return async ({ element, spaceId, environment, req }) => {
|
|
58
|
+
const pathname = pathOf(element.attributes.query);
|
|
59
|
+
if (pathname === void 0 || !pathname.startsWith(PROJECT_DATA_PREFIX)) return;
|
|
60
|
+
const deployment = req.ctx.spaceDeployment;
|
|
61
|
+
const text = (await getData(spaceId, {
|
|
62
|
+
environment: deployment?.environment ?? environment,
|
|
63
|
+
revision: deployment?.revision ?? 0
|
|
64
|
+
}))?.[pathname.slice(PROJECT_DATA_PREFIX.length)];
|
|
65
|
+
if (text === void 0) return null;
|
|
66
|
+
try {
|
|
67
|
+
return {
|
|
68
|
+
status: 200,
|
|
69
|
+
data: JSON.parse(text)
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
44
76
|
//#endregion
|
|
45
|
-
export { publicFileResolver };
|
|
77
|
+
export { dataFileResolver, dataLookupResolver, publicFileResolver };
|
|
@@ -39,6 +39,8 @@ export type ResolveRscDataOptions = {
|
|
|
39
39
|
* per deployment as `rsc.elementTimeoutMs`.
|
|
40
40
|
*/
|
|
41
41
|
timeoutMs?: number;
|
|
42
|
+
/** Whoever asked stopped waiting: every element still resolving is cancelled, as when its budget runs out. */
|
|
43
|
+
signal?: AbortSignal;
|
|
42
44
|
};
|
|
43
45
|
/**
|
|
44
46
|
* Resolves the server-side data slices for the page addressed by `req`.
|
|
@@ -47,4 +49,4 @@ export type ResolveRscDataOptions = {
|
|
|
47
49
|
* and the route params are what the browser will see. Each `runtime: 'server'` element resolves independently:
|
|
48
50
|
* a provider that fails or hangs costs its own slice and nothing else.
|
|
49
51
|
*/
|
|
50
|
-
export declare const resolveRscData: ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs }: ResolveRscDataOptions) => Promise<SSRRscData>;
|
|
52
|
+
export declare const resolveRscData: ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs, signal }: ResolveRscDataOptions) => Promise<SSRRscData>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { serverLog } from "../../helpers/serverLog.js";
|
|
2
|
+
import onAbort from "../../helpers/onAbort.js";
|
|
2
3
|
import { matchRscPage } from "./matchRscPage.js";
|
|
3
4
|
import { flagUserFromSSR, flagValues, resolveFlags } from "@plitzi/sdk-shared/flags";
|
|
4
5
|
import { collectServerElements } from "@plitzi/sdk-shared/schema/serverElements";
|
|
@@ -11,18 +12,20 @@ var DEFAULT_ELEMENT_TIMEOUT_MS = 5e3;
|
|
|
11
12
|
* the loser running to its own timeout — for actions, five more seconds of a held slot and an in-flight request
|
|
12
13
|
* per element, for a page that was answered without it.
|
|
13
14
|
*/
|
|
14
|
-
var withBudget = async (run, timeoutMs, label) => {
|
|
15
|
+
var withBudget = async (run, timeoutMs, label, asked) => {
|
|
15
16
|
const controller = new AbortController();
|
|
16
17
|
let timer;
|
|
18
|
+
const releaseAsked = asked ? onAbort(asked, () => controller.abort()) : void 0;
|
|
17
19
|
try {
|
|
18
20
|
return await Promise.race([run(controller.signal), new Promise((_resolve, reject) => {
|
|
19
21
|
timer = setTimeout(() => {
|
|
20
22
|
controller.abort();
|
|
21
|
-
reject(/* @__PURE__ */ new Error(`RSC resolution timed out after ${timeoutMs}ms for ${label}
|
|
23
|
+
reject(/* @__PURE__ */ new Error(`RSC resolution timed out after ${timeoutMs}ms for ${label} — its section arrives empty. A slower producer is given more with \`createServer({ rsc: { elementTimeoutMs } })\`.`));
|
|
22
24
|
}, timeoutMs);
|
|
23
25
|
})]);
|
|
24
26
|
} finally {
|
|
25
27
|
if (timer) clearTimeout(timer);
|
|
28
|
+
releaseAsked?.();
|
|
26
29
|
}
|
|
27
30
|
};
|
|
28
31
|
/**
|
|
@@ -32,7 +35,7 @@ var withBudget = async (run, timeoutMs, label) => {
|
|
|
32
35
|
* and the route params are what the browser will see. Each `runtime: 'server'` element resolves independently:
|
|
33
36
|
* a provider that fails or hangs costs its own slice and nothing else.
|
|
34
37
|
*/
|
|
35
|
-
var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS }) => {
|
|
38
|
+
var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS, signal }) => {
|
|
36
39
|
if (schema.rsc?.enabled === false) return {};
|
|
37
40
|
const match = matchRscPage(schema, req.path, user);
|
|
38
41
|
if (!match) return { serverData: {} };
|
|
@@ -58,10 +61,10 @@ var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, reso
|
|
|
58
61
|
environment,
|
|
59
62
|
user,
|
|
60
63
|
signal
|
|
61
|
-
}), timeoutMs, `element ${element.id}
|
|
64
|
+
}), timeoutMs, `element ${element.id}`, signal)
|
|
62
65
|
})))).reduce((acum, result, index) => {
|
|
63
66
|
if (result.status === "rejected") {
|
|
64
|
-
serverLog.error("RSC", `element ${targets[index].id} failed to resolve`, result.reason);
|
|
67
|
+
if (!signal?.aborted) serverLog.error("RSC", `element ${targets[index].id} failed to resolve`, result.reason);
|
|
65
68
|
return acum;
|
|
66
69
|
}
|
|
67
70
|
if (result.value.data !== void 0) acum[result.value.id] = result.value.data;
|
|
@@ -17,6 +17,11 @@ export declare const inspectRuntime: (bytes: Uint8Array) => {
|
|
|
17
17
|
entry: string;
|
|
18
18
|
files: string[];
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* A project's runtime module, as a server of its own runs it — `src/runtime/index.ts`, or what `build` compiled it to —
|
|
22
|
+
* or nothing when the project has none: a project starts one by writing it (`plitzi add runtime`), with nothing to wire.
|
|
23
|
+
*/
|
|
24
|
+
export declare const loadRuntimeModule: (file: string) => Promise<SpaceRuntime | undefined>;
|
|
20
25
|
/**
|
|
21
26
|
* A packed runtime written into `dir` and imported: its module's default export. `dir` must be inside the host's own
|
|
22
27
|
* project, so the packages the bundle leaves to the host resolve to the host's — one copy of each.
|
|
@@ -85,6 +85,24 @@ var inspectRuntime = (bytes) => {
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
var isRuntime = (value) => isRecord(value) && typeof value.start === "function";
|
|
88
|
+
/** A module's default export, held to what a runtime is — or refused, saying how to write one. */
|
|
89
|
+
var runtimeOf = (loaded) => {
|
|
90
|
+
const runtime = isRecord(loaded) ? loaded.default : void 0;
|
|
91
|
+
if (!isRuntime(runtime)) throw new Error("A runtime module exports its runtime by default: export default defineRuntime({ start: … })");
|
|
92
|
+
return runtime;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* A project's runtime module, as a server of its own runs it — `src/runtime/index.ts`, or what `build` compiled it to —
|
|
96
|
+
* or nothing when the project has none: a project starts one by writing it (`plitzi add runtime`), with nothing to wire.
|
|
97
|
+
*/
|
|
98
|
+
var loadRuntimeModule = async (file) => {
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(file);
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
return runtimeOf(await import(pathToFileURL(file).href));
|
|
105
|
+
};
|
|
88
106
|
/**
|
|
89
107
|
* A packed runtime written into `dir` and imported: its module's default export. `dir` must be inside the host's own
|
|
90
108
|
* project, so the packages the bundle leaves to the host resolve to the host's — one copy of each.
|
|
@@ -93,10 +111,7 @@ var loadRuntime = async (bytes, dir) => {
|
|
|
93
111
|
const { entry, files } = unpack(bytes);
|
|
94
112
|
await fs.mkdir(dir, { recursive: true });
|
|
95
113
|
await Promise.all(Object.entries(files).map(([name, content]) => fs.writeFile(path.join(dir, name), Buffer.from(content, "base64"))));
|
|
96
|
-
|
|
97
|
-
const runtime = isRecord(loaded) ? loaded.default : void 0;
|
|
98
|
-
if (!isRuntime(runtime)) throw new Error("A runtime module exports its runtime by default: export default defineRuntime({ start: … })");
|
|
99
|
-
return runtime;
|
|
114
|
+
return runtimeOf(await import(pathToFileURL(path.join(dir, entry)).href));
|
|
100
115
|
};
|
|
101
116
|
//#endregion
|
|
102
|
-
export { MAX_RUNTIME_BUNDLE_BYTES, inspectRuntime, loadRuntime, packRuntime, runtimeBundleId };
|
|
117
|
+
export { MAX_RUNTIME_BUNDLE_BYTES, inspectRuntime, loadRuntime, loadRuntimeModule, packRuntime, runtimeBundleId };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { ActionKvAdapter } from '../actions/types';
|
|
3
|
+
export type SqliteKvOptions = ({
|
|
4
|
+
/** The database file — created, with its folder, when it is not there. Opened in WAL mode. */
|
|
5
|
+
file: string;
|
|
6
|
+
db?: never;
|
|
7
|
+
} | {
|
|
8
|
+
/** A database the deployment opened and shares with its own tables (a job queue, an activity log). */
|
|
9
|
+
db: DatabaseSync;
|
|
10
|
+
file?: never;
|
|
11
|
+
}) & {
|
|
12
|
+
/** The table the keys live in, created when it is not there. Default `action_kv`. */
|
|
13
|
+
table?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A key/value store in a SQLite file, over Node's own `node:sqlite` — nothing to install, nothing to run beside the
|
|
17
|
+
* server. For a deployment with no database that wants what `kv` keeps to survive a restart AND to be shared: every
|
|
18
|
+
* process pointed at the same file — replicas on one machine, a server started with `workers` — sees the same keys,
|
|
19
|
+
* the same counters and the same single-flight keys.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { createSqliteKv } from '@plitzi/sdk-server/sqlite';
|
|
23
|
+
*
|
|
24
|
+
* createServer({ action: { kv: createSqliteKv({ file: 'data/kv.sqlite' }) } });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Every operation is ONE statement, so it is atomic without a transaction: an increment is an upsert that adds in
|
|
28
|
+
* place, a swap is an update whose `WHERE` names the value it expects. Lifetimes are instants on the store's clock,
|
|
29
|
+
* enforced on every read, and expired rows are swept as writes go by.
|
|
30
|
+
*
|
|
31
|
+
* Needs a Node with `node:sqlite` unflagged (22.13 and later), which still prints an `ExperimentalWarning` once when
|
|
32
|
+
* it loads — the reason this is its own entry rather than the default.
|
|
33
|
+
*/
|
|
34
|
+
export declare const createSqliteKv: (options: SqliteKvOptions) => ActionKvAdapter;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
//#region src/modules/sqlite/kv.ts
|
|
5
|
+
/**
|
|
6
|
+
* The STORE's clock in epoch ms, read inside each statement: every process on the file agrees on what "now" is.
|
|
7
|
+
* `julianday()` with no argument is the current instant.
|
|
8
|
+
*/
|
|
9
|
+
var NOW_MS = "CAST((julianday() - 2440587.5) * 86400000 AS INTEGER)";
|
|
10
|
+
var open = (file) => {
|
|
11
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
12
|
+
const db = new DatabaseSync(file);
|
|
13
|
+
db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;");
|
|
14
|
+
return db;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A key/value store in a SQLite file, over Node's own `node:sqlite` — nothing to install, nothing to run beside the
|
|
18
|
+
* server. For a deployment with no database that wants what `kv` keeps to survive a restart AND to be shared: every
|
|
19
|
+
* process pointed at the same file — replicas on one machine, a server started with `workers` — sees the same keys,
|
|
20
|
+
* the same counters and the same single-flight keys.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { createSqliteKv } from '@plitzi/sdk-server/sqlite';
|
|
24
|
+
*
|
|
25
|
+
* createServer({ action: { kv: createSqliteKv({ file: 'data/kv.sqlite' }) } });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Every operation is ONE statement, so it is atomic without a transaction: an increment is an upsert that adds in
|
|
29
|
+
* place, a swap is an update whose `WHERE` names the value it expects. Lifetimes are instants on the store's clock,
|
|
30
|
+
* enforced on every read, and expired rows are swept as writes go by.
|
|
31
|
+
*
|
|
32
|
+
* Needs a Node with `node:sqlite` unflagged (22.13 and later), which still prints an `ExperimentalWarning` once when
|
|
33
|
+
* it loads — the reason this is its own entry rather than the default.
|
|
34
|
+
*/
|
|
35
|
+
var createSqliteKv = (options) => {
|
|
36
|
+
const table = options.table ?? "action_kv";
|
|
37
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) throw new Error(`createSqliteKv: "${table}" is not a table name (letters, digits and _, not starting with a digit).`);
|
|
38
|
+
const db = options.db ?? open(options.file);
|
|
39
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${table} (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER)`);
|
|
40
|
+
const live = `(expires_at IS NULL OR expires_at > ${NOW_MS})`;
|
|
41
|
+
const expiry = (ttlSeconds) => ttlSeconds === void 0 ? null : ttlSeconds * 1e3;
|
|
42
|
+
const expiresAt = `CASE WHEN ? IS NULL THEN NULL ELSE ${NOW_MS} + ? END`;
|
|
43
|
+
const statements = {
|
|
44
|
+
get: db.prepare(`SELECT value FROM ${table} WHERE key = ? AND ${live}`),
|
|
45
|
+
set: db.prepare(`INSERT INTO ${table} (key, value, expires_at) VALUES (?, ?, ${expiresAt})
|
|
46
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`),
|
|
47
|
+
delete: db.prepare(`DELETE FROM ${table} WHERE key = ?`),
|
|
48
|
+
increment: db.prepare(`INSERT INTO ${table} (key, value, expires_at) VALUES (?, ?, NULL)
|
|
49
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
50
|
+
value = CASE WHEN ${table}.expires_at IS NOT NULL AND ${table}.expires_at <= ${NOW_MS}
|
|
51
|
+
THEN excluded.value ELSE CAST(${table}.value + ? AS TEXT) END,
|
|
52
|
+
expires_at = CASE WHEN ${table}.expires_at IS NOT NULL AND ${table}.expires_at <= ${NOW_MS}
|
|
53
|
+
THEN NULL ELSE ${table}.expires_at END
|
|
54
|
+
RETURNING value`),
|
|
55
|
+
expire: db.prepare(`UPDATE ${table} SET expires_at = ${expiresAt} WHERE key = ? AND ${live}`),
|
|
56
|
+
claim: db.prepare(`INSERT INTO ${table} (key, value, expires_at) VALUES (?, ?, ${expiresAt})
|
|
57
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at
|
|
58
|
+
WHERE ${table}.expires_at IS NOT NULL AND ${table}.expires_at <= ${NOW_MS}`),
|
|
59
|
+
replace: db.prepare(`UPDATE ${table} SET value = ?, expires_at = ${expiresAt} WHERE key = ? AND value = ? AND ${live}`),
|
|
60
|
+
sweep: db.prepare(`DELETE FROM ${table} WHERE expires_at IS NOT NULL AND expires_at <= ${NOW_MS}`)
|
|
61
|
+
};
|
|
62
|
+
/** Used-once keys (a delivery's idempotency key, a caller's window) expire and stay until swept: once a minute. */
|
|
63
|
+
let sweptAt = 0;
|
|
64
|
+
const sweep = () => {
|
|
65
|
+
if (Date.now() - sweptAt < 6e4) return;
|
|
66
|
+
sweptAt = Date.now();
|
|
67
|
+
statements.sweep.run();
|
|
68
|
+
};
|
|
69
|
+
const text = (row) => typeof row?.value === "string" ? row.value : void 0;
|
|
70
|
+
return {
|
|
71
|
+
get: (key) => Promise.resolve(text(statements.get.get(key))),
|
|
72
|
+
set: (key, value, ttlSeconds) => {
|
|
73
|
+
statements.set.run(key, value, expiry(ttlSeconds), expiry(ttlSeconds));
|
|
74
|
+
sweep();
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
},
|
|
77
|
+
delete: (key) => {
|
|
78
|
+
statements.delete.run(key);
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
},
|
|
81
|
+
increment: (key, amount) => {
|
|
82
|
+
const step = Number.isInteger(amount) ? BigInt(amount) : amount;
|
|
83
|
+
const value = text(statements.increment.get(key, String(amount), step));
|
|
84
|
+
sweep();
|
|
85
|
+
return Promise.resolve(Number(value ?? amount));
|
|
86
|
+
},
|
|
87
|
+
expire: (key, ttlSeconds) => {
|
|
88
|
+
statements.expire.run(expiry(ttlSeconds), expiry(ttlSeconds), key);
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
},
|
|
91
|
+
swap: (key, expected, next, ttlSeconds) => {
|
|
92
|
+
const written = expected === void 0 ? statements.claim.run(key, next, expiry(ttlSeconds), expiry(ttlSeconds)) : statements.replace.run(next, expiry(ttlSeconds), expiry(ttlSeconds), key, expected);
|
|
93
|
+
sweep();
|
|
94
|
+
return Promise.resolve(written.changes === 1);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
export { createSqliteKv };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -8,6 +8,7 @@ export type ComponentProps = {
|
|
|
8
8
|
plugins?: Record<string, SSRPlugin>;
|
|
9
9
|
ssrResult?: SSRRenderResult;
|
|
10
10
|
sdkDevToolsStylePath?: string;
|
|
11
|
+
sdkIconsStylePath?: string;
|
|
11
12
|
debugMode?: boolean;
|
|
12
13
|
/** Set when the metering adapter degrades this render: the account behind this space is over its quota. */
|
|
13
14
|
overQuota?: boolean;
|
|
@@ -20,5 +21,5 @@ export type ComponentProps = {
|
|
|
20
21
|
/** The flags a tester forced, for a page allowed to debug. See `prepareRender`. */
|
|
21
22
|
forcedFlags?: Record<string, boolean>;
|
|
22
23
|
};
|
|
23
|
-
declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, debugMode, overQuota, theme, state, serverFlags, forcedFlags }: ComponentProps) => import('react').JSX.Element;
|
|
24
|
+
declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, sdkIconsStylePath, debugMode, overQuota, theme, state, serverFlags, forcedFlags }: ComponentProps) => import('react').JSX.Element;
|
|
24
25
|
export default Component;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import PlitziSdk from "@plitzi/plitzi-sdk";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
//#region src/modules/ssr/Component.tsx
|
|
4
|
-
var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, debugMode = false, overQuota, theme, state, serverFlags, forcedFlags }) => {
|
|
4
|
+
var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, sdkIconsStylePath, debugMode = false, overQuota, theme, state, serverFlags, forcedFlags }) => {
|
|
5
5
|
const serverWithResult = {
|
|
6
6
|
...server,
|
|
7
7
|
ssr: {
|
|
@@ -17,6 +17,7 @@ var Component = ({ server, renderMode = "raw", previewMode = true, offlineData,
|
|
|
17
17
|
offlineMode: !!offlineData && Object.keys(offlineData).length > 0,
|
|
18
18
|
offlineData,
|
|
19
19
|
sdkDevToolsStylePath,
|
|
20
|
+
sdkIconsStylePath,
|
|
20
21
|
debugMode,
|
|
21
22
|
...overQuota === void 0 ? {} : { overQuota },
|
|
22
23
|
...theme === void 0 ? {} : { theme },
|