@plitzi/sdk-server 0.37.8 → 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 +825 -0
- package/README.md +24 -2
- package/dist/actions.d.ts +1 -1
- package/dist/adapters/cloudAdapters.d.ts +5 -3
- package/dist/adapters/cloudAdapters.js +123 -63
- 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/functionRoutes.d.ts +2 -2
- package/dist/core/services/functionRoutes.js +2 -2
- package/dist/core/services/registry.js +2 -0
- package/dist/functions-api.d.ts +23 -2
- package/dist/functions.d.ts +1 -1
- package/dist/functions.js +2 -2
- 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/index.d.ts +1 -1
- package/dist/modules/actions/index.js +3 -2
- 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/renderResolver.js +30 -4
- package/dist/modules/actions/runtime/runAction.js +2 -0
- package/dist/modules/actions/taskCatalog.d.ts +3 -1
- package/dist/modules/actions/taskCatalog.js +1 -0
- package/dist/modules/actions/tasks/registry.js +4 -2
- package/dist/modules/actions/transport/callHandler.js +8 -0
- package/dist/modules/actions/types.d.ts +20 -1
- package/dist/modules/connectors/engine.js +1 -1
- package/dist/modules/functions/capabilities.js +1 -1
- package/dist/modules/functions/config.d.ts +19 -3
- package/dist/modules/functions/config.js +30 -14
- package/dist/modules/functions/contract.d.ts +10 -3
- package/dist/modules/functions/driver.js +4 -2
- package/dist/modules/functions/load.js +2 -2
- package/dist/modules/functions/manifest.d.ts +5 -3
- package/dist/modules/functions/manifest.js +37 -6
- package/dist/modules/functions/routes.d.ts +1 -1
- package/dist/modules/functions/routes.js +3 -3
- package/dist/modules/functions/runner/messages.js +1 -1
- package/dist/modules/functions/space.d.ts +7 -5
- package/dist/modules/functions/space.js +17 -10
- 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/runtime/contract.d.ts +1 -1
- package/dist/modules/runtime/stages.d.ts +1 -1
- package/dist/modules/runtime/stages.js +1 -1
- package/dist/modules/ssr/Component.d.ts +5 -1
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/prepareRender.js +38 -10
- package/dist/modules/ssr/registerExternalPlugins.d.ts +5 -1
- package/dist/modules/ssr/registerExternalPlugins.js +6 -2
- package/package.json +9 -4
- package/skills/plitzi-authoring/CHEATSHEET.md +135 -0
- package/skills/plitzi-authoring/SKILL.md +77 -74
- 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 +83 -0
- 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 +26 -37
- 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 +27 -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} +15 -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
|
@@ -155,6 +155,11 @@ adapters: createJsonAdapters({ offlineData: './space.json' })
|
|
|
155
155
|
`validateSpace({ schema, style })` from `@plitzi/sdk-authoring` answers whether one is servable before you
|
|
156
156
|
serve it — worth running over anything that arrives as a file.
|
|
157
157
|
|
|
158
|
+
A space on Plitzi comes out whole, as a project wired to this server: `npx @plitzi/cli create my-site --from <space>`
|
|
159
|
+
writes its pages as authoring code, its actions as `defineAction` code, its functions, the source of its plugins and
|
|
160
|
+
runtime, and its files into `public/` — a draft, or any published snapshot (`--environment`, `--revision`) — and
|
|
161
|
+
`plitzi pull` keeps it in step. See `docs/en/projects-from-spaces.md` in the workspace.
|
|
162
|
+
|
|
158
163
|
## Configuration
|
|
159
164
|
|
|
160
165
|
| Option | Type | Default | Description |
|
|
@@ -164,6 +169,7 @@ serve it — worth running over anything that arrives as a file.
|
|
|
164
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`. |
|
|
165
170
|
| `assetVersion` | `string` | — | Cache-buster appended as `?v=<assetVersion>` to all default SDK asset URLs. Compute from file mtime or package version at startup. |
|
|
166
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. |
|
|
167
173
|
| `loginPath` | `string \| false` | `'/auth/login'` | Path for the built-in login endpoint. Set to `false` to disable it entirely. |
|
|
168
174
|
| `logoutPath` | `string \| false` | `'/auth/logout'` | Path for the built-in logout endpoint. Set to `false` to disable it entirely. |
|
|
169
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. |
|
|
@@ -171,10 +177,11 @@ serve it — worth running over anything that arrives as a file.
|
|
|
171
177
|
| `plugins` | `Record<string, PluginSource>` | — | Named plugin definitions. Compiled or copied on first use and cached for `pluginsTtlMs`. |
|
|
172
178
|
| `pluginsCacheDir` | `string` | `.sdk-plugins` | Directory where compiled plugin files are stored. |
|
|
173
179
|
| `pluginsTtlMs` | `number` | `604800000` | TTL in milliseconds for compiled plugins (default: 1 week). |
|
|
174
|
-
| `autoLoadSchemaPlugins` | `boolean` | `true` | Auto-download and cache plugins declared in the schema's `offlineData.plugins` list. Set to `false` to manage plugin loading manually. |
|
|
180
|
+
| `autoLoadSchemaPlugins` | `boolean` | `true` | Auto-download and cache plugins declared in the schema's `offlineData.plugins` list. A type the deployment registers itself (`plugins`, named in `pluginNames`) is never looked for there: its own build is the one rendered. Set to `false` to manage plugin loading manually. |
|
|
175
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. |
|
|
176
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. |
|
|
177
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)). |
|
|
178
185
|
| `ssrOnly` | `boolean` | `false` | Omit client-side JS from the rendered page. Useful for verifying SSR HTML without hydration. |
|
|
179
186
|
| `streaming` | `boolean` | `false` | Stream HTML to the browser incrementally to reduce TTFB. See [Streaming](#streaming). |
|
|
180
187
|
| `middlewares` | `SSRMiddleware[]` | — | Array of custom middleware functions executed before the SSR renderer on every request (see [Custom middlewares](#custom-middlewares)). |
|
|
@@ -213,7 +220,7 @@ type SSRAdapters = {
|
|
|
213
220
|
};
|
|
214
221
|
```
|
|
215
222
|
|
|
216
|
-
- **`getOfflineData`** — returns the space snapshot (schema, plugins, styles,
|
|
223
|
+
- **`getOfflineData`** — returns the space snapshot (schema, plugins, styles, collections) for SSR.
|
|
217
224
|
- **`getSpaceDeployment`** — resolves which space and environment to render for a given inbound request. Return `{ error: { code, message } }` to abort with an HTTP error. Optionally include `templateProps` to override template variables, or `pluginNames` to activate plugins for the space (see [Plugins](#plugins) and [Template props](#template-props)).
|
|
218
225
|
- **`getUser`** *(optional)* — resolves the authenticated user from the inbound request (e.g. via a session cookie or `Authorization` header). Called in parallel with `getOfflineData` on every cache miss. Return `undefined` for unauthenticated requests. What it returns is inlined into the page — `authenticated`, `user.details`, the access token and its expiry — and that does two things: it decides page-level access for guest vs. registered users during the render, and it **saves the browser its first auth request**. The SDK adopts the inlined session on boot, stores it and schedules the renewal, rather than asking the same authority the same question a few milliseconds later. Implement this one adapter and every server-rendered page starts signed in with no round trip; `createAuth` answers it for you.
|
|
219
226
|
- **`authenticate`** *(optional)* — called when `POST {loginPath}` is received, with the credentials already parsed from the body (a posted form or JSON), whatever fields they are. Return a session to grant one, `undefined` to refuse — and never throw for a wrong password. **Identity only**: the cookies, their lifetimes and the readable hint are the server's, which is what keeps a session established here visible to the API side and the other way round. For a navigation (full-page form submit, `Sec-Fetch-Mode: navigate`) the server answers a `303` so the view re-renders via a GET; for a fetch it answers `200` with `{ success, access_token, expire_at }` or `401` with `{ error, reason }` — never a bodyless status, which leaves a caller holding nothing. This route knows a session and not the account behind it; `createServer({ auth })` serves the same path from the auth kernel instead, which answers the full grant.
|
|
@@ -381,6 +388,21 @@ createServer({
|
|
|
381
388
|
|
|
382
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.
|
|
383
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
|
+
|
|
384
406
|
### Public directory
|
|
385
407
|
|
|
386
408
|
Use `publicDir` to serve your own root-level files (e.g. `robots.txt`, `sitemap.xml`, `favicon.png`) without prefixes:
|
package/dist/actions.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export type { KvListEntry, KvListPut, KvListPutOptions, KvListRange } from './mo
|
|
|
68
68
|
*/
|
|
69
69
|
export { createRunLogger, createRejectLogger } from './modules/actions/runtime/runLogger';
|
|
70
70
|
export type { TaskRegistryOptions } from './modules/actions/tasks/registry';
|
|
71
|
-
export type { ActionCredential, ActionDbDriver, ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionJobsConfig, ActionKvAdapter, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './modules/actions/types';
|
|
71
|
+
export type { ActionCredential, ActionDbDriver, ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionJobsConfig, ActionKvAdapter, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskOrigin, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './modules/actions/types';
|
|
72
72
|
export type { ActionsModule } from './modules/actions';
|
|
73
73
|
export type { ActionJobs, JobWorker, JobWorkerOptions, MemoryJobQueueOptions, Scheduler, SchedulerOptions, SweepResult } from './modules/actions/jobs';
|
|
74
74
|
export type { ActiveRun, RunGuards } from './modules/actions/runtime/guards';
|
|
@@ -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.
|
|
@@ -27,22 +29,14 @@ var SPACE_QUERY = `query InitQuery($environment: String!, $revision: Int) {
|
|
|
27
29
|
Space(environment: $environment, revision: $revision) {
|
|
28
30
|
schema {
|
|
29
31
|
settings
|
|
30
|
-
|
|
32
|
+
rsc
|
|
33
|
+
flat { id definition { label type initialState styleSelectors bindings interactions parentId rootId items runtime loadStrategy flag { name is } anchor } attributes }
|
|
31
34
|
pages
|
|
35
|
+
components
|
|
36
|
+
flags
|
|
32
37
|
pageFolders { id name slug parentId }
|
|
33
38
|
variables { name type value subValues { when value } }
|
|
34
39
|
}
|
|
35
|
-
segments {
|
|
36
|
-
id
|
|
37
|
-
identifier
|
|
38
|
-
definition
|
|
39
|
-
schema {
|
|
40
|
-
settings
|
|
41
|
-
variables { name type value subValues { value when } }
|
|
42
|
-
flat { id definition { label type initialState styleSelectors bindings interactions parentId rootId items } attributes }
|
|
43
|
-
}
|
|
44
|
-
style { cache }
|
|
45
|
-
}
|
|
46
40
|
plugins { type resource settings }
|
|
47
41
|
style { variables cache }
|
|
48
42
|
}
|
|
@@ -57,44 +51,19 @@ var SPACE_QUERY = `query InitQuery($environment: String!, $revision: Int) {
|
|
|
57
51
|
var LATEST_REVISION_QUERY = `query SpaceLatestRevisionQuery($environment: String!) {
|
|
58
52
|
SpaceLatestRevision(environment: $environment) {
|
|
59
53
|
snapshot { revision }
|
|
54
|
+
flagsHash
|
|
60
55
|
}
|
|
61
56
|
}`;
|
|
62
57
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* Without it nothing throws — an array is a perfectly good object — it just answers `undefined` to every lookup, so
|
|
68
|
-
* the page router matches no page and every URL is a 404 on a space that fetched and parsed correctly.
|
|
69
|
-
*/
|
|
70
|
-
var byElementId = (flat) => {
|
|
71
|
-
if (!Array.isArray(flat)) return flat ?? {};
|
|
72
|
-
return flat.reduce((acum, element) => {
|
|
73
|
-
if (element.id) acum[element.id] = element;
|
|
74
|
-
return acum;
|
|
75
|
-
}, {});
|
|
76
|
-
};
|
|
77
|
-
/** The runtime shape of a schema: the wire's `flat` list, keyed. */
|
|
78
|
-
var toSchema = (schema) => ({
|
|
79
|
-
...schema,
|
|
80
|
-
flat: byElementId(schema.flat)
|
|
81
|
-
});
|
|
82
|
-
/**
|
|
83
|
-
* Segments arrive as a list and are read by identifier, which is how every other reader of a space holds them. Each
|
|
84
|
-
* one carries a schema of its own, so its `flat` needs the same keying the space's does.
|
|
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.
|
|
85
61
|
*/
|
|
86
|
-
var
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
...acum,
|
|
92
|
-
[identifier]: {
|
|
93
|
-
...entry,
|
|
94
|
-
...entry.schema && { schema: toSchema(entry.schema) }
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}, {});
|
|
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);
|
|
98
67
|
/**
|
|
99
68
|
* The `scope` claim, read without verifying anything.
|
|
100
69
|
*
|
|
@@ -172,16 +141,74 @@ var createCloudAdapters = (config) => {
|
|
|
172
141
|
}, `space ${env}@${rev ?? "latest"}`))?.Space;
|
|
173
142
|
if (!space?.schema) return;
|
|
174
143
|
return {
|
|
175
|
-
schema:
|
|
144
|
+
schema: schemaFromWire(space.schema),
|
|
176
145
|
style: space.style,
|
|
177
|
-
plugins: space.plugins
|
|
178
|
-
segments: byIdentifier(space.segments)
|
|
146
|
+
plugins: space.plugins
|
|
179
147
|
};
|
|
180
148
|
};
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
};
|
|
183
156
|
};
|
|
184
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
|
+
};
|
|
185
212
|
const held = /* @__PURE__ */ new Map();
|
|
186
213
|
/** One request per key at a time: a burst of renders on a cold cache joins the first, it does not multiply it. */
|
|
187
214
|
const inFlight = /* @__PURE__ */ new Map();
|
|
@@ -229,7 +256,9 @@ var createCloudAdapters = (config) => {
|
|
|
229
256
|
* A probe that fails leaves `checkedAt` alone so the next request tries again, and leaves the held copy serving.
|
|
230
257
|
*/
|
|
231
258
|
const refreshLatest = async (env) => {
|
|
232
|
-
|
|
259
|
+
await restoreFlags(env);
|
|
260
|
+
const { revision: current, flagsHash } = await probe(env);
|
|
261
|
+
await syncFlags(env, flagsHash);
|
|
233
262
|
const previous = held.get(env);
|
|
234
263
|
if (current === void 0) return previous?.data;
|
|
235
264
|
if (previous && previous.revision === current) {
|
|
@@ -239,10 +268,27 @@ var createCloudAdapters = (config) => {
|
|
|
239
268
|
return await loadRevision(env, current) ?? previous?.data;
|
|
240
269
|
};
|
|
241
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
|
+
/**
|
|
242
287
|
* The space, and how hard this deployment leans on Plitzi to get it.
|
|
243
288
|
*
|
|
244
289
|
* - **`main`**: read live, every time. It is being edited; a cached answer is a wrong one.
|
|
245
|
-
* - **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.
|
|
246
292
|
* - **Latest**: the held copy is served immediately and a revision probe runs behind it once the window is up.
|
|
247
293
|
* After the first render no page ever waits on Plitzi, and a failed probe or fetch changes nothing — the last
|
|
248
294
|
* good copy keeps serving, because a self-hosted site going blank over somebody else's bad minute is not a
|
|
@@ -252,16 +298,21 @@ var createCloudAdapters = (config) => {
|
|
|
252
298
|
if (isLive(env)) return once(`live:${env}`, () => fetchSpace(env));
|
|
253
299
|
const pinned = rev ?? revision;
|
|
254
300
|
if (pinned !== void 0 && isPinned(env, pinned)) {
|
|
301
|
+
await refreshPinnedFlags(env);
|
|
255
302
|
const already = held.get(env);
|
|
256
|
-
|
|
303
|
+
const data = already?.revision === pinned ? already.data : await loadRevision(env, pinned);
|
|
304
|
+
return data && withFlags(env, data);
|
|
257
305
|
}
|
|
258
306
|
const current = held.get(env);
|
|
259
|
-
if (!current)
|
|
307
|
+
if (!current) {
|
|
308
|
+
const data = await refreshLatest(env);
|
|
309
|
+
return data && withFlags(env, data);
|
|
310
|
+
}
|
|
260
311
|
if (Date.now() - current.checkedAt >= cacheSeconds * 1e3) {
|
|
261
312
|
current.checkedAt = Date.now();
|
|
262
313
|
refreshLatest(env).catch(() => void 0);
|
|
263
314
|
}
|
|
264
|
-
return current.data;
|
|
315
|
+
return withFlags(env, current.data);
|
|
265
316
|
};
|
|
266
317
|
/**
|
|
267
318
|
* Every request is this one space.
|
|
@@ -270,7 +321,7 @@ var createCloudAdapters = (config) => {
|
|
|
270
321
|
* builds two servers or supplies its own resolver — which is more honest than a lookup table that silently
|
|
271
322
|
* serves the wrong space when a domain is added and this is not.
|
|
272
323
|
*
|
|
273
|
-
* 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
|
|
274
325
|
* found: a deployment reading `at: { environment, revision }` must be told the version its page was built from,
|
|
275
326
|
* not the zero that means "whatever is live".
|
|
276
327
|
*/
|
|
@@ -281,18 +332,27 @@ var createCloudAdapters = (config) => {
|
|
|
281
332
|
environment,
|
|
282
333
|
revision: 0
|
|
283
334
|
};
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
environment,
|
|
288
|
-
revision
|
|
335
|
+
const flagsVersion = () => {
|
|
336
|
+
const flags = heldFlags.get(environment);
|
|
337
|
+
return flags ? { flagsVersion: flags.hash } : {};
|
|
289
338
|
};
|
|
339
|
+
if (isPinned(environment, revision)) {
|
|
340
|
+
await refreshPinnedFlags(environment);
|
|
341
|
+
return {
|
|
342
|
+
...deployment,
|
|
343
|
+
spaceId,
|
|
344
|
+
environment,
|
|
345
|
+
revision,
|
|
346
|
+
...flagsVersion()
|
|
347
|
+
};
|
|
348
|
+
}
|
|
290
349
|
if (!held.has(environment)) await refreshLatest(environment);
|
|
291
350
|
return {
|
|
292
351
|
...deployment,
|
|
293
352
|
spaceId,
|
|
294
353
|
environment,
|
|
295
|
-
revision: held.get(environment)?.revision ?? 0
|
|
354
|
+
revision: held.get(environment)?.revision ?? 0,
|
|
355
|
+
...flagsVersion()
|
|
296
356
|
};
|
|
297
357
|
};
|
|
298
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;
|