@plitzi/sdk-server 0.37.9 → 0.37.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +589 -0
- package/README.md +17 -0
- package/dist/adapters/cloudAdapters.d.ts +5 -3
- package/dist/adapters/cloudAdapters.js +120 -34
- package/dist/adapters/space/types.d.ts +3 -0
- package/dist/core/configSeam.d.ts +14 -0
- package/dist/core/configSeam.js +114 -0
- package/dist/core/configSeam.test.d.ts +1 -0
- package/dist/core/createServer.js +11 -5
- package/dist/core/createServer.publicData.test.d.ts +1 -0
- package/dist/core/freePort.d.ts +10 -0
- package/dist/core/freePort.js +36 -0
- package/dist/core/freePort.test.d.ts +1 -0
- package/dist/core/http/stages/images.d.ts +6 -0
- package/dist/core/http/stages/images.js +47 -0
- package/dist/core/http/stages/images.test.d.ts +1 -0
- package/dist/core/server/fleet/channel.js +1 -1
- package/dist/core/server/fleet/relay.js +1 -1
- package/dist/core/services/action.js +5 -4
- package/dist/core/services/registry.js +2 -0
- package/dist/helpers/cache/keys.d.ts +23 -10
- package/dist/helpers/cache/keys.js +16 -2
- package/dist/helpers/flagOverrides.d.ts +27 -0
- package/dist/helpers/flagOverrides.js +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/moduleFor.js +5 -3
- package/dist/modules/actions/runtime/flags.d.ts +16 -0
- package/dist/modules/actions/runtime/flags.js +49 -0
- package/dist/modules/actions/runtime/flags.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvList.js +1 -1
- package/dist/modules/actions/runtime/runAction.js +2 -0
- package/dist/modules/actions/transport/callHandler.js +8 -0
- package/dist/modules/actions/types.d.ts +14 -1
- package/dist/modules/connectors/engine.js +1 -1
- package/dist/modules/functions/capabilities.js +1 -1
- package/dist/modules/functions/load.js +2 -2
- package/dist/modules/functions/manifest.js +1 -1
- package/dist/modules/functions/runner/messages.js +1 -1
- package/dist/modules/images/allowlist.d.ts +5 -0
- package/dist/modules/images/allowlist.js +18 -0
- package/dist/modules/images/imageProxy.d.ts +36 -0
- package/dist/modules/images/imageProxy.js +273 -0
- package/dist/modules/images/imageProxy.test.d.ts +1 -0
- package/dist/modules/images/transform.d.ts +10 -0
- package/dist/modules/images/transform.js +28 -0
- package/dist/modules/images/transform.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +12 -7
- package/dist/modules/rsc/connectorRscData.js +12 -8
- package/dist/modules/rsc/handler.js +18 -4
- package/dist/modules/rsc/publicFileResolver.d.ts +13 -0
- package/dist/modules/rsc/publicFileResolver.js +39 -0
- package/dist/modules/rsc/publicFileResolver.test.d.ts +1 -0
- package/dist/modules/rsc/resolveRscData.d.ts +4 -2
- package/dist/modules/rsc/resolveRscData.js +10 -2
- package/dist/modules/runtime/bundle.js +2 -2
- package/dist/modules/ssr/Component.d.ts +5 -1
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/prepareRender.js +36 -8
- package/package.json +9 -4
- package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
- package/skills/plitzi-authoring/SKILL.md +71 -71
- package/skills/plitzi-authoring/recipes/carousel.ts +114 -0
- package/skills/plitzi-authoring/recipes/detail-page.ts +70 -0
- package/skills/plitzi-authoring/recipes/embed-and-svg.ts +48 -0
- package/skills/plitzi-authoring/recipes/every-few-seconds.ts +119 -0
- package/skills/plitzi-authoring/recipes/feature-flag.ts +24 -0
- package/skills/plitzi-authoring/recipes/filter-a-list.ts +63 -0
- package/skills/plitzi-authoring/recipes/forms-and-modals.ts +64 -0
- package/skills/plitzi-authoring/recipes/from-tailwind.ts +52 -0
- package/skills/plitzi-authoring/recipes/link-to-a-section.ts +35 -0
- package/skills/plitzi-authoring/recipes/link-with-data.ts +40 -0
- package/skills/plitzi-authoring/recipes/marquee.ts +57 -0
- package/skills/plitzi-authoring/recipes/plugin.ts +53 -0
- package/skills/plitzi-authoring/recipes/scroll-a-row.ts +79 -0
- package/skills/plitzi-authoring/recipes/show-data.ts +73 -0
- package/skills/plitzi-authoring/recipes/style-a-page.ts +45 -0
- package/skills/plitzi-authoring/recipes/typed-data.ts +64 -0
- package/skills/plitzi-authoring/recipes/usable-without-sight.ts +39 -0
- package/skills/plitzi-authoring/reference/accessibility.md +2 -2
- package/skills/plitzi-authoring/reference/authoring-errors.md +195 -53
- package/skills/plitzi-authoring/reference/colours-and-motion.md +63 -0
- package/skills/plitzi-authoring/reference/components.md +9 -1
- package/skills/plitzi-authoring/reference/data-and-visibility.md +29 -29
- package/skills/plitzi-authoring/reference/efficiency.md +53 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +39 -24
- package/skills/plitzi-authoring/reference/feature-flags.md +78 -0
- package/skills/plitzi-authoring/reference/flows.md +25 -36
- package/skills/plitzi-authoring/reference/kept-state.md +28 -0
- package/skills/plitzi-authoring/reference/layouts.md +20 -3
- package/skills/plitzi-authoring/reference/lists.md +45 -16
- package/skills/plitzi-authoring/reference/plugins.md +15 -0
- package/skills/plitzi-authoring/reference/realtime.md +35 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +8 -3
- package/skills/plitzi-authoring/reference/{templates-and-export.md → snippets-and-export.md} +9 -8
- package/skills/plitzi-authoring/reference/structure.md +29 -20
- package/skills/plitzi-authoring/reference/tailwind.md +19 -0
- package/skills/plitzi-authoring/reference/templates.md +6 -5
- package/skills/plitzi-authoring/reference/testing.md +11 -0
- package/skills/plitzi-authoring/reference/typed-sources.md +20 -0
- package/skills/plitzi-authoring/reference/validation.md +6 -2
package/README.md
CHANGED
|
@@ -169,6 +169,7 @@ runtime, and its files into `public/` — a draft, or any published snapshot (`-
|
|
|
169
169
|
| `devMode` | `boolean` | `false` | Enables development mode: appends `?dev` to esm.sh CDN URLs for React, and activates per-request timing metrics (see [Dev metrics](#dev-metrics)). Off, the process must run with `NODE_ENV=production` or React renders with its development build; the server says so once, at `error`. |
|
|
170
170
|
| `assetVersion` | `string` | — | Cache-buster appended as `?v=<assetVersion>` to all default SDK asset URLs. Compute from file mtime or package version at startup. |
|
|
171
171
|
| `cacheTtlMs` | `number` | `300000` | TTL in milliseconds for the SSR render cache. Set to `0` to disable. |
|
|
172
|
+
| `flags` | `Record<string, boolean> \| ({ spaceId, environment }) => Record<string, boolean>` | — | This deployment's say over the spaces' feature flags: above what a space declares, below the SDK's `flags` prop and a tester's dev tools, and only for flags the space declares. A function answers per space. Server actions and RSC resolve with it too. See `docs/en/feature-flags.md` in the workspace. |
|
|
172
173
|
| `loginPath` | `string \| false` | `'/auth/login'` | Path for the built-in login endpoint. Set to `false` to disable it entirely. |
|
|
173
174
|
| `logoutPath` | `string \| false` | `'/auth/logout'` | Path for the built-in logout endpoint. Set to `false` to disable it entirely. |
|
|
174
175
|
| `signIn` | `SSRSignInConfig` | — | Sign visitors in by redirect through an OAuth 2.1 authorization server: `GET /auth/sign-in?return=/x` registers this host as a client, sends the browser out with PKCE (state in a `__Host-` cookie), and `/auth/sign-in/callback` redeems the code server to server and hands the token to `exchangeCredential`, which makes the session here. Needs the `exchangeCredential` adapter. |
|
|
@@ -180,6 +181,7 @@ runtime, and its files into `public/` — a draft, or any published snapshot (`-
|
|
|
180
181
|
| `allowPrivatePluginHosts` | `boolean` | `false` | Read a schema plugin from a private address (`localhost`, this server's own network). A plugin's address is typed by whoever edits a space, so leave it off anywhere but a development machine whose bucket is local. |
|
|
181
182
|
| `publicDir` | `string` | — | Absolute path to a directory served at the root URL level (e.g. `robots.txt`, `favicon.png`). Files are checked before `static` prefix routes. |
|
|
182
183
|
| `static` | `Record<string, string>` | — | URL prefix → filesystem path mappings for static file serving. |
|
|
184
|
+
| `images` | `{ domains, cacheDir? }` | — | Pictures from other sites resized at `/_plitzi/img`, for an `image` to offer as a `srcset` (see [Images](#images)). |
|
|
183
185
|
| `ssrOnly` | `boolean` | `false` | Omit client-side JS from the rendered page. Useful for verifying SSR HTML without hydration. |
|
|
184
186
|
| `streaming` | `boolean` | `false` | Stream HTML to the browser incrementally to reduce TTFB. See [Streaming](#streaming). |
|
|
185
187
|
| `middlewares` | `SSRMiddleware[]` | — | Array of custom middleware functions executed before the SSR renderer on every request (see [Custom middlewares](#custom-middlewares)). |
|
|
@@ -386,6 +388,21 @@ createServer({
|
|
|
386
388
|
|
|
387
389
|
Static responses include `ETag`, `Last-Modified`, and `Cache-Control` headers. Subsequent requests with `If-None-Match` receive `304 Not Modified` when the file has not changed. JS, CSS, and font files are served with `Cache-Control: immutable`; all other assets use a 1-hour max-age.
|
|
388
390
|
|
|
391
|
+
### Images
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
createServer({ images: { domains: ['images.example.com', '*.cdn.example.com'] }, adapters: { ... } });
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
An `image` whose `src` is another site's then offers the browser a `srcset` of widths this server makes (320 to 1920
|
|
398
|
+
px) at `/_plitzi/img?url=…&w=…`, in AVIF or WebP when the browser takes them. Everything is kept on disk (`cacheDir`,
|
|
399
|
+
default `.plitzi/images`): each original is downloaded once, and each size is made once from it. A week on, the kept
|
|
400
|
+
files keep answering while the original is asked for with its `ETag` / `Last-Modified` — unchanged, nothing is resized
|
|
401
|
+
again; changed, its sizes are made anew. A picture that could not be fetched is not asked for again for five minutes.
|
|
402
|
+
Only the listed hosts are fetched, every redirect is held to the same
|
|
403
|
+
list and to the rule every outbound request follows (no private network), and SVG is refused. Resizing needs `sharp`
|
|
404
|
+
(an optional peer): without it pictures are passed through and kept, and the server says so once.
|
|
405
|
+
|
|
389
406
|
### Public directory
|
|
390
407
|
|
|
391
408
|
Use `publicDir` to serve your own root-level files (e.g. `robots.txt`, `sitemap.xml`, `favicon.png`) without prefixes:
|
|
@@ -35,7 +35,8 @@ export type CloudAdaptersConfig = {
|
|
|
35
35
|
* revisions, it has whatever the builder last saved.
|
|
36
36
|
*
|
|
37
37
|
* Pinned to a number, this deployment serves that exact version until somebody changes the config: a revision
|
|
38
|
-
* cannot change, so it is fetched once and kept for the life of the process.
|
|
38
|
+
* cannot change, so it is fetched once and kept for the life of the process. Its feature flags are not part of it
|
|
39
|
+
* and still follow the environment — a flag turned in production reaches a pinned deployment too. That is what a deployment wants
|
|
39
40
|
* when it rolls forward on its own schedule, or when it has to be able to say precisely what it is serving.
|
|
40
41
|
*
|
|
41
42
|
* Left out, it serves the **latest** revision of that environment and notices when a new one is published: a
|
|
@@ -52,8 +53,9 @@ export type CloudAdaptersConfig = {
|
|
|
52
53
|
* - A **pinned revision** never expires. Expiring an immutable document on a timer would be paying for a
|
|
53
54
|
* question whose answer is already known.
|
|
54
55
|
*
|
|
55
|
-
* So this paces exactly one thing: the "which revision is current"
|
|
56
|
-
*
|
|
56
|
+
* So this paces exactly one thing: the probe — "which revision is current" in latest mode, and in both modes "what
|
|
57
|
+
* are the flags at", whose answer is a hash and whose flags are fetched only when it moved. It never sits in front
|
|
58
|
+
* of a visitor: the copy already held is served while the probe runs behind it.
|
|
57
59
|
*/
|
|
58
60
|
cacheSeconds?: number;
|
|
59
61
|
/** Where fetched spaces are kept. Omitted, they are kept in this process only — see {@link CloudSpaceCache}. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { serverLog } from "../helpers/serverLog.js";
|
|
2
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
3
|
+
import { schemaFromWire } from "@plitzi/sdk-shared/schema/wire";
|
|
2
4
|
//#region src/adapters/cloudAdapters.ts
|
|
3
5
|
/**
|
|
4
6
|
* A space fetched from Plitzi, for a server that is not Plitzi's.
|
|
@@ -28,9 +30,10 @@ var SPACE_QUERY = `query InitQuery($environment: String!, $revision: Int) {
|
|
|
28
30
|
schema {
|
|
29
31
|
settings
|
|
30
32
|
rsc
|
|
31
|
-
flat { id definition { label type initialState styleSelectors bindings interactions parentId rootId items runtime loadStrategy } attributes }
|
|
33
|
+
flat { id definition { label type initialState styleSelectors bindings interactions parentId rootId items runtime loadStrategy flag { name is } anchor } attributes }
|
|
32
34
|
pages
|
|
33
35
|
components
|
|
36
|
+
flags
|
|
34
37
|
pageFolders { id name slug parentId }
|
|
35
38
|
variables { name type value subValues { when value } }
|
|
36
39
|
}
|
|
@@ -48,28 +51,19 @@ var SPACE_QUERY = `query InitQuery($environment: String!, $revision: Int) {
|
|
|
48
51
|
var LATEST_REVISION_QUERY = `query SpaceLatestRevisionQuery($environment: String!) {
|
|
49
52
|
SpaceLatestRevision(environment: $environment) {
|
|
50
53
|
snapshot { revision }
|
|
54
|
+
flagsHash
|
|
51
55
|
}
|
|
52
56
|
}`;
|
|
53
57
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Without it nothing throws — an array is a perfectly good object — it just answers `undefined` to every lookup, so
|
|
59
|
-
* the page router matches no page and every URL is a 404 on a space that fetched and parsed correctly.
|
|
58
|
+
* The environment's feature flags. Asked only when the probe's `flagsHash` moved: flags change apart from revisions —
|
|
59
|
+
* one set per environment, shared by every revision it serves — so they are fetched and kept on their own, and a flag
|
|
60
|
+
* turned in production costs this deployment a few hundred bytes rather than the space again.
|
|
60
61
|
*/
|
|
61
|
-
var
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}, {});
|
|
67
|
-
};
|
|
68
|
-
/** The runtime shape of a schema: the wire's `flat` list, keyed. */
|
|
69
|
-
var toSchema = (schema) => ({
|
|
70
|
-
...schema,
|
|
71
|
-
flat: byElementId(schema.flat)
|
|
72
|
-
});
|
|
62
|
+
var FLAGS_QUERY = `query SpaceFlagsQuery($environment: String!) {
|
|
63
|
+
SpaceFlags(environment: $environment) { flags hash }
|
|
64
|
+
}`;
|
|
65
|
+
/** What the shared cache holds under a flags key — written by this module, but read back from outside the process. */
|
|
66
|
+
var isHeldFlags = (value) => isRecord(value) && typeof value.hash === "string" && isRecord(value.flags);
|
|
73
67
|
/**
|
|
74
68
|
* The `scope` claim, read without verifying anything.
|
|
75
69
|
*
|
|
@@ -147,15 +141,74 @@ var createCloudAdapters = (config) => {
|
|
|
147
141
|
}, `space ${env}@${rev ?? "latest"}`))?.Space;
|
|
148
142
|
if (!space?.schema) return;
|
|
149
143
|
return {
|
|
150
|
-
schema:
|
|
144
|
+
schema: schemaFromWire(space.schema),
|
|
151
145
|
style: space.style,
|
|
152
146
|
plugins: space.plugins
|
|
153
147
|
};
|
|
154
148
|
};
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
/** Which revision is current, and what the flags are at — one small question for both halves of a served space. */
|
|
150
|
+
const probe = async (env) => {
|
|
151
|
+
const answer = (await post(LATEST_REVISION_QUERY, { environment: env }, `latest revision of ${env}`))?.SpaceLatestRevision;
|
|
152
|
+
return {
|
|
153
|
+
revision: answer?.snapshot?.revision,
|
|
154
|
+
flagsHash: answer?.flagsHash ?? void 0
|
|
155
|
+
};
|
|
157
156
|
};
|
|
158
157
|
const cacheKey = (env, rev) => `plitzi:space:${env}:${rev}`;
|
|
158
|
+
const flagsCacheKey = (env) => `plitzi:flags:${env}`;
|
|
159
|
+
/** Each environment's flags as last fetched, and when the probe last confirmed them. */
|
|
160
|
+
const heldFlags = /* @__PURE__ */ new Map();
|
|
161
|
+
/** The flags this process last held, or — on a cold start — the shared cache's, so a restart with Plitzi down keeps
|
|
162
|
+
* serving the flags it knew rather than the ones frozen into a cached revision. */
|
|
163
|
+
const restoreFlags = async (env) => {
|
|
164
|
+
if (heldFlags.has(env)) return;
|
|
165
|
+
try {
|
|
166
|
+
const stored = await cache?.get(flagsCacheKey(env));
|
|
167
|
+
const held = stored ? JSON.parse(stored) : void 0;
|
|
168
|
+
if (isHeldFlags(held)) heldFlags.set(env, {
|
|
169
|
+
...held,
|
|
170
|
+
checkedAt: 0
|
|
171
|
+
});
|
|
172
|
+
} catch {}
|
|
173
|
+
};
|
|
174
|
+
/** Brings the flags up to the hash the probe answered, fetching them only when it moved. A failed probe or fetch keeps
|
|
175
|
+
* the flags already held. */
|
|
176
|
+
const syncFlags = async (env, hash) => {
|
|
177
|
+
if (hash === void 0) return;
|
|
178
|
+
const current = heldFlags.get(env);
|
|
179
|
+
if (current?.hash === hash) {
|
|
180
|
+
current.checkedAt = Date.now();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const fetched = (await post(FLAGS_QUERY, { environment: env }, `flags of ${env}`))?.SpaceFlags;
|
|
184
|
+
if (!fetched) return;
|
|
185
|
+
heldFlags.set(env, {
|
|
186
|
+
...fetched,
|
|
187
|
+
checkedAt: Date.now()
|
|
188
|
+
});
|
|
189
|
+
await cache?.set(flagsCacheKey(env), JSON.stringify(fetched)).catch(() => void 0);
|
|
190
|
+
};
|
|
191
|
+
/** A served space with the flags held for its environment — the same object for the same pair, so the page server
|
|
192
|
+
* serialises it once rather than per request. */
|
|
193
|
+
const combined = /* @__PURE__ */ new WeakMap();
|
|
194
|
+
const withFlags = (env, data) => {
|
|
195
|
+
const flags = heldFlags.get(env);
|
|
196
|
+
if (!flags) return data;
|
|
197
|
+
const known = combined.get(data);
|
|
198
|
+
if (known?.hash === flags.hash) return known.data;
|
|
199
|
+
const joined = {
|
|
200
|
+
...data,
|
|
201
|
+
schema: {
|
|
202
|
+
...data.schema,
|
|
203
|
+
flags: flags.flags
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
combined.set(data, {
|
|
207
|
+
hash: flags.hash,
|
|
208
|
+
data: joined
|
|
209
|
+
});
|
|
210
|
+
return joined;
|
|
211
|
+
};
|
|
159
212
|
const held = /* @__PURE__ */ new Map();
|
|
160
213
|
/** One request per key at a time: a burst of renders on a cold cache joins the first, it does not multiply it. */
|
|
161
214
|
const inFlight = /* @__PURE__ */ new Map();
|
|
@@ -203,7 +256,9 @@ var createCloudAdapters = (config) => {
|
|
|
203
256
|
* A probe that fails leaves `checkedAt` alone so the next request tries again, and leaves the held copy serving.
|
|
204
257
|
*/
|
|
205
258
|
const refreshLatest = async (env) => {
|
|
206
|
-
|
|
259
|
+
await restoreFlags(env);
|
|
260
|
+
const { revision: current, flagsHash } = await probe(env);
|
|
261
|
+
await syncFlags(env, flagsHash);
|
|
207
262
|
const previous = held.get(env);
|
|
208
263
|
if (current === void 0) return previous?.data;
|
|
209
264
|
if (previous && previous.revision === current) {
|
|
@@ -213,10 +268,27 @@ var createCloudAdapters = (config) => {
|
|
|
213
268
|
return await loadRevision(env, current) ?? previous?.data;
|
|
214
269
|
};
|
|
215
270
|
/**
|
|
271
|
+
* A pinned revision's flags: waited for once — the first render must not go out with the flags frozen into the copy
|
|
272
|
+
* it fetched — and then checked behind the answer, on the window.
|
|
273
|
+
*/
|
|
274
|
+
const refreshPinnedFlags = async (env) => {
|
|
275
|
+
await restoreFlags(env);
|
|
276
|
+
const flags = heldFlags.get(env);
|
|
277
|
+
if (!flags) {
|
|
278
|
+
await syncFlags(env, (await probe(env)).flagsHash);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (Date.now() - flags.checkedAt >= cacheSeconds * 1e3) {
|
|
282
|
+
flags.checkedAt = Date.now();
|
|
283
|
+
probe(env).then(({ flagsHash }) => syncFlags(env, flagsHash)).catch(() => void 0);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
216
287
|
* The space, and how hard this deployment leans on Plitzi to get it.
|
|
217
288
|
*
|
|
218
289
|
* - **`main`**: read live, every time. It is being edited; a cached answer is a wrong one.
|
|
219
|
-
* - **A pinned revision**: from cache forever after the first read. It cannot change
|
|
290
|
+
* - **A pinned revision**: from cache forever after the first read. It cannot change — its flags can, so they are
|
|
291
|
+
* probed on the same window as the latest mode's revision, and fetched only when they moved.
|
|
220
292
|
* - **Latest**: the held copy is served immediately and a revision probe runs behind it once the window is up.
|
|
221
293
|
* After the first render no page ever waits on Plitzi, and a failed probe or fetch changes nothing — the last
|
|
222
294
|
* good copy keeps serving, because a self-hosted site going blank over somebody else's bad minute is not a
|
|
@@ -226,16 +298,21 @@ var createCloudAdapters = (config) => {
|
|
|
226
298
|
if (isLive(env)) return once(`live:${env}`, () => fetchSpace(env));
|
|
227
299
|
const pinned = rev ?? revision;
|
|
228
300
|
if (pinned !== void 0 && isPinned(env, pinned)) {
|
|
301
|
+
await refreshPinnedFlags(env);
|
|
229
302
|
const already = held.get(env);
|
|
230
|
-
|
|
303
|
+
const data = already?.revision === pinned ? already.data : await loadRevision(env, pinned);
|
|
304
|
+
return data && withFlags(env, data);
|
|
231
305
|
}
|
|
232
306
|
const current = held.get(env);
|
|
233
|
-
if (!current)
|
|
307
|
+
if (!current) {
|
|
308
|
+
const data = await refreshLatest(env);
|
|
309
|
+
return data && withFlags(env, data);
|
|
310
|
+
}
|
|
234
311
|
if (Date.now() - current.checkedAt >= cacheSeconds * 1e3) {
|
|
235
312
|
current.checkedAt = Date.now();
|
|
236
313
|
refreshLatest(env).catch(() => void 0);
|
|
237
314
|
}
|
|
238
|
-
return current.data;
|
|
315
|
+
return withFlags(env, current.data);
|
|
239
316
|
};
|
|
240
317
|
/**
|
|
241
318
|
* Every request is this one space.
|
|
@@ -244,7 +321,7 @@ var createCloudAdapters = (config) => {
|
|
|
244
321
|
* builds two servers or supplies its own resolver — which is more honest than a lookup table that silently
|
|
245
322
|
* serves the wrong space when a domain is added and this is not.
|
|
246
323
|
*
|
|
247
|
-
* The revision it reports is the one actually being served, which in latest mode is whatever the last probe
|
|
324
|
+
* The revision it reports is the one actually being served, with what its flags are at, which in latest mode is whatever the last probe
|
|
248
325
|
* found: a deployment reading `at: { environment, revision }` must be told the version its page was built from,
|
|
249
326
|
* not the zero that means "whatever is live".
|
|
250
327
|
*/
|
|
@@ -255,18 +332,27 @@ var createCloudAdapters = (config) => {
|
|
|
255
332
|
environment,
|
|
256
333
|
revision: 0
|
|
257
334
|
};
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
environment,
|
|
262
|
-
revision
|
|
335
|
+
const flagsVersion = () => {
|
|
336
|
+
const flags = heldFlags.get(environment);
|
|
337
|
+
return flags ? { flagsVersion: flags.hash } : {};
|
|
263
338
|
};
|
|
339
|
+
if (isPinned(environment, revision)) {
|
|
340
|
+
await refreshPinnedFlags(environment);
|
|
341
|
+
return {
|
|
342
|
+
...deployment,
|
|
343
|
+
spaceId,
|
|
344
|
+
environment,
|
|
345
|
+
revision,
|
|
346
|
+
...flagsVersion()
|
|
347
|
+
};
|
|
348
|
+
}
|
|
264
349
|
if (!held.has(environment)) await refreshLatest(environment);
|
|
265
350
|
return {
|
|
266
351
|
...deployment,
|
|
267
352
|
spaceId,
|
|
268
353
|
environment,
|
|
269
|
-
revision: held.get(environment)?.revision ?? 0
|
|
354
|
+
revision: held.get(environment)?.revision ?? 0,
|
|
355
|
+
...flagsVersion()
|
|
270
356
|
};
|
|
271
357
|
};
|
|
272
358
|
return {
|
|
@@ -20,6 +20,9 @@ export interface SpaceResolution {
|
|
|
20
20
|
templateProps?: SSRTemplateProps;
|
|
21
21
|
pluginNames?: string[];
|
|
22
22
|
pluginSources?: Record<string, PluginSource>;
|
|
23
|
+
/** What the space's flags are at, when they change apart from its revisions — see `SSRSpaceDeployment.flagsVersion`.
|
|
24
|
+
* Usually set by `decorate`, which runs on every request: a cached resolution would hold a stale one. */
|
|
25
|
+
flagsVersion?: string;
|
|
23
26
|
}
|
|
24
27
|
/** A resolver that has decided this request gets nothing, and that no later resolver should be asked. */
|
|
25
28
|
export interface SpaceRefusal {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ActionDbDriver, ActionLookups, ActionsConfig } from '../modules/actions/types';
|
|
2
|
+
import { ConnectorLookups } from '../modules/connectors/resolver';
|
|
3
|
+
import { ActionLookupsConfig, ConnectorLookupsConfig } from '@plitzi/sdk-shared';
|
|
4
|
+
/** The connector lookups a deployment configured, each manifest checked as it is read. One per config object. */
|
|
5
|
+
export declare const connectorLookupsOf: (config: ConnectorLookupsConfig) => ConnectorLookups;
|
|
6
|
+
/**
|
|
7
|
+
* The action lookups a deployment configured, each document checked as it is read. Asked for one, a document that is
|
|
8
|
+
* not one is refused; listed, it is left out and said, so one broken action does not take its space's schedule down.
|
|
9
|
+
*/
|
|
10
|
+
export declare const actionLookupsOf: (config: ActionLookupsConfig) => ActionLookups;
|
|
11
|
+
/** The deployment's database drivers, each held to what a `db` task calls — or why one is not, as the server starts. */
|
|
12
|
+
export declare const dbDriversOf: (drivers: readonly unknown[] | undefined) => ActionDbDriver[] | undefined;
|
|
13
|
+
/** The deployment's functions config, held to what runs a space's code — or why it is not, as the server starts. */
|
|
14
|
+
export declare const functionsConfigOf: (config: unknown) => ActionsConfig["functions"];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { serverLog } from "../helpers/serverLog.js";
|
|
2
|
+
import { isActionDocument, validateActionDocument } from "@plitzi/sdk-shared/actions";
|
|
3
|
+
import { isConnectorManifestDraft, validateConnectorManifest } from "@plitzi/sdk-shared/connectors";
|
|
4
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
5
|
+
//#region src/core/configSeam.ts
|
|
6
|
+
/**
|
|
7
|
+
* A guard that checks each object once: a deployment that caches what it reads hands back the same object on every
|
|
8
|
+
* call, and a render must not pay for validating its action again.
|
|
9
|
+
*/
|
|
10
|
+
var checkedOnce = (guard) => {
|
|
11
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
return (value) => {
|
|
13
|
+
if (!isRecord(value)) return false;
|
|
14
|
+
const known = seen.get(value);
|
|
15
|
+
if (known !== void 0) return known;
|
|
16
|
+
const valid = guard(value);
|
|
17
|
+
seen.set(value, valid);
|
|
18
|
+
return valid;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
var isManifest = checkedOnce(isConnectorManifestDraft);
|
|
22
|
+
var isDocument = checkedOnce(isActionDocument);
|
|
23
|
+
/** What `getFunctions` answers: built by `@plitzi/sdk-server/functions` from a space's saved bundle, a live object. */
|
|
24
|
+
var isSpaceFunctions = (value) => isRecord(value) && isRecord(value.bundle) && typeof value.bundle.id === "string" && typeof value.bundle.load === "function" && isRecord(value.manifest);
|
|
25
|
+
/** Why a document is not one this server runs: the first problem its validator found, by where it is. */
|
|
26
|
+
var firstProblem = (errors) => {
|
|
27
|
+
const first = errors.at(0);
|
|
28
|
+
if (!first) return "it is not a document";
|
|
29
|
+
return first.path ? `${first.path}: ${first.message}` : first.message;
|
|
30
|
+
};
|
|
31
|
+
var manifestFrom = (found, spaceId, connectorId) => {
|
|
32
|
+
if (found === void 0 || found === null) return;
|
|
33
|
+
if (!isManifest(found)) throw new Error(`Connector "${connectorId}" of space ${String(spaceId)} is not a manifest this server can run — ${firstProblem(validateConnectorManifest(found).errors)}`);
|
|
34
|
+
return {
|
|
35
|
+
...found,
|
|
36
|
+
id: connectorId
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/** An action as a lookup found it: the entry, or why it is not one. */
|
|
40
|
+
var readAction = (found) => {
|
|
41
|
+
const document = isRecord(found) ? found.document : void 0;
|
|
42
|
+
if (!isRecord(found) || typeof found.id !== "string") return { problem: "it has no id" };
|
|
43
|
+
return isDocument(document) ? { entry: {
|
|
44
|
+
id: found.id,
|
|
45
|
+
document
|
|
46
|
+
} } : { problem: firstProblem(validateActionDocument(document).errors) };
|
|
47
|
+
};
|
|
48
|
+
var connectorSeams = /* @__PURE__ */ new WeakMap();
|
|
49
|
+
/** The connector lookups a deployment configured, each manifest checked as it is read. One per config object. */
|
|
50
|
+
var connectorLookupsOf = (config) => {
|
|
51
|
+
const existing = connectorSeams.get(config);
|
|
52
|
+
if (existing) return existing;
|
|
53
|
+
const lookups = {
|
|
54
|
+
getConnector: async (spaceId, connectorId, at) => manifestFrom(await config.getConnector(spaceId, connectorId, at), spaceId, connectorId),
|
|
55
|
+
...config.getCredential ? { getCredential: config.getCredential } : {},
|
|
56
|
+
...config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}
|
|
57
|
+
};
|
|
58
|
+
connectorSeams.set(config, lookups);
|
|
59
|
+
return lookups;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The action lookups a deployment configured, each document checked as it is read. Asked for one, a document that is
|
|
63
|
+
* not one is refused; listed, it is left out and said, so one broken action does not take its space's schedule down.
|
|
64
|
+
*/
|
|
65
|
+
var actionLookupsOf = (config) => {
|
|
66
|
+
const { getConnector, getFunctions, listActions } = config;
|
|
67
|
+
return {
|
|
68
|
+
getAction: async (spaceId, actionId, at) => {
|
|
69
|
+
const found = await config.getAction(spaceId, actionId, at);
|
|
70
|
+
if (found === void 0 || found === null) return;
|
|
71
|
+
const read = readAction(found);
|
|
72
|
+
if ("problem" in read) throw new Error(`Action "${actionId}" of space ${String(spaceId)} is not a document this server can run — ${read.problem}`);
|
|
73
|
+
return read.entry;
|
|
74
|
+
},
|
|
75
|
+
...listActions ? { listActions: async (spaceId, at) => (await listActions(spaceId, at)).flatMap((found) => {
|
|
76
|
+
const read = readAction(found);
|
|
77
|
+
if ("problem" in read) {
|
|
78
|
+
serverLog.warn("actions", `An action of space ${String(spaceId)} is left out — ${read.problem}`);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return [read.entry];
|
|
82
|
+
}) } : {},
|
|
83
|
+
...config.listScheduledSpaces ? { listScheduledSpaces: config.listScheduledSpaces } : {},
|
|
84
|
+
...config.getCredential ? { getCredential: config.getCredential } : {},
|
|
85
|
+
...getConnector ? { getConnector: async (spaceId, connectorId, at) => manifestFrom(await getConnector(spaceId, connectorId, at), spaceId, connectorId) } : {},
|
|
86
|
+
...getFunctions ? { getFunctions: async (spaceId, at) => {
|
|
87
|
+
const found = await getFunctions(spaceId, at);
|
|
88
|
+
if (found === void 0 || found === null) return;
|
|
89
|
+
if (!isSpaceFunctions(found)) throw new Error(`The functions of space ${String(spaceId)} are not what @plitzi/sdk-server/functions builds for one`);
|
|
90
|
+
return found;
|
|
91
|
+
} } : {},
|
|
92
|
+
...config.getFlags ? { getFlags: config.getFlags } : {}
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
var isFunction = (value) => typeof value === "function";
|
|
96
|
+
/** A driver as `createDbDrivers` builds one: the engine a credential names, and how to query it. */
|
|
97
|
+
var isDbDriver = (value) => isRecord(value) && typeof value.engine === "string" && isFunction(value.query);
|
|
98
|
+
/** The functions config as `@plitzi/sdk-server/functions` builds it: native definitions, a runner, ceilings. */
|
|
99
|
+
var isFunctionsConfig = (value) => isRecord(value) && (value.native === void 0 || Array.isArray(value.native) && value.native.every(isRecord)) && (value.runner === void 0 || isRecord(value.runner) && isFunction(value.runner.describe) && isFunction(value.runner.invoke)) && (value.limits === void 0 || isRecord(value.limits));
|
|
100
|
+
/** The deployment's database drivers, each held to what a `db` task calls — or why one is not, as the server starts. */
|
|
101
|
+
var dbDriversOf = (drivers) => {
|
|
102
|
+
if (drivers === void 0) return;
|
|
103
|
+
const wrong = drivers.findIndex((driver) => !isDbDriver(driver));
|
|
104
|
+
if (wrong >= 0) throw new Error(`action.dbDrivers[${String(wrong)}] is not a database driver: it needs an \`engine\` and a \`query\` function.`);
|
|
105
|
+
return drivers.filter(isDbDriver);
|
|
106
|
+
};
|
|
107
|
+
/** The deployment's functions config, held to what runs a space's code — or why it is not, as the server starts. */
|
|
108
|
+
var functionsConfigOf = (config) => {
|
|
109
|
+
if (config === void 0) return;
|
|
110
|
+
if (!isFunctionsConfig(config)) throw new Error("`functions` is not what @plitzi/sdk-server/functions configures: `native` definitions, a `runner` with `describe` and `invoke`, `limits`.");
|
|
111
|
+
return config;
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
export { actionLookupsOf, connectorLookupsOf, dbDriversOf, functionsConfigOf };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { actionLookupsOf, connectorLookupsOf } from "./configSeam.js";
|
|
1
2
|
import { actionsModuleFor } from "../modules/actions/moduleFor.js";
|
|
2
3
|
import { connectorRscData } from "../modules/rsc/connectorRscData.js";
|
|
3
4
|
import { createAuthApiStage } from "./http/stages/authApi.js";
|
|
@@ -12,8 +13,8 @@ import { createPageServer } from "./server/pageServer.js";
|
|
|
12
13
|
* A dedicated MCP server is `createServer` from `@plitzi/sdk-mcp` — it builds none of the render template,
|
|
13
14
|
* caches or plugin manager this one does. */
|
|
14
15
|
/**
|
|
15
|
-
* Fills in `getRscData` from whatever can produce a server element's data — `connectors`, `action.lookups`,
|
|
16
|
-
*
|
|
16
|
+
* Fills in `getRscData` from whatever can produce a server element's data — `connectors`, `action.lookups`, the
|
|
17
|
+
* files of `publicDir` — when the deployment did not write one.
|
|
17
18
|
*
|
|
18
19
|
* The lookups are already here and the assembly is entirely this package's — a resolver over `resolveRscData`.
|
|
19
20
|
* Leaving it out meant every deployment passed the same lookups twice: once as config, for the write endpoint,
|
|
@@ -24,17 +25,22 @@ import { createPageServer } from "./server/pageServer.js";
|
|
|
24
25
|
* with no configuration missing anywhere.
|
|
25
26
|
*/
|
|
26
27
|
var withConnectorRsc = (config) => {
|
|
27
|
-
if (config.adapters.getRscData || !config.connectors && !config.action?.lookups) return config;
|
|
28
|
+
if (config.adapters.getRscData || !config.connectors && !config.action?.lookups && !config.publicDir) return config;
|
|
28
29
|
const resolved = {
|
|
29
30
|
...config,
|
|
30
31
|
adapters: { ...config.adapters }
|
|
31
32
|
};
|
|
32
33
|
const module = actionsModuleFor(resolved);
|
|
33
34
|
const actions = module && config.action?.lookups ? {
|
|
34
|
-
lookups: config.action.lookups,
|
|
35
|
+
lookups: actionLookupsOf(config.action.lookups),
|
|
35
36
|
module
|
|
36
37
|
} : void 0;
|
|
37
|
-
resolved.adapters.getRscData = connectorRscData(
|
|
38
|
+
resolved.adapters.getRscData = connectorRscData({
|
|
39
|
+
connectors: config.connectors ? connectorLookupsOf(config.connectors) : void 0,
|
|
40
|
+
actions,
|
|
41
|
+
publicDir: config.publicDir,
|
|
42
|
+
elementTimeoutMs: config.rsc?.elementTimeoutMs
|
|
43
|
+
});
|
|
38
44
|
return resolved;
|
|
39
45
|
};
|
|
40
46
|
/** The renewal stage in front of whatever else gates itself, when there is anywhere to renew. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first port from `preferred` up that nothing uses — for a development server, which should start beside
|
|
3
|
+
* whatever else is running rather than refuse to.
|
|
4
|
+
*
|
|
5
|
+
* Free means both that nothing answers there and that this process may listen there: a server bound to every
|
|
6
|
+
* address leaves `127.0.0.1` bindable on some systems, and a page server started beside it would never be reached —
|
|
7
|
+
* the other one would answer, with a `200` that looks like success. A deployment that sets its port does not need
|
|
8
|
+
* this: a fixed port that is taken is an error worth stopping for.
|
|
9
|
+
*/
|
|
10
|
+
export declare const freePort: (preferred: number, host?: string, attempts?: number) => Promise<number>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { connect, createServer } from "node:net";
|
|
2
|
+
//#region src/core/freePort.ts
|
|
3
|
+
/** Whether something already answers at `port` — the one test that sees a listener on every address, not just `host`. */
|
|
4
|
+
var answers = (port, host) => new Promise((resolve) => {
|
|
5
|
+
const socket = connect({
|
|
6
|
+
port,
|
|
7
|
+
host
|
|
8
|
+
});
|
|
9
|
+
socket.once("connect", () => {
|
|
10
|
+
socket.destroy();
|
|
11
|
+
resolve(true);
|
|
12
|
+
});
|
|
13
|
+
socket.once("error", () => resolve(false));
|
|
14
|
+
});
|
|
15
|
+
/** Whether this process could listen at `port` on `host`. */
|
|
16
|
+
var bindable = (port, host) => new Promise((resolve) => {
|
|
17
|
+
const probe = createServer();
|
|
18
|
+
probe.once("error", () => resolve(false));
|
|
19
|
+
probe.once("listening", () => probe.close(() => resolve(true)));
|
|
20
|
+
probe.listen(port, host);
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* The first port from `preferred` up that nothing uses — for a development server, which should start beside
|
|
24
|
+
* whatever else is running rather than refuse to.
|
|
25
|
+
*
|
|
26
|
+
* Free means both that nothing answers there and that this process may listen there: a server bound to every
|
|
27
|
+
* address leaves `127.0.0.1` bindable on some systems, and a page server started beside it would never be reached —
|
|
28
|
+
* the other one would answer, with a `200` that looks like success. A deployment that sets its port does not need
|
|
29
|
+
* this: a fixed port that is taken is an error worth stopping for.
|
|
30
|
+
*/
|
|
31
|
+
var freePort = async (preferred, host = "127.0.0.1", attempts = 20) => {
|
|
32
|
+
for (let port = preferred; port < preferred + attempts; port += 1) if (!await answers(port, host) && await bindable(port, host)) return port;
|
|
33
|
+
throw new Error(`No free port between ${preferred} and ${preferred + attempts - 1} on ${host}. Set PORT to one.`);
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { freePort };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Stage } from '../types';
|
|
2
|
+
import { SSRServerConfig } from '@plitzi/sdk-shared';
|
|
3
|
+
/** Where pictures are resized, when this server resizes them: published to the SDK, which builds every `srcset` on it. */
|
|
4
|
+
export declare const imagesPathOf: (config: Pick<SSRServerConfig, "images">) => string | undefined;
|
|
5
|
+
/** `/_plitzi/img?url=…&w=…` — a remote picture at one of the widths the SDK asks for. See `SSRImagesConfig`. */
|
|
6
|
+
export declare const imagesStage: Stage;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
|
+
import { createImageProxy } from "../../../modules/images/imageProxy.js";
|
|
3
|
+
import { loadSharpTransform } from "../../../modules/images/transform.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { IMAGE_PATH } from "@plitzi/sdk-shared/helpers/images";
|
|
6
|
+
//#region src/core/http/stages/images.ts
|
|
7
|
+
/** Where pictures are resized, when this server resizes them: published to the SDK, which builds every `srcset` on it. */
|
|
8
|
+
var imagesPathOf = (config) => config.images && config.images.domains.length > 0 ? IMAGE_PATH : void 0;
|
|
9
|
+
/** One proxy per configuration, made on the first request: `sharp` is looked for once, and said once when missing. */
|
|
10
|
+
var proxies = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
var proxyFor = (images) => {
|
|
12
|
+
const known = proxies.get(images);
|
|
13
|
+
if (known) return known;
|
|
14
|
+
const made = loadSharpTransform().then((transform) => {
|
|
15
|
+
if (!transform) serverLog.warn("IMAGES", "sharp is not installed: pictures are passed through and kept, not resized (npm i sharp).");
|
|
16
|
+
return createImageProxy({
|
|
17
|
+
domains: images.domains,
|
|
18
|
+
cacheDir: path.resolve(images.cacheDir ?? ".plitzi/images"),
|
|
19
|
+
fetch: globalThis.fetch,
|
|
20
|
+
transform
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
proxies.set(images, made);
|
|
24
|
+
return made;
|
|
25
|
+
};
|
|
26
|
+
/** `/_plitzi/img?url=…&w=…` — a remote picture at one of the widths the SDK asks for. See `SSRImagesConfig`. */
|
|
27
|
+
var imagesStage = async (ctx) => {
|
|
28
|
+
const { images } = ctx.config;
|
|
29
|
+
if (!images || imagesPathOf(ctx.config) === void 0 || ctx.req.path !== IMAGE_PATH) return false;
|
|
30
|
+
if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") {
|
|
31
|
+
ctx.res.setStatus(405);
|
|
32
|
+
ctx.res.setHeader("Allow", "GET, HEAD");
|
|
33
|
+
ctx.res.send("Method not allowed");
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const answer = await (await proxyFor(images))({
|
|
37
|
+
url: ctx.req.query.url,
|
|
38
|
+
width: ctx.req.query.w,
|
|
39
|
+
accept: ctx.req.headers.accept ?? ""
|
|
40
|
+
});
|
|
41
|
+
ctx.res.setStatus(answer.status);
|
|
42
|
+
for (const [name, value] of Object.entries(answer.headers)) ctx.res.setHeader(name, value);
|
|
43
|
+
ctx.res.send(answer.body);
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { imagesPathOf, imagesStage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
1
2
|
//#region src/core/server/fleet/channel.ts
|
|
2
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
3
3
|
/** The channel is shared with `node:cluster` and anything else the deployment sends: only these are ours. */
|
|
4
4
|
var isFleetMessage = (value) => {
|
|
5
5
|
if (!isRecord(value)) return false;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
2
|
import { broadcast, onBroadcast } from "./link.js";
|
|
3
|
+
import { isRecord } from "@plitzi/sdk-shared/helpers/isRecord";
|
|
3
4
|
//#region src/core/server/fleet/relay.ts
|
|
4
5
|
/**
|
|
5
6
|
* `server.cache` and `server.plugins` in a fleet, whose workers each keep their own: what one process is told, every
|
|
@@ -10,7 +11,6 @@ import { broadcast, onBroadcast } from "./link.js";
|
|
|
10
11
|
* Code that runs in every process (a pub/sub subscription made where the server is created) is told N times; what
|
|
11
12
|
* is relayed is safe to apply again, and cheap: it only drops entries from memory.
|
|
12
13
|
*/
|
|
13
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
14
14
|
var PLUGIN_ACTIONS = [
|
|
15
15
|
"copy",
|
|
16
16
|
"compile",
|