@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/README.md +126 -17
  3. package/dist/actions.d.ts +8 -5
  4. package/dist/actions.js +3 -1
  5. package/dist/adapters/jsonAdapters.d.ts +1 -1
  6. package/dist/adapters/jsonAdapters.js +16 -3
  7. package/dist/core/configSeam.js +20 -4
  8. package/dist/core/createServer.js +3 -2
  9. package/dist/core/handlers/socialRouteHandlers.js +6 -1
  10. package/dist/core/http/stages/devReload.d.ts +11 -0
  11. package/dist/core/http/stages/devReload.js +17 -0
  12. package/dist/core/server/baseServer.d.ts +2 -0
  13. package/dist/core/server/baseServer.js +3 -0
  14. package/dist/core/server/pageServer.js +18 -0
  15. package/dist/core/services/registry.js +2 -0
  16. package/dist/core/services/rsc.js +1 -1
  17. package/dist/core/services/spaceDocument.d.ts +2 -0
  18. package/dist/core/services/spaceDocument.js +11 -0
  19. package/dist/functions-api.d.ts +6 -1
  20. package/dist/functionsRunner.d.ts +5 -0
  21. package/dist/functionsRunner.js +2 -1
  22. package/dist/helpers/hydrationPayload.d.ts +25 -4
  23. package/dist/helpers/hydrationPayload.js +36 -30
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +2 -2
  26. package/dist/modules/actions/index.d.ts +11 -2
  27. package/dist/modules/actions/index.js +29 -7
  28. package/dist/modules/actions/runtime/fileKv.d.ts +28 -0
  29. package/dist/modules/actions/runtime/fileKv.js +95 -0
  30. package/dist/modules/actions/runtime/fileKv.test.d.ts +1 -0
  31. package/dist/modules/actions/runtime/memoryKv.d.ts +23 -0
  32. package/dist/modules/actions/runtime/memoryKv.js +39 -24
  33. package/dist/modules/actions/runtime/namespaceKv.d.ts +2 -0
  34. package/dist/modules/actions/runtime/namespaceKv.js +5 -3
  35. package/dist/modules/actions/runtime/renderResolver.d.ts +3 -2
  36. package/dist/modules/actions/runtime/renderResolver.js +13 -9
  37. package/dist/modules/actions/runtime/renderShare.d.ts +7 -24
  38. package/dist/modules/actions/runtime/renderShare.js +47 -13
  39. package/dist/modules/actions/runtime/runAction.js +15 -1
  40. package/dist/modules/actions/runtime/scope.js +16 -1
  41. package/dist/modules/actions/runtime/signing.js +4 -1
  42. package/dist/modules/actions/runtime/taskContext.d.ts +7 -1
  43. package/dist/modules/actions/runtime/taskContext.js +6 -2
  44. package/dist/modules/actions/tasks/registry.d.ts +3 -1
  45. package/dist/modules/actions/tasks/registry.js +15 -1
  46. package/dist/modules/actions/types.d.ts +15 -2
  47. package/dist/modules/functions/config.d.ts +6 -0
  48. package/dist/modules/functions/context.d.ts +4 -2
  49. package/dist/modules/functions/context.js +43 -21
  50. package/dist/modules/functions/load.d.ts +8 -1
  51. package/dist/modules/functions/load.js +8 -4
  52. package/dist/modules/functions/manifest.d.ts +4 -1
  53. package/dist/modules/functions/manifest.js +14 -2
  54. package/dist/modules/functions/native.d.ts +2 -1
  55. package/dist/modules/functions/native.js +2 -2
  56. package/dist/modules/functions/plugins.test.d.ts +1 -0
  57. package/dist/modules/functions/routes.d.ts +8 -3
  58. package/dist/modules/functions/routes.js +41 -21
  59. package/dist/modules/functions/scope.d.ts +30 -0
  60. package/dist/modules/functions/scope.js +28 -0
  61. package/dist/modules/functions/space.d.ts +10 -5
  62. package/dist/modules/functions/space.js +37 -15
  63. package/dist/modules/rsc/connectorRscData.d.ts +3 -1
  64. package/dist/modules/rsc/connectorRscData.js +8 -4
  65. package/dist/modules/rsc/handler.d.ts +3 -16
  66. package/dist/modules/rsc/handler.js +10 -4
  67. package/dist/modules/rsc/publicFileResolver.d.ts +15 -12
  68. package/dist/modules/rsc/publicFileResolver.js +56 -24
  69. package/dist/modules/rsc/resolveRscData.d.ts +3 -1
  70. package/dist/modules/rsc/resolveRscData.js +8 -5
  71. package/dist/modules/runtime/bundle.d.ts +5 -0
  72. package/dist/modules/runtime/bundle.js +20 -5
  73. package/dist/modules/sqlite/kv.d.ts +34 -0
  74. package/dist/modules/sqlite/kv.js +99 -0
  75. package/dist/modules/sqlite/kv.test.d.ts +1 -0
  76. package/dist/modules/ssr/prepareRender.js +45 -6
  77. package/dist/modules/ssr/spaceDocument.d.ts +25 -0
  78. package/dist/modules/ssr/spaceDocument.js +72 -0
  79. package/dist/modules/ssr/spaceDocument.test.d.ts +1 -0
  80. package/dist/modules/ssr/testing/offlineData.d.ts +5 -0
  81. package/dist/modules/ssr/views/template.ejs +122 -20
  82. package/dist/plugins/compile.js +6 -21
  83. package/dist/plugins/compile.test.d.ts +1 -0
  84. package/dist/plugins/copy.d.ts +5 -0
  85. package/dist/plugins/copy.js +16 -1
  86. package/dist/plugins/copy.test.d.ts +1 -0
  87. package/dist/plugins/manager.d.ts +13 -0
  88. package/dist/plugins/manager.js +40 -11
  89. package/dist/plugins/watch.d.ts +12 -0
  90. package/dist/plugins/watch.js +79 -0
  91. package/dist/runtime.d.ts +1 -1
  92. package/dist/runtime.js +2 -2
  93. package/dist/sqlite.d.ts +17 -0
  94. package/dist/sqlite.js +2 -0
  95. package/package.json +8 -3
  96. package/skills/plitzi-authoring/CHEATSHEET.md +4 -2
  97. package/skills/plitzi-authoring/SKILL.md +20 -19
  98. package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
  99. package/skills/plitzi-authoring/recipes/server-data.ts +3 -3
  100. package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
  101. package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
  102. package/skills/plitzi-authoring/reference/authoring-errors.md +8 -2
  103. package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -1
  104. package/skills/plitzi-authoring/reference/data-and-visibility.md +24 -22
  105. package/skills/plitzi-authoring/reference/drawing.md +24 -0
  106. package/skills/plitzi-authoring/reference/efficiency.md +19 -2
  107. package/skills/plitzi-authoring/reference/flows.md +3 -0
  108. package/skills/plitzi-authoring/reference/layouts.md +8 -23
  109. package/skills/plitzi-authoring/reference/performance.md +2 -2
  110. package/skills/plitzi-authoring/reference/plugins.md +51 -38
  111. package/skills/plitzi-authoring/reference/review-checklist.md +2 -0
  112. package/skills/plitzi-authoring/reference/structure.md +32 -1
  113. package/skills/plitzi-authoring/reference/templates.md +5 -2
  114. package/skills/plitzi-authoring/reference/testing.md +9 -2
  115. package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
  116. 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,2 @@
1
+ import { SSRContext, Stage } from '../http/types';
2
+ export declare const spaceDocumentStage: Stage<SSRContext>;
@@ -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 };
@@ -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';
@@ -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
- * The compiled stylesheet is left out when the page's own runtime stylesheet carries it in a form the browser reads
8
- * back exactly (`styleCacheTravelsInDocument`), and `styleCacheInDocument` tells the bootstrap to. It is the largest
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>) => string;
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 serializedSpaces = /* @__PURE__ */ new WeakMap();
5
+ var documents = /* @__PURE__ */ new WeakMap();
5
6
  /**
6
- * The space as the page embeds it, serialized once per space object rather than once per render.
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 — tens of kilobytes of schema and style — and the adapters hand every request the same
9
- * object until the space changes, so turning it into escaped JSON on each render was a tenth of a render's CPU spent
10
- * producing the same string. Keyed weakly: a space the adapters let go of takes its string with it.
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 serializeSpace = (offlineData, withoutStyleCache) => {
13
- let serialized = serializedSpaces.get(offlineData);
14
- if (!serialized) {
15
- serialized = {};
16
- serializedSpaces.set(offlineData, serialized);
17
- }
18
- if (withoutStyleCache) {
19
- serialized.withoutStyleCache ??= escapeJson(JSON.stringify({
20
- ...offlineData,
21
- style: {
22
- ...offlineData.style,
23
- cache: ""
24
- }
25
- }));
26
- return serialized.withoutStyleCache;
27
- }
28
- serialized.whole ??= escapeJson(JSON.stringify(offlineData));
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
- * The compiled stylesheet is left out when the page's own runtime stylesheet carries it in a form the browser reads
37
- * back exactly (`styleCacheTravelsInDocument`), and `styleCacheInDocument` tells the bootstrap to. It is the largest
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":${serializeSpace(offlineData, inDocument)}${separator}${serializedRest.slice(1)}`;
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 registry = createTaskRegistry(config.functions?.native, { db: (config.dbDrivers?.length ?? 0) > 0 });
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(registry.list().map((task) => task.namespace));
53
- const registryFor = async (spaceId, at) => spaceRegistries.registryFor(await config.lookups.getFunctions?.(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;