@plitzi/sdk-server 0.38.4 → 0.38.5
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 +333 -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/prepareRender.js +45 -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
|
@@ -9,10 +9,12 @@ import { buildCacheManager } from "../../helpers/cache/cacheManager.js";
|
|
|
9
9
|
import { createServerCaches, destroyServerCaches } from "../../helpers/cache/serverCaches.js";
|
|
10
10
|
import normalizePlugins, { normalizePluginSource } from "../../helpers/normalizePlugins.js";
|
|
11
11
|
import { reportReactBuild } from "../../helpers/reportReactBuild.js";
|
|
12
|
+
import { isFunctionsDefinition } from "../../modules/functions/load.js";
|
|
12
13
|
import { invalidatePluginComponentCache } from "../../modules/ssr/loadPluginComponents.js";
|
|
13
14
|
import { DRAFT_STORE_METHODS, createMemoryDraftStore } from "../../modules/ssr/preview.js";
|
|
14
15
|
import { compileTemplate } from "../../modules/ssr/template.js";
|
|
15
16
|
import { PluginManager } from "../../plugins/manager.js";
|
|
17
|
+
import { watchPluginSources } from "../../plugins/watch.js";
|
|
16
18
|
import { makeHandler } from "../http/dispatcher.js";
|
|
17
19
|
import { createDevReload } from "../http/stages/devReload.js";
|
|
18
20
|
import { buildPagePipeline } from "../services/registry.js";
|
|
@@ -51,6 +53,17 @@ var createPageServer = (config, services, extensions) => {
|
|
|
51
53
|
const actions = actionsModuleFor(config);
|
|
52
54
|
const realtime = realtimeModuleFor(config);
|
|
53
55
|
const devReload = config.devReload ? createDevReload() : void 0;
|
|
56
|
+
/**
|
|
57
|
+
* A plugin whose source changed is built again and handed to the open pages, which swap it where it is drawn — the
|
|
58
|
+
* rest of the page, its state included, stays as it was. Only while developing with `devReload` on: nobody edits a
|
|
59
|
+
* deployment's plugins under it, and a page that does not listen has nothing to swap.
|
|
60
|
+
*/
|
|
61
|
+
const stopWatchingPlugins = devReload && config.devMode ? watchPluginSources(pluginManager, (key) => {
|
|
62
|
+
invalidatePluginComponentCache();
|
|
63
|
+
pluginManager.rebuild(key).then((entry) => {
|
|
64
|
+
if (entry?.js) devReload.plugin(entry);
|
|
65
|
+
});
|
|
66
|
+
}) : void 0;
|
|
54
67
|
const stages = [...devReload ? [devReload.stage] : [], ...buildPagePipeline(services, extensions)];
|
|
55
68
|
const makeHandlerForPort = (port) => {
|
|
56
69
|
const buildContext = (raw, rawRes, req, res) => ({
|
|
@@ -85,9 +98,14 @@ var createPageServer = (config, services, extensions) => {
|
|
|
85
98
|
},
|
|
86
99
|
onClosing: () => {
|
|
87
100
|
realtime?.close();
|
|
101
|
+
stopWatchingPlugins?.();
|
|
88
102
|
devReload?.close();
|
|
89
103
|
},
|
|
90
104
|
...devReload ? { reloadPages: devReload.reload } : {},
|
|
105
|
+
...actions ? { setPluginFunctions: (plugin, definition) => {
|
|
106
|
+
if (definition !== void 0 && !isFunctionsDefinition(definition)) throw new Error(`[SSR] The functions of plugin "${plugin}" are not a definition: defineFunctions({ … })`);
|
|
107
|
+
actions.setPluginFunctions(plugin, definition);
|
|
108
|
+
} } : {},
|
|
91
109
|
onDestroy: async () => {
|
|
92
110
|
await actions?.jobs?.stop();
|
|
93
111
|
destroyServerCaches(caches);
|
|
@@ -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 { spaceDocumentStage } from "./spaceDocument.js";
|
|
5
6
|
import { imagesStage } from "../http/stages/images.js";
|
|
6
7
|
import { notFoundStage, ssrStage } from "./ssr.js";
|
|
7
8
|
import { authRoutesStages } from "../http/stages/authRoutes.js";
|
|
@@ -30,6 +31,7 @@ var buildPagePipeline = (services, extensions = {}) => {
|
|
|
30
31
|
stages.push(realtimeStage);
|
|
31
32
|
stages.push(...extensions.data ?? []);
|
|
32
33
|
stages.push(functionRoutesStage);
|
|
34
|
+
if (services.ssr) stages.push(spaceDocumentStage);
|
|
33
35
|
stages.push(services.ssr ? ssrStage : notFoundStage);
|
|
34
36
|
return stages;
|
|
35
37
|
};
|
|
@@ -6,7 +6,7 @@ var rscStage = async (ctx) => {
|
|
|
6
6
|
const rscPath = resolveRscEndpoint(config);
|
|
7
7
|
if (!rscPath || req.method !== "GET" || req.path !== rscPath) return false;
|
|
8
8
|
ctx.operation = "rsc";
|
|
9
|
-
await handleRsc(req, ctx.res, config, ctx.pluginManager, ctx.caches.rsc);
|
|
9
|
+
await handleRsc(req, ctx.res, config, ctx.pluginManager, ctx.caches.rsc, ctx.signal);
|
|
10
10
|
return true;
|
|
11
11
|
};
|
|
12
12
|
//#endregion
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { handleSpaceDocument } from "../../modules/ssr/spaceDocument.js";
|
|
2
|
+
//#region src/core/services/spaceDocument.ts
|
|
3
|
+
var spaceDocumentStage = async (ctx) => {
|
|
4
|
+
const { config, req } = ctx;
|
|
5
|
+
if (req.method !== "GET" || !req.path.startsWith("/_plitzi/space/")) return false;
|
|
6
|
+
ctx.operation = "space";
|
|
7
|
+
await handleSpaceDocument(req, ctx.res, config, ctx.caches.offlineData);
|
|
8
|
+
return true;
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { spaceDocumentStage };
|
package/dist/functions-api.d.ts
CHANGED
|
@@ -200,6 +200,11 @@ declare type InteractionCallbackParam<T extends Record<string, unknown> = Record
|
|
|
200
200
|
} & ({
|
|
201
201
|
type: 'text';
|
|
202
202
|
defaultValue?: string | number;
|
|
203
|
+
}
|
|
204
|
+
/** Written as text — a number or a template — and handed to the callback as a number. */
|
|
205
|
+
| {
|
|
206
|
+
type: 'number';
|
|
207
|
+
defaultValue?: number;
|
|
203
208
|
} | {
|
|
204
209
|
type: 'textarea';
|
|
205
210
|
defaultValue?: string | number;
|
|
@@ -244,7 +249,7 @@ declare type InteractionCallbackParam<T extends Record<string, unknown> = Record
|
|
|
244
249
|
|
|
245
250
|
declare type InteractionCallbackParamValues<T extends Record<string, unknown> = Record<string, unknown>> = T;
|
|
246
251
|
|
|
247
|
-
declare type InteractionParamType = 'boolean' | 'select' | 'text' | 'textarea' | 'codemirror-text' | 'codemirror-json' | 'elements';
|
|
252
|
+
declare type InteractionParamType = 'boolean' | 'select' | 'text' | 'number' | 'textarea' | 'codemirror-text' | 'codemirror-json' | 'elements';
|
|
248
253
|
|
|
249
254
|
/**
|
|
250
255
|
* The lifetime a change writes with: a number of seconds, one worked out from the value written, or none.
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* and the isolates both are made of. Its own entry, apart from the contract, so a space's code and a self-hosted
|
|
4
4
|
* server's native functions never load a line of it — nor need `isolated-vm` or `core-js`, which only the process
|
|
5
5
|
* that runs isolates installs.
|
|
6
|
+
*
|
|
7
|
+
* `buildFunctions` is the build every runner runs first, on its own: what `plitzi doctor` checks a project's
|
|
8
|
+
* `src/functions/` with, by the rules of the sdk-server the project installed.
|
|
6
9
|
*/
|
|
7
10
|
export { startFunctionsRunnerService } from './modules/functions/runner/service';
|
|
8
11
|
export type { FunctionsRunnerService, FunctionsRunnerServiceOptions } from './modules/functions/runner/service';
|
|
@@ -13,3 +16,5 @@ export type { IsolateRunnerOptions } from './modules/functions/sandbox/isolate';
|
|
|
13
16
|
export { FUNCTIONS_PROTOCOL, FunctionFailure } from './modules/functions/protocol';
|
|
14
17
|
export { createLocalFunctions } from './modules/functions/local';
|
|
15
18
|
export type { LocalFunctions, LocalFunctionsOptions } from './modules/functions/local';
|
|
19
|
+
export { buildFunctions, FunctionsBuildError } from './modules/functions/build';
|
|
20
|
+
export type { FunctionsBuild, FunctionsSource } from './modules/functions/build';
|
package/dist/functionsRunner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FUNCTIONS_PROTOCOL, FunctionFailure } from "./modules/functions/protocol.js";
|
|
2
|
+
import { FunctionsBuildError, buildFunctions } from "./modules/functions/build.js";
|
|
2
3
|
import { createIsolateRunner } from "./modules/functions/sandbox/isolate.js";
|
|
3
4
|
import { startFunctionsRunnerService } from "./modules/functions/runner/service.js";
|
|
4
5
|
import { createRemoteRunner } from "./modules/functions/runner/remote.js";
|
|
5
6
|
import { createLocalFunctions } from "./modules/functions/local.js";
|
|
6
|
-
export { FUNCTIONS_PROTOCOL, FunctionFailure, createIsolateRunner, createLocalFunctions, createRemoteRunner, startFunctionsRunnerService };
|
|
7
|
+
export { FUNCTIONS_PROTOCOL, FunctionFailure, FunctionsBuildError, buildFunctions, createIsolateRunner, createLocalFunctions, createRemoteRunner, startFunctionsRunnerService };
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { OfflineDataRaw } from '@plitzi/sdk-shared';
|
|
2
|
+
/** The space as a page reads it, and the name of exactly that text. */
|
|
3
|
+
export type SpaceDocument = {
|
|
4
|
+
json: string;
|
|
5
|
+
hash: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The space as the page reads it — escaped JSON, safe inside a `<script>` — made once per space object rather than
|
|
9
|
+
* once per render.
|
|
10
|
+
*
|
|
11
|
+
* It is most of the payload, and the adapters hand every request the same object until the space changes, so turning
|
|
12
|
+
* it into JSON on each render was a tenth of a render's CPU spent producing the same string. Keyed weakly: a space the
|
|
13
|
+
* adapters let go of takes its string with it.
|
|
14
|
+
*
|
|
15
|
+
* The compiled stylesheet is left out when the page's own runtime stylesheet carries it in a form the browser reads
|
|
16
|
+
* back exactly (`styleCacheTravelsInDocument`): it is the largest part of the style document, and the page would
|
|
17
|
+
* otherwise ship it twice. A fact of the space alone, so the document is the same wherever it is asked for.
|
|
18
|
+
*
|
|
19
|
+
* The hash names the text, so a page can be told where to fetch it and the answer can be kept forever.
|
|
20
|
+
*/
|
|
21
|
+
export declare const spaceDocument: (offlineData: OfflineDataRaw) => SpaceDocument;
|
|
2
22
|
/**
|
|
3
23
|
* What the page's bootstrap reads: `{ offlineData, ...rest }` as escaped JSON, byte for byte what serializing the
|
|
4
24
|
* whole object would give. The escaping replaces characters one by one, so escaping the parts and joining them is
|
|
5
25
|
* the same as escaping the whole.
|
|
6
26
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* part of the style document, and the page would otherwise ship it twice.
|
|
27
|
+
* `apart`, the space is not in it: the page fetches it (`spaceDocumentPath`), and only what is this request's travels
|
|
28
|
+
* inline. `styleCacheInDocument` tells the bootstrap the stylesheet is in the page either way.
|
|
10
29
|
*/
|
|
11
|
-
export declare const hydrationPayload: (offlineData: OfflineDataRaw | undefined, rest: Record<string, unknown
|
|
30
|
+
export declare const hydrationPayload: (offlineData: OfflineDataRaw | undefined, rest: Record<string, unknown>, { apart }?: {
|
|
31
|
+
apart?: boolean;
|
|
32
|
+
}) => string;
|
|
@@ -1,51 +1,57 @@
|
|
|
1
1
|
import { escapeJson } from "./escapeJson.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { styleCacheTravelsInDocument } from "@plitzi/sdk-shared/style";
|
|
3
4
|
//#region src/helpers/hydrationPayload.ts
|
|
4
|
-
var
|
|
5
|
+
var documents = /* @__PURE__ */ new WeakMap();
|
|
5
6
|
/**
|
|
6
|
-
* The space as the page
|
|
7
|
+
* The space as the page reads it — escaped JSON, safe inside a `<script>` — made once per space object rather than
|
|
8
|
+
* once per render.
|
|
7
9
|
*
|
|
8
|
-
* It is most of the payload
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* It is most of the payload, and the adapters hand every request the same object until the space changes, so turning
|
|
11
|
+
* it into JSON on each render was a tenth of a render's CPU spent producing the same string. Keyed weakly: a space the
|
|
12
|
+
* adapters let go of takes its string with it.
|
|
13
|
+
*
|
|
14
|
+
* The compiled stylesheet is left out when the page's own runtime stylesheet carries it in a form the browser reads
|
|
15
|
+
* back exactly (`styleCacheTravelsInDocument`): it is the largest part of the style document, and the page would
|
|
16
|
+
* otherwise ship it twice. A fact of the space alone, so the document is the same wherever it is asked for.
|
|
17
|
+
*
|
|
18
|
+
* The hash names the text, so a page can be told where to fetch it and the answer can be kept forever.
|
|
11
19
|
*/
|
|
12
|
-
var
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return serialized.whole;
|
|
20
|
+
var spaceDocument = (offlineData) => {
|
|
21
|
+
const known = documents.get(offlineData);
|
|
22
|
+
if (known) return known;
|
|
23
|
+
const space = styleCacheTravelsInDocument(offlineData.style.cache) ? {
|
|
24
|
+
...offlineData,
|
|
25
|
+
style: {
|
|
26
|
+
...offlineData.style,
|
|
27
|
+
cache: ""
|
|
28
|
+
}
|
|
29
|
+
} : offlineData;
|
|
30
|
+
const json = escapeJson(JSON.stringify(space));
|
|
31
|
+
const document = {
|
|
32
|
+
json,
|
|
33
|
+
hash: createHash("sha256").update(json).digest("base64url").slice(0, 22)
|
|
34
|
+
};
|
|
35
|
+
documents.set(offlineData, document);
|
|
36
|
+
return document;
|
|
30
37
|
};
|
|
31
38
|
/**
|
|
32
39
|
* What the page's bootstrap reads: `{ offlineData, ...rest }` as escaped JSON, byte for byte what serializing the
|
|
33
40
|
* whole object would give. The escaping replaces characters one by one, so escaping the parts and joining them is
|
|
34
41
|
* the same as escaping the whole.
|
|
35
42
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* part of the style document, and the page would otherwise ship it twice.
|
|
43
|
+
* `apart`, the space is not in it: the page fetches it (`spaceDocumentPath`), and only what is this request's travels
|
|
44
|
+
* inline. `styleCacheInDocument` tells the bootstrap the stylesheet is in the page either way.
|
|
39
45
|
*/
|
|
40
|
-
var hydrationPayload = (offlineData, rest) => {
|
|
46
|
+
var hydrationPayload = (offlineData, rest, { apart = false } = {}) => {
|
|
41
47
|
const inDocument = offlineData !== void 0 && styleCacheTravelsInDocument(offlineData.style.cache);
|
|
42
48
|
const serializedRest = escapeJson(JSON.stringify(inDocument ? {
|
|
43
49
|
...rest,
|
|
44
50
|
styleCacheInDocument: true
|
|
45
51
|
} : rest));
|
|
46
|
-
if (offlineData === void 0) return serializedRest;
|
|
52
|
+
if (offlineData === void 0 || apart) return serializedRest;
|
|
47
53
|
const separator = serializedRest === "{}" ? "" : ",";
|
|
48
|
-
return `{"offlineData":${
|
|
54
|
+
return `{"offlineData":${spaceDocument(offlineData).json}${separator}${serializedRest.slice(1)}`;
|
|
49
55
|
};
|
|
50
56
|
//#endregion
|
|
51
|
-
export { hydrationPayload };
|
|
57
|
+
export { hydrationPayload, spaceDocument };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createServer, resolveServices } from './core/createServer';
|
|
2
2
|
export { closeOnSignals } from './core/server/closeOnSignals';
|
|
3
|
-
export { loadFunctions, readFunctionsDir } from './modules/functions/load';
|
|
3
|
+
export { loadFunctions, loadFunctionsSource, readFunctionsDir } from './modules/functions/load';
|
|
4
4
|
export { freePort } from './core/freePort';
|
|
5
5
|
export { registerHealthCheck, buildHealthPayload } from './core/health';
|
|
6
6
|
export { consoleLogger, isLogged, logLevelOf, renderLogEvent, serverLog } from './helpers/serverLog';
|
|
@@ -25,6 +25,6 @@ export type { ConnectorAuth, ConnectorCredential, ConnectorEndpoints, ConnectorF
|
|
|
25
25
|
export type { FontStore, StoredFont } from './core/fontStore';
|
|
26
26
|
export type { PubSubAdapter, SSRRealtimeConfig } from '@plitzi/sdk-shared';
|
|
27
27
|
export type { RedisPubSubClients } from './modules/realtime/redisPubSub';
|
|
28
|
-
export type { ResolvedServices } from './core/createServer';
|
|
28
|
+
export type { ResolvedServices, ServerConfig } from './core/createServer';
|
|
29
29
|
export type { Closable, CloseOnSignalsOptions } from './core/server/closeOnSignals';
|
|
30
30
|
export type { BaseContext, PipelineExtensions, SSRContext, Stage } from './core/http/types';
|
package/dist/index.js
CHANGED
|
@@ -3,13 +3,13 @@ import { fetchConnectorRecords, rebaseMedia, writeConnectorRecord } from "./modu
|
|
|
3
3
|
import { createMemoryPubSub } from "./modules/realtime/memoryPubSub.js";
|
|
4
4
|
import { createConnectorResolver } from "./modules/connectors/resolver.js";
|
|
5
5
|
import { resolveRscData } from "./modules/rsc/resolveRscData.js";
|
|
6
|
+
import { loadFunctions, loadFunctionsSource, readFunctionsDir } from "./modules/functions/load.js";
|
|
6
7
|
import { PREVIEW_TOKEN_PARAM } from "./core/previewToken.js";
|
|
7
8
|
import { createMemoryDraftStore, takeDraftOverride } from "./modules/ssr/preview.js";
|
|
8
9
|
import { resolveServices } from "./core/services/resolve.js";
|
|
9
10
|
import { buildHealthPayload, registerHealthCheck } from "./core/health.js";
|
|
10
11
|
import { createServer } from "./core/createServer.js";
|
|
11
12
|
import { closeOnSignals } from "./core/server/closeOnSignals.js";
|
|
12
|
-
import { loadFunctions, readFunctionsDir } from "./modules/functions/load.js";
|
|
13
13
|
import { freePort } from "./core/freePort.js";
|
|
14
14
|
import { createCloudAdapters } from "./adapters/cloudAdapters.js";
|
|
15
15
|
import { createJsonAdapters } from "./adapters/jsonAdapters.js";
|
|
@@ -19,4 +19,4 @@ import { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildc
|
|
|
19
19
|
import { createSpaceAdapters } from "./adapters/space/index.js";
|
|
20
20
|
import { createLocalFontStore, hostedPathsOf } from "./core/fontStore.js";
|
|
21
21
|
import { createRedisPubSub } from "./modules/realtime/redisPubSub.js";
|
|
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 };
|
|
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, loadFunctionsSource, logLevelOf, presentsCredential, readFunctionsDir, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, serverLog, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
@@ -3,8 +3,10 @@ import { RunGuards } from './runtime/guards';
|
|
|
3
3
|
import { ActionRunner } from './runtime/runAction';
|
|
4
4
|
import { ActionKvStore, ActionsConfig, ActionTaskRegistry, ResolvedActionLimits, SpaceRevision } from './types';
|
|
5
5
|
import { FunctionsSource } from '../functions/build';
|
|
6
|
+
import { FunctionsDefinition } from '../functions/contract';
|
|
6
7
|
import { ManifestReading } from '../functions/manifest';
|
|
7
8
|
import { RouteHandler, RouteVisit } from '../functions/routes';
|
|
9
|
+
import { FunctionScope } from '../functions/scope';
|
|
8
10
|
import { PreparedFunctions } from '../functions/space';
|
|
9
11
|
import { ActionDocument } from '@plitzi/sdk-shared';
|
|
10
12
|
export type ActionsModule = ActionRunner & {
|
|
@@ -17,9 +19,10 @@ export type ActionsModule = ActionRunner & {
|
|
|
17
19
|
registryFor: (spaceId: number, at?: SpaceRevision) => Promise<ActionTaskRegistry>;
|
|
18
20
|
/**
|
|
19
21
|
* A space's functions source, built, read and checked — ready to store, or the problems to show where they are. The
|
|
20
|
-
* one way a space's functions become storable; refused when this server has no runner.
|
|
22
|
+
* one way a space's functions become storable; refused when this server has no runner. With a `scope`, a plugin's
|
|
23
|
+
* server half: its tasks named after the plugin, its routes its own.
|
|
21
24
|
*/
|
|
22
|
-
prepareFunctions: (source: FunctionsSource) => Promise<PreparedFunctions>;
|
|
25
|
+
prepareFunctions: (source: FunctionsSource, scope?: FunctionScope) => Promise<PreparedFunctions>;
|
|
23
26
|
/**
|
|
24
27
|
* What a space's code declares, read by every rule a saved space's functions meet — for code that is not built here:
|
|
25
28
|
* a space runtime's, described by the runtime itself when it starts. The manifest, and the problems that refuse it.
|
|
@@ -36,6 +39,12 @@ export type ActionsModule = ActionRunner & {
|
|
|
36
39
|
kv: (spaceId: number) => ActionKvStore;
|
|
37
40
|
/** What this server will allow one run of that document to spend — the deployment's ceilings, tightened by it. */
|
|
38
41
|
limitsFor: (document: ActionDocument) => ResolvedActionLimits;
|
|
42
|
+
/**
|
|
43
|
+
* Puts the server half of a plugin this server ships in place — or takes it away, with none — while it runs: what a
|
|
44
|
+
* development server does when a plugin's `functions/` change, instead of restarting. Checked as at boot: a task not
|
|
45
|
+
* named after the plugin throws, and nothing changes.
|
|
46
|
+
*/
|
|
47
|
+
setPluginFunctions: (plugin: string, definition: FunctionsDefinition | undefined) => void;
|
|
39
48
|
/**
|
|
40
49
|
* Scheduled runs: the queue, the sweep that fills it and the workers that drain it.
|
|
41
50
|
*
|
|
@@ -47,15 +47,29 @@ var withFleetKv = (config) => {
|
|
|
47
47
|
};
|
|
48
48
|
var createActionsModule = (given) => {
|
|
49
49
|
const config = withFleetKv(given);
|
|
50
|
-
const
|
|
50
|
+
const plugins = new Map(Object.entries(config.functions?.plugins ?? {}));
|
|
51
|
+
const build = (from) => createTaskRegistry(config.functions?.native, {
|
|
52
|
+
db: (config.dbDrivers?.length ?? 0) > 0,
|
|
53
|
+
plugins: Object.fromEntries(from)
|
|
54
|
+
});
|
|
55
|
+
let current = build(plugins);
|
|
56
|
+
const registry = {
|
|
57
|
+
get: (name) => current.get(name),
|
|
58
|
+
list: () => current.list()
|
|
59
|
+
};
|
|
51
60
|
const spaceRegistries = createSpaceRegistries(registry, config.functions);
|
|
52
|
-
const reserved = new Set(
|
|
53
|
-
const registryFor = async (spaceId, at) =>
|
|
61
|
+
const reserved = () => new Set(current.list().map((task) => task.namespace));
|
|
62
|
+
const registryFor = async (spaceId, at) => {
|
|
63
|
+
const [functions, plugins] = await Promise.all([config.lookups.getFunctions?.(spaceId, at), config.lookups.getPluginFunctions?.(spaceId, at)]);
|
|
64
|
+
return spaceRegistries.registryFor(functions, plugins);
|
|
65
|
+
};
|
|
54
66
|
const registryForRun = (request) => Object.values(request.entry.document.nodes).every((node) => node.type !== "task" || !node.action || registry.get(node.action)) ? registry : registryFor(request.spaceId, request.at);
|
|
55
67
|
const { runAction, taskContext } = createActionRunner(config, registryForRun, config.fetchImpl);
|
|
56
68
|
const routes = createRoutes({
|
|
57
69
|
config: config.functions ?? {},
|
|
70
|
+
plugins: () => plugins,
|
|
58
71
|
...config.lookups.getFunctions ? { getFunctions: config.lookups.getFunctions } : {},
|
|
72
|
+
...config.lookups.getPluginFunctions ? { getPluginFunctions: config.lookups.getPluginFunctions } : {},
|
|
59
73
|
taskContext
|
|
60
74
|
});
|
|
61
75
|
/**
|
|
@@ -71,17 +85,25 @@ var createActionsModule = (given) => {
|
|
|
71
85
|
registry,
|
|
72
86
|
registryFor,
|
|
73
87
|
routeFor: routes.routeFor,
|
|
74
|
-
prepareFunctions: (source) => {
|
|
88
|
+
prepareFunctions: (source, scope) => {
|
|
75
89
|
const runner = config.functions?.runner;
|
|
76
|
-
return runner ? prepareFunctions(source, runner, reserved, functionCeilings(config.functions?.limits)) : Promise.resolve({
|
|
90
|
+
return runner ? prepareFunctions(source, runner, reserved(), functionCeilings(config.functions?.limits), scope) : Promise.resolve({
|
|
77
91
|
ok: false,
|
|
78
92
|
problems: [{ message: "This server runs no space functions" }]
|
|
79
93
|
});
|
|
80
94
|
},
|
|
81
|
-
readManifest: (declared) => readManifest(declared, reserved, functionCeilings(config.functions?.limits)),
|
|
95
|
+
readManifest: (declared) => readManifest(declared, reserved(), functionCeilings(config.functions?.limits)),
|
|
82
96
|
guards,
|
|
83
97
|
kv: (spaceId) => namespaceKv(kv, spaceId),
|
|
84
|
-
limitsFor
|
|
98
|
+
limitsFor,
|
|
99
|
+
setPluginFunctions: (plugin, definition) => {
|
|
100
|
+
const next = new Map(plugins);
|
|
101
|
+
if (definition) next.set(plugin, definition);
|
|
102
|
+
else next.delete(plugin);
|
|
103
|
+
current = build(next);
|
|
104
|
+
plugins.clear();
|
|
105
|
+
next.forEach((value, key) => plugins.set(key, value));
|
|
106
|
+
}
|
|
85
107
|
};
|
|
86
108
|
if (config.jobs !== false && config.lookups.listActions) module.jobs = createActionJobs(config.jobs ?? {}, module, config.lookups);
|
|
87
109
|
return module;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ActionKvAdapter } from '../types';
|
|
2
|
+
export type FileKvOptions = {
|
|
3
|
+
/** The JSON file the store lives in — created, with its folder, on the first write. */
|
|
4
|
+
file: string;
|
|
5
|
+
};
|
|
6
|
+
export type FileKv = ActionKvAdapter & {
|
|
7
|
+
/** Resolves once everything written so far is on disk: for a shutdown, or a test that reads the file. */
|
|
8
|
+
flush: () => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* A key/value store in one JSON file: the `kv` server actions keep — saved layouts, counters, a source's cached answer —
|
|
12
|
+
* surviving a restart, for a deployment with no database. Nothing to install, and a file anybody can read, back up or
|
|
13
|
+
* edit by hand while the server is stopped.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* createServer({ action: { kv: createFileKv({ file: 'state/kv.json' }) } });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The rules are the in-process store's, to the letter (`createMapKv`): the file is only where the map is kept. Every
|
|
20
|
+
* change is written whole, through a temporary file renamed over the old one, as soon as the change is made — changes
|
|
21
|
+
* that arrive while a write is under way are written together right after it, so a crash loses at most the writes
|
|
22
|
+
* of that instant and never leaves half a file. Entries past their lifetime are not written.
|
|
23
|
+
*
|
|
24
|
+
* ONE process: it reads the file when it starts and owns it after. Two processes on one file overwrite each other —
|
|
25
|
+
* replicas, or a server started with `workers`, use `createSqliteKv` (`@plitzi/sdk-server/sqlite`) or a database.
|
|
26
|
+
* And a whole file per change: right for what a site keeps, not for a counter bumped thousands of times a second.
|
|
27
|
+
*/
|
|
28
|
+
export declare const createFileKv: ({ file }: FileKvOptions) => FileKv;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
|
+
import { createMapKv } from "./memoryKv.js";
|
|
3
|
+
import { writeFileAtomic } from "../../../helpers/atomicFile.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
//#region src/modules/actions/runtime/fileKv.ts
|
|
7
|
+
var isEntry = (value) => typeof value === "object" && value !== null && "value" in value && typeof value.value === "string" && (!("expiresAt" in value) || typeof value.expiresAt === "number");
|
|
8
|
+
var load = (file) => {
|
|
9
|
+
if (!fs.existsSync(file)) return /* @__PURE__ */ new Map();
|
|
10
|
+
const refuse = (why) => /* @__PURE__ */ new Error(`createFileKv: ${file} is not a store it wrote (${why}). Move it aside, or point \`file\` elsewhere.`);
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
throw refuse(error instanceof Error ? error.message : String(error));
|
|
16
|
+
}
|
|
17
|
+
if (typeof parsed !== "object" || parsed === null || !("format" in parsed) || parsed.format !== 1) throw refuse("no `format: 1`");
|
|
18
|
+
const entries = "entries" in parsed && typeof parsed.entries === "object" ? parsed.entries : null;
|
|
19
|
+
if (entries === null) throw refuse("no `entries`");
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const kept = Object.entries(entries).filter((pair) => isEntry(pair[1]) && (pair[1].expiresAt === void 0 || pair[1].expiresAt > now));
|
|
22
|
+
return new Map(kept);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A key/value store in one JSON file: the `kv` server actions keep — saved layouts, counters, a source's cached answer —
|
|
26
|
+
* surviving a restart, for a deployment with no database. Nothing to install, and a file anybody can read, back up or
|
|
27
|
+
* edit by hand while the server is stopped.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* createServer({ action: { kv: createFileKv({ file: 'state/kv.json' }) } });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* The rules are the in-process store's, to the letter (`createMapKv`): the file is only where the map is kept. Every
|
|
34
|
+
* change is written whole, through a temporary file renamed over the old one, as soon as the change is made — changes
|
|
35
|
+
* that arrive while a write is under way are written together right after it, so a crash loses at most the writes
|
|
36
|
+
* of that instant and never leaves half a file. Entries past their lifetime are not written.
|
|
37
|
+
*
|
|
38
|
+
* ONE process: it reads the file when it starts and owns it after. Two processes on one file overwrite each other —
|
|
39
|
+
* replicas, or a server started with `workers`, use `createSqliteKv` (`@plitzi/sdk-server/sqlite`) or a database.
|
|
40
|
+
* And a whole file per change: right for what a site keeps, not for a counter bumped thousands of times a second.
|
|
41
|
+
*/
|
|
42
|
+
var createFileKv = ({ file }) => {
|
|
43
|
+
const entries = load(file);
|
|
44
|
+
let dirty = false;
|
|
45
|
+
let failed = false;
|
|
46
|
+
/** The write under way, resolving to whether everything it took on reached the disk. */
|
|
47
|
+
let writing;
|
|
48
|
+
const document = () => {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const live = [...entries].filter(([, entry]) => entry.expiresAt === void 0 || entry.expiresAt > now);
|
|
51
|
+
const body = {
|
|
52
|
+
format: 1,
|
|
53
|
+
entries: Object.fromEntries(live)
|
|
54
|
+
};
|
|
55
|
+
return `${JSON.stringify(body, null, 2)}\n`;
|
|
56
|
+
};
|
|
57
|
+
const drain = async () => {
|
|
58
|
+
while (dirty) {
|
|
59
|
+
dirty = false;
|
|
60
|
+
try {
|
|
61
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
62
|
+
await writeFileAtomic(file, document());
|
|
63
|
+
} catch (error) {
|
|
64
|
+
dirty = true;
|
|
65
|
+
failed = true;
|
|
66
|
+
serverLog.error("Actions", `kv file ${file} could not be written`, error);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
const write = () => {
|
|
73
|
+
writing ??= new Promise((resolve) => {
|
|
74
|
+
queueMicrotask(resolve);
|
|
75
|
+
}).then(drain).finally(() => {
|
|
76
|
+
writing = void 0;
|
|
77
|
+
if (dirty && !failed) write();
|
|
78
|
+
});
|
|
79
|
+
return writing;
|
|
80
|
+
};
|
|
81
|
+
const changed = () => {
|
|
82
|
+
dirty = true;
|
|
83
|
+
failed = false;
|
|
84
|
+
write();
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
...createMapKv(entries, changed),
|
|
88
|
+
flush: async () => {
|
|
89
|
+
failed = false;
|
|
90
|
+
while (writing !== void 0 || dirty) if (!await (writing ?? write())) throw new Error(`createFileKv: ${file} could not be written; the reason is in the log.`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
//#endregion
|
|
95
|
+
export { createFileKv };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { StoreMethods } from '../../../core/server/fleet/channel';
|
|
2
2
|
import { ActionKvAdapter } from '../types';
|
|
3
|
+
/** A value and, when it has one, the instant (epoch ms) it stops being one. */
|
|
4
|
+
export type KvEntry = {
|
|
5
|
+
value: string;
|
|
6
|
+
expiresAt?: number;
|
|
7
|
+
};
|
|
3
8
|
/** Every method of the store, for the copy a server's workers share (see `fleet/stores.ts`). */
|
|
4
9
|
export declare const KV_METHODS: StoreMethods<ActionKvAdapter>;
|
|
10
|
+
/**
|
|
11
|
+
* The store's rules over a map somebody holds: what `createMemoryKv` is, and what a store that keeps the map
|
|
12
|
+
* somewhere (`createFileKv`) is too — so the two answer identically and differ only in where the map lives.
|
|
13
|
+
*
|
|
14
|
+
* An adapter like any other: it stores strings and obeys no rules of its own. What a counter DOES lives in
|
|
15
|
+
* `createKvStore`, so the in-process store and a deployment's own behave identically rather than nearly so.
|
|
16
|
+
*
|
|
17
|
+
* `changed` is told after every write that changed the map — an expired entry swept or read past included.
|
|
18
|
+
*/
|
|
19
|
+
export declare const createMapKv: (entries: Map<string, KvEntry>, changed?: () => void) => ActionKvAdapter;
|
|
20
|
+
/**
|
|
21
|
+
* The `kv` tasks' fallback store: in this process, and nowhere else — gone when it stops.
|
|
22
|
+
*
|
|
23
|
+
* Real enough for a single replica and for local work, and deliberately not pretending otherwise — a deployment
|
|
24
|
+
* running several replicas passes an adapter over something shared, because a counter that only counts its own
|
|
25
|
+
* replica is a rate limit that multiplies by the number of them. One that must survive a restart passes
|
|
26
|
+
* `createFileKv` (one process) or `createSqliteKv` (`@plitzi/sdk-server/sqlite`).
|
|
27
|
+
*/
|
|
5
28
|
export declare const createMemoryKv: () => ActionKvAdapter;
|