@plitzi/sdk-server 0.38.9 → 0.38.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +3 -3
  3. package/dist/auth.js +1 -1
  4. package/dist/core/auth/createAuth.js +1 -1
  5. package/dist/core/http/stages/signInRoute.js +1 -1
  6. package/dist/core/server/baseServer.js +11 -6
  7. package/dist/core/server/fleet/stores.d.ts +7 -0
  8. package/dist/core/server/fleet/stores.js +16 -2
  9. package/dist/core/server/fleet/supervisor.d.ts +2 -0
  10. package/dist/core/server/fleet/supervisor.js +14 -2
  11. package/dist/core/server/pageServer.js +1 -1
  12. package/dist/functions-api.d.ts +29 -0
  13. package/dist/helpers/cache/cacheManager.d.ts +9 -2
  14. package/dist/helpers/cache/cacheManager.js +46 -22
  15. package/dist/helpers/cache/keys.d.ts +12 -5
  16. package/dist/helpers/cache/keys.js +56 -11
  17. package/dist/modules/actions/index.js +6 -2
  18. package/dist/modules/actions/jobs/later.d.ts +43 -0
  19. package/dist/modules/actions/jobs/later.js +66 -0
  20. package/dist/modules/actions/jobs/later.test.d.ts +1 -0
  21. package/dist/modules/actions/jobs/memoryQueue.js +14 -1
  22. package/dist/modules/actions/jobs/worker.js +2 -2
  23. package/dist/modules/actions/runtime/credentials.d.ts +10 -0
  24. package/dist/modules/actions/runtime/credentials.js +28 -0
  25. package/dist/modules/actions/runtime/credentials.test.d.ts +1 -0
  26. package/dist/modules/actions/runtime/fileKv.d.ts +8 -3
  27. package/dist/modules/actions/runtime/fileKv.js +79 -7
  28. package/dist/modules/actions/runtime/precheck.js +1 -1
  29. package/dist/modules/actions/runtime/runAction.d.ts +4 -1
  30. package/dist/modules/actions/runtime/runAction.js +26 -4
  31. package/dist/modules/actions/runtime/taskContext.d.ts +4 -1
  32. package/dist/modules/actions/runtime/taskContext.js +7 -1
  33. package/dist/modules/actions/tasks/flow.js +113 -35
  34. package/dist/modules/actions/types.d.ts +8 -0
  35. package/dist/modules/functions/build.js +1 -1
  36. package/dist/modules/functions/capabilities.js +21 -0
  37. package/dist/modules/functions/context.js +6 -0
  38. package/dist/modules/functions/contract.d.ts +10 -0
  39. package/dist/modules/functions/driver.js +8 -0
  40. package/dist/modules/functions/local.d.ts +1 -1
  41. package/dist/modules/functions/local.js +2 -1
  42. package/dist/modules/functions/manifest.js +6 -1
  43. package/dist/modules/functions/protocol.d.ts +7 -0
  44. package/dist/modules/mongo/jobQueue.js +26 -0
  45. package/dist/modules/mysql/jobs/jobQueue.js +21 -2
  46. package/dist/modules/mysql/jobs/schema.d.ts +3 -2
  47. package/dist/modules/mysql/jobs/schema.js +24 -11
  48. package/dist/modules/project/serveProject.d.ts +5 -2
  49. package/dist/modules/project/serveProject.js +31 -6
  50. package/dist/modules/realtime/handlers.d.ts +4 -1
  51. package/dist/modules/realtime/handlers.js +8 -4
  52. package/dist/modules/realtime/hub.d.ts +3 -1
  53. package/dist/modules/realtime/hub.js +8 -4
  54. package/dist/modules/ssr/prepareRender.js +8 -6
  55. package/dist/modules/ssr/resolvePageSeo.d.ts +4 -1
  56. package/dist/modules/ssr/resolvePageSeo.js +7 -3
  57. package/package.json +3 -3
  58. package/skills/plitzi-authoring/CHEATSHEET.md +4 -2
  59. package/skills/plitzi-authoring/recipes/filter-a-list.ts +0 -2
  60. package/skills/plitzi-authoring/recipes/forms-and-modals.ts +2 -1
  61. package/skills/plitzi-authoring/recipes/style-a-page.ts +1 -1
  62. package/skills/plitzi-authoring/reference/accessibility.md +1 -0
  63. package/skills/plitzi-authoring/reference/authoring-errors.md +9 -4
  64. package/skills/plitzi-authoring/reference/data-and-visibility.md +11 -10
  65. package/skills/plitzi-authoring/reference/flows.md +2 -2
  66. package/skills/plitzi-authoring/reference/layouts.md +5 -2
  67. package/skills/plitzi-authoring/reference/lists.md +1 -0
  68. package/skills/plitzi-authoring/reference/plugins.md +3 -2
  69. package/skills/plitzi-authoring/reference/realtime.md +4 -1
  70. package/skills/plitzi-authoring/reference/structure.md +3 -1
  71. package/skills/plitzi-authoring/reference/typed-sources.md +6 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,268 @@
1
1
  # @plitzi/sdk-server
2
2
 
3
+ ## 0.38.11
4
+
5
+ ### Patch Changes
6
+
7
+ - cadf70a: - **`server.cache.invalidate` drops what a page is rendered from** (`@plitzi/sdk-server`): the pages, the RSC answers
8
+ and the schema data they are rendered from go together. It dropped the pages alone, and the next request rendered
9
+ the same page again from the schema still cached under the same key — a publish that invalidated the space changed
10
+ nothing until the schema's TTL ran out. `clear()` and `size` cover the three. A filter on `hostname` drops the schema
11
+ data too, which belongs to no one host. `server.cache` is `null` only when nothing is cached (`cacheTtlMs: 0` and
12
+ `rsc.cacheTtlMs: 0`).
13
+
14
+ ## Style inspector
15
+ - **Finds a property** (`@plitzi/sdk-style`): a search above the categories narrows them to the ones that edit what was
16
+ typed — a CSS name in any order of its words (`border radius` finds the four corners), a category's title, or a
17
+ common word for it (`bg`, `rounded`) — opens them, and shows the advanced rows when the match lives there. Escape
18
+ clears it. The categories' properties live in one list (`categoryKeys.ts`) that the dots, the advanced hint and the
19
+ search all read.
20
+ - **Rows that fit**: a section with more than two controls puts its label above them and lays them out in as many
21
+ columns as the panel is wide, instead of one row where every label was cut (`Appearan…`, `Cell Spaci…`, the five
22
+ scroll-snap controls). A row's label column is 80px.
23
+ - **Where a value comes from, said quietly**: the label's tint keeps its four meanings — set here, from a token, bound
24
+ to data, inherited — without the solid blocks of colour; hovering says which (an inherited one names its selector and
25
+ breakpoint) and that a click resets it, the click shows it by striking the label through. The footer's info icon
26
+ holds the legend, and its arrow folds every category.
27
+ - **What is being edited**: an "Editing … › Hover" line under the pickers whenever the rules being written are not the
28
+ selector's plain ones, with a button back to them. Ancestor, pseudo-element and condition fold behind a toggle —
29
+ never while one is in use.
30
+ - The class being edited is the panel's one strong mark (violet), the element's type defaults a softer one; the tools'
31
+ tabs fit the panel and are reachable from the keyboard.
32
+
33
+ - **Lists edited in a popover, all alike**: box and text shadows, transforms, transitions and filters share one row
34
+ (the CSS it writes, a swatch, a remove button) and one popover (a title, a close button, Escape, the focus moved
35
+ into it). What none of them could read is now kept and edited as text instead of being replaced with a default — a
36
+ token for the whole value, `drop-shadow(…)`, `url(#…)`.
37
+ - **Values read the way CSS writes them**: a shadow with two, three or four lengths and its color on either end;
38
+ `rgba(…)` inside text shadows and filters no longer split; negative shadow offsets accepted; a transition with any
39
+ of its parts left out; brightness, contrast and saturation past 1. Transition presets are written as CSS — the
40
+ preset names (`easeInQuad`) made the browser drop the whole declaration — the curve can be dragged into one of your
41
+ own, and the preview stops with the editor. `font-color` (not CSS) became `color`.
42
+ - **Background layers keep what they hold**: a repeating gradient (now a switch), a radial gradient's explicit size, a
43
+ token or `image-set()` as a layer (a "Custom (CSS)" layer), a one-value position read as CSS reads it (`20%` is
44
+ `20% center`), lists shorter than the layers repeated as CSS repeats them, an unset repeat read as tiling, two-position
45
+ stops, negative angles — every one of them was rewritten on the next edit of any layer. Defaults are no longer
46
+ written back, and "Token values" no longer bakes resolved tokens into the layers.
47
+ - **A layer's editor in two halves** — what it draws, then where it goes (size, position, tile, attachment, clip,
48
+ which now offers `text`). The gradient's stops sit on one bar, each a handle that drags or moves with the arrow keys;
49
+ the stop's color and position below it. Previews and swatches resolve the space's tokens.
50
+ - Spacing tells margin (dashed, outside) from padding; Border's sides are labelled and readable in the dark theme; a
51
+ class's menu says what each action does to whom; the Style Manager opens at a size that holds both columns.
52
+
53
+ ## Builder
54
+ - **Shortcuts**: `?` shows every shortcut the builder answers to — the same list the "Nothing selected" card teaches
55
+ from — and ⌘\ / Ctrl+\ hides both side panels for the canvas alone and brings back the ones that were open. Both
56
+ work with the canvas focused.
57
+ - **Panels keep their width**: each side panel opens at the width it was left at; the right one starts at 380px.
58
+ - **Text no smaller than 11px** across the builder's panels (it went down to 10px in fifty places).
59
+
60
+ ## Realtime
61
+ - **A page hears what it sent when it asks to** (`@plitzi/sdk-shared`, `@plitzi/sdk-server`, `@plitzi/sdk-elements`):
62
+ `publishOn('room', 'react', data, { echo: true })`, `useChannel().publish(type, data, { echo: true })` and the
63
+ channel's `publish` callback (`echo`) hand the message back to the page that sent it too, once the server took it —
64
+ stamped like everyone's, with `echo: true`, its `from` the channel's `me`. Without it a page still never hears its
65
+ own messages, and the docs said "every page on its topic hears it": they now say every OTHER page. Presence never
66
+ echoes.
67
+ - **A topic is opened only once it names one** (`@plitzi/sdk-elements`): `useChannel` — and so the `channel` element —
68
+ opens a topic only when a channel of the space covers it, by the same `matchChannel` the server decides by. A topic
69
+ written from data that has not arrived (`room:` while its provider loads, or its template still as text) asked the
70
+ server for both, was refused with a 403 and a console error on every load, and is now waited for.
71
+ - **Pages that watch** (`@plitzi/sdk-server`, `@plitzi/cli`): a connection whose page carries the `plitzi-observer`
72
+ cookie hears its topics and says nothing on them — no `$join`, no presence, its publishes taken and dropped.
73
+ `page check`, `page shot` and `verify` load pages that way, so checking a live site is no longer a visitor walking
74
+ into a room (`X walked in`, a member more in every picture). `--presence` takes part as a visitor does.
75
+
76
+ ## Authoring
77
+ - **`template-in-value`** (refused): a `{{ }}` inside an attribute that is an object or a list — a provider's
78
+ `input`, a list's `items`, a plugin's settings — is never evaluated: only an attribute that is text is interpolated,
79
+ and the page server reads attributes as saved. A provider's `input` also replaced the route param of the same name
80
+ the action was already given, so `input: { room: '{{ navigation.routeParams.room }}' }` reached the task as `""` and
81
+ the page answered 404; its refusal says the action is handed the page's route and query params already. Anything
82
+ else is a binding on the attribute. `mockData`, a sample, is not held to it.
83
+ - **Rules on top of a class need no `id`** (`class: [card, { gap: '6px' }]`): they are named after the id the element
84
+ is given, written or not. `modifier-without-id` is gone; a one-off tweak no longer needs a name nothing reads.
85
+ - **Typed list rows have `inTemplate`**, as untyped ones do, and `bindTemplate`, `visibleWhen`, `hiddenWhen` and
86
+ `variantFrom` take a typed source's path as well as a name: turning a list typed broke every template of its rows.
87
+ - **`when({ field, operator: 'empty' })`** takes no `value` (nor `notEmpty`), as flows.md recommends; it did not
88
+ type-check.
89
+ - **Data-driven colours**: a style binding on a custom property (`{ to: '--who', category: 'style' }`) is written as
90
+ named — it was camel-cased into `who` — so a class reads `var(--who, var(--muted))`. A style binding's value carrying
91
+ `;` or braces is not written: the page server writes inline styles as text, and such a value added declarations of
92
+ its own. `binding-target-unknown` on `style` or a CSS property says to bind it in the `style` category.
93
+ - `unknown-attribute` for `decorative` on an `svg` or a `fontAwesome` says what it means: without a `label` it is
94
+ already hidden from screen readers.
95
+
96
+ - **A folder nobody declared says where to declare it**: `folder-undeclared` (a page, a layout, a folder's parent)
97
+ ends with `pageFolders: [{ id, name }]` on the space, and with the line of the `pageFamily` that wrote the page. A
98
+ folder starts its pages' addresses (`/docs/quickstart`), so a misspelt one is never declared on the author's behalf.
99
+ The skill's `pageFamily` example declares its folder.
100
+ - **A tag is an element**: a `subType` this element has not but another has — `container({ subType: 'ol' })` — names
101
+ that element (`list({ subType: 'ol' })`) instead of the value two letters away (`dl`). Read off the elements'
102
+ declarations, for every element and tag.
103
+ - **A `pageFamily`'s body is written once**: `repeated-shape` and `repeated-on-pages` no longer count the same part of
104
+ each page of one family as copies — they were offering a component for exactly what the docs recommend. Copies within
105
+ one page, or across pages written one by one, are still offered.
106
+ - **Colour emoji are not text to `page check`**: a character painted in its own colours (🎲, `❤️`, a keycap, a ZWJ
107
+ sequence) is left out of the contrast measure, which compared `color` with the tile behind it and failed `verify`
108
+ in the theme whose text colour was close to the tile. A symbol drawn as text (✓, ★) is still measured.
109
+
110
+ - **`declared-callback-target`** (refused): a value beside `on` in `declaredCallback(…, { on, date })` — written as
111
+ `openModal`'s target reads — was read nowhere, and the step ran without it. Refused, with the `params` it goes in
112
+ (`{ on: 'calendar', params: { date } }`) and whether it is one of the callback's declared params.
113
+ - The skill's list rows show the typed form beside the untyped one: with `items` typed by a sample, `r.item` is a path
114
+ (`from: r.item.title`), not text to put in a template string.
115
+ - **`slot-not-class`** (refused): rules written in place in a slot — `slots: { error: [fieldError, { color }] }`, past
116
+ the type in JavaScript or a spec rebuilt from JSON — were taken for a declaration with no rules, and `authorSpace`
117
+ failed later with `Cannot read properties of undefined (reading 'desktop')`, naming nothing. Refused where the slot
118
+ is written, naming the slot and the element, with the `styles()` declaration to hand instead.
119
+
120
+ ## CLI
121
+ - **`page shot --click` (and `--clip`, `--wait-for`, `--scroll-to`, `--steps`) takes any selector**: a value that is
122
+ not an element's name (letters, digits, `-`, `_`) is a selector, CSS or Playwright's own (`button:has-text("Orbit")`).
123
+ It was wrapped as a name, and an invalid selector ended the command with Playwright's stack trace; it is now refused
124
+ with the reason.
125
+
126
+ - **`plitzi functions add <package>`**: a package the functions use comes with them, as a function carries its
127
+ dependencies. Installed in the project, it is bundled (web APIs only, minified, its licence kept) into
128
+ `src/functions/vendor/<package>.js`, with a `.d.ts` re-exporting the installed package's types, imported as
129
+ `./vendor/<package>.js` and pushed, built and run with the rest — nothing is resolved anywhere else, so the builder,
130
+ the sandbox and the push are unchanged. A package that reaches Node (`fs`, `node:*`) is refused, naming what it
131
+ reached; one past the functions' 1 MB is too; one that reads `process`/`Buffer` is warned about. The build's refusal
132
+ of a package import now names the command.
133
+ - **`page shot` targets**: a bare word is an element's name, or — no element named so — a tag (`textarea`); resolved on
134
+ the loaded page for `--click`, `--clip`, `--scroll-to`, `--wait-for` and `--steps`. In `--steps`, `type` takes a
135
+ quoted element with spaces (`type ".panel textarea" hola`) — it was cut at its first space and typed the rest — and
136
+ fails when the click leaves no field focused, rather than sending the keys to the page's shortcuts.
137
+
138
+ ## Server
139
+ - **`server.listen()` resolves once the port answers** (`@plitzi/sdk-server`) — in a fleet, once the first worker
140
+ does. `serveProject` writes `tmp/dev-server.json` and says `pages on …` after it, not on the line after `listen`:
141
+ for about 0.4 s a tool trusting the line (CI, Playwright's `webServer`, `page check`) was refused. A port it cannot
142
+ take still ends the process, or goes to `onListenError`, and the promise is then never resolved.
143
+ - **Runs set for later** (`@plitzi/sdk-server`, `@plitzi/sdk-shared`, `@plitzi/sdk-authoring`, `@plitzi/plitzi-builder`):
144
+ a turn that runs out, a bot's move, a hold that lapses — one run starting one of the space's actions in N seconds,
145
+ whether or not a page is still open. The action says it may be with a **`later` trigger** (no access rule: the run
146
+ that set it was let in); a flow sets it with `flow.later { action, in, input, key }`, a function with
147
+ `ctx.later({ … })`, and `flow.cancelLater` / `ctx.cancelLater(key)` drops it. A `key` names it: set again under the
148
+ same key, the one still waiting is replaced — the newer of two set at once, on every replica — and a running one is
149
+ left. It is a job of the queue schedules use: due by the store's clock (0 s to 30 days, about a second late at most),
150
+ once across replicas, retried, shown in Automations → Queue. Refused before anything is queued, with why: no `later`
151
+ trigger, one switched off, a time out of range, a key that is not one, a server with no jobs. A plugin's functions
152
+ set none. `ActionJobQueue` gains `cancelPending({ spaceId, key, olderThan? })` and jobs a `key`: the memory, Mongo
153
+ and MySQL queues implement it, and the shared contract tests it. **MySQL**: `action_jobs` gains `job_key` (and the
154
+ `action_jobs_key` index), added on first use; with `createTables: false`, run that statement of
155
+ `mysqlJobSchemaUpgrades()`. A queue of a deployment's own implements `cancelPending` (`09-schedules` shows one).
156
+ - **A project's `kv` file has one holder** (`@plitzi/sdk-server`): `createFileKv` takes `<file>.lock` with its pid;
157
+ a second process — another `npm start`, a `--watch` left running — is refused, naming the first (`the server of
158
+ "orbita", on port 8080`) instead of both writing their whole map over each other's changes, and a lock whose process
159
+ is gone is taken over. `close()` writes what is pending and lets go. A server with `workers` opens the file once, in
160
+ the primary, and its workers reach it through the fleet (`fleetKv`): each used to open a copy of its own.
161
+ - **A self-hosted project's credentials** (`@plitzi/sdk-server`, `@plitzi/cli`): `serveProject` answers
162
+ `getCredential` from `PLITZI_CREDENTIALS` in `.env` — one JSON object, credential id → its keys — so
163
+ `ctx.fetch({ credential })`, a connector and an `http.request` step work on the project's own server, not only in
164
+ `functions dev`. The same variable for both (it was `PLITZI_FUNCTIONS_CREDENTIALS`, read by the CLI alone); a value
165
+ that is not that object is refused with how to write it, instead of every credential going missing.
166
+ - **`HOST` beyond loopback says where to open it**: with `HOST=0.0.0.0`, `serveProject` prints the machine's network
167
+ addresses — what a tablet on the same Wi-Fi opens — and that anyone on that network can. `.env.example` names `HOST`
168
+ and `PLITZI_CREDENTIALS`.
169
+
170
+ ## SDK
171
+ - **`useElementSize(ref)`** (`@plitzi/plitzi-sdk`): the size a plugin's own box is drawn at, followed as it changes —
172
+ what `useDisplayMode()` cannot say: a tablet held upright is `mobile` by the window and still a week wide by the
173
+ plugin's box. `undefined` until measured, so the server's page and the hydrated one agree. The breakpoints are
174
+ unchanged.
175
+ - The functions docs say how a task's `params` types (how the builder edits them: `codemirror-json`) and an action's
176
+ `input` types (what the value is: `json`) correspond.
177
+
178
+ - Updated dependencies [cadf70a]
179
+ - @plitzi/plitzi-sdk@0.38.11
180
+ - @plitzi/sdk-shared@0.38.11
181
+
182
+ ## 0.38.10
183
+
184
+ ### Patch Changes
185
+
186
+ - 200da2f: - **`focus-on-field-box`** (`@plitzi/sdk-authoring`): a warning for a `focus` or `focus-visible` state on a text
187
+ field's or a select's `input` slot. That slot is the box the field is drawn in, a `<div>` that never takes focus, so
188
+ the rule was never seen and a keyboard user tabbing to the field found no ring. The fix it names: `'focus-within'` on
189
+ the same class, or the state on the `field` slot.
190
+ - **`modalContainer` says how it is laid out** (`@plitzi/sdk-elements`): its description now names its slots and how
191
+ they sit — the dim layer and the dialog side by side, not one inside the other; the dialog centred by `top`/`left`
192
+ 50% and a `translate`. A space styled the dim layer as a flex container to place the dialog, which moved nothing.
193
+ - **A field shows where the keyboard is** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-elements`): a text field's or a select's
194
+ box is ringed (`2px solid currentColor`) while the field inside has keyboard focus, and a textarea while it has. No
195
+ text field or select showed any focus at all: the box had `outline: none`, and the field inherited it. One class of
196
+ specificity, so whatever a space writes about the outline wins; never in the builder's canvas, where the selection is
197
+ the ring. The field no longer inherits the box's outline — the box draws it.
198
+ - **The dim layer of a modal and a dialog is `rgb(0 0 0 / 50%)`** (`@plitzi/plitzi-sdk`, `@plitzi/sdk-elements`), not
199
+ black at `opacity: 0.5`: the default looks the same, and a colour a space gives the layer is drawn as it is — it was
200
+ halved. A space that set its own colour sees it darker: the one it wrote.
201
+ - **`required-message-unused`** (`@plitzi/sdk-authoring`): a warning for a `requiredMessage` on a field nothing requires —
202
+ since 0.38.9 a field is optional unless `required: true`, so the message is never shown and an empty answer is sent.
203
+ The formControl's description, the cheat sheet and the forms recipe say the default.
204
+ - **A page's title can say its record** (`@plitzi/sdk-shared`, `@plitzi/sdk-server`, `@plitzi/sdk-elements`,
205
+ `@plitzi/sdk-authoring`): `seoTitle` and `seoDescription` may be templates over the page's `runtime: 'server'`
206
+ providers (its layouts' too) and `navigation` — `'{{ apiContainer_post.title }} — Blog'`. The server evaluates them
207
+ as it writes the head, from the answers it already has; the browser evaluates the same over `rsc.data`, so the tab
208
+ follows a navigation from record to record. One that reads anything else — a browser provider, `state` — is refused
209
+ (`page-template`): it was written into the head as it was, braces included. A declaration's `serverTemplates` names
210
+ the attributes the page server evaluates against a context of its own (a page's title and description, a provider's
211
+ `notFound`): the element runtime leaves them as written — interpolated where the page renders, a `|default(…)`
212
+ resolved to its fallback before the head could read the providers — and the authoring lint reads the same list.
213
+ - **A page says when its address shows nothing** (`@plitzi/sdk-server`, `@plitzi/sdk-authoring`): `notFound` on the
214
+ page, one expression over the same answers its title reads — `"{{ not (apiContainer_feed.topics|find('slug',
215
+ navigation.routeParams.slug)) }}"` — sends it with status 404, rendered as written. A provider's own `notFound` reads
216
+ only its answer, and one in a layout is every page's: a record read from the layout had no way to say it was not
217
+ there, and the page answered 200.
218
+ - **The Export keeps the flows of a plugin hosted by `custom`** (`@plitzi/sdk-authoring`): `specFromSpace` judged such
219
+ an element by `custom`'s own triggers and callbacks, and dropped a flow on the component's event (`onChange` on an
220
+ editor) as one "a custom never fires" — the space read back was not the space written.
221
+ - **`page check` reads every word on the page, in the colour the browser paints it** (`@plitzi/sdk-authoring`): what a
222
+ plugin draws inside an element of the space is measured too, named by the element and a selector; the measure is
223
+ WCAG's contrast (under 2:1 is unreadable), not "nearly the same colour"; and a colour the browser answers as
224
+ `oklch()`, `oklab()` or `color(srgb …)` — every token and every `color-mix()` — is read as that, where it was read as
225
+ `rgb()` numbers and came out near black. A plugin's dark-on-dark status line passed as "nothing wrong".
226
+ - **`verify` checks every theme the space can be painted in** (`@plitzi/cli`): light and dark, unless the space says
227
+ light only — "48 checks at 1440 and 390 px, light and dark". A page unreadable in the dark theme passed in the light
228
+ one; a failure names the theme it was found in.
229
+ - **`plitzi upgrade` says where fields became optional** (`@plitzi/cli`): for a project last upgraded before 0.38.9,
230
+ each `formControl(…)` with no `required`, at its file and line — said, never written. And a `.gitkeep` is asked for
231
+ only in a folder with nothing else in it: `upgrade` wrote one beside code, and `doctor` called it missing once deleted
232
+ as AGENTS.md asks ("nothing unused") — the three now say the same.
233
+ - **`devMode` and `devReload` are the project's** (`@plitzi/sdk-server`): `src/config/serverOptions.ts` may set them,
234
+ over `NODE_ENV`, which they still follow when left out. A deployment started without `NODE_ENV` (a container's `CMD`)
235
+ says `devMode: false`, and a public action answers with its output alone.
236
+ - **`page shot --from load`** (`@plitzi/cli`): `--frames` start as soon as the page's HTML is in, not once it settles —
237
+ an entrance that plays while the page loads had ended before the first picture.
238
+ - **`heavy-animation` says where each animation is** (`@plitzi/sdk-authoring`): the class and its variant, state or
239
+ slot — or the `customCss` rule — and writes the fix against that selector. It said "in `customCss`" for an animation
240
+ declared on a class's variant, and offered `.glow`, a class no space has.
241
+ - **`page shot --steps`** (`@plitzi/cli`): an interaction played step by step — `click`, `type`, `press`, `wait`,
242
+ `wait-for`, `shot [label]`, `frames <n> [ms]`, separated by `;` or written in a file (`@file`) — each picture labelled
243
+ with its step and the time since the first, and said when nothing changed since the one before. A queue filling, a
244
+ restart, an opening frame by frame took a Playwright script of the project's own. Targets take Playwright's selectors
245
+ too (`text=Saved`).
246
+ - **A space's task may take a `number` param** (`@plitzi/sdk-server`): saved functions refused it ("a type the builder
247
+ cannot draw") while a project's server ran it, so code that worked self-hosted failed on the platform — and the
248
+ builder draws one, a callback's. A step's params now arrive as the types the task declares, on every server: a
249
+ `number` written as text is the number, a `boolean` written `'true'` is `true`, as the browser reads a callback's.
250
+ - **`page shot --as <username>`** (`@plitzi/cli`): signs in first, as `page check --as` does, the password from
251
+ `PLITZI_CHECK_PASSWORD` in `.env`. A page for signed-in visitors was pictured as the sign-in page it sent the browser
252
+ to, without a word; `page shot` now says when the page sent it elsewhere, and that the picture is of that page.
253
+ - **A capture is of the page at rest** (`@plitzi/sdk-mcp`): the local screenshot client takes every picture with
254
+ `prefers-reduced-motion: reduce`, as the browser pod now does — the declared motion and a space's own keyframes that
255
+ honour the query are shown finished. A splash still covering its page, or an arrival caught halfway, was what a
256
+ thumbnail or a `plitzi_look` showed of a page that animates as it loads.
257
+ - **The server renders in the visitor's theme, not only the class** (`@plitzi/sdk-shared`): `ThemeProvider` set the
258
+ starting theme from a layout effect, which never runs on a server, so a page was drawn from the theme store's own
259
+ start — `{{ theme.resolved }}` was `light` under a `<html class="dark">`, and whatever named the scheme (a picture's
260
+ address, a `when` rule) was swapped for the other one as the page hydrated. The theme a host supplies (the cookie) is
261
+ now the server's snapshot of the store, and the hydration's; the shared store is never written on the server.
262
+ - Updated dependencies [200da2f]
263
+ - @plitzi/plitzi-sdk@0.38.10
264
+ - @plitzi/sdk-shared@0.38.10
265
+
3
266
  ## 0.38.9
4
267
 
5
268
  ### Patch Changes
package/README.md CHANGED
@@ -610,7 +610,7 @@ Responses include an `X-Cache: HIT` or `X-Cache: MISS` header for observability.
610
610
 
611
611
  ### Cache manager
612
612
 
613
- `server.cache` exposes programmatic cache control, useful when content changes and you need to invalidate entries without restarting the server.
613
+ `server.cache` exposes programmatic cache control, useful when content changes and you need to invalidate entries without restarting the server. It reaches every cache a render is kept in — the pages, the RSC answers and the schema data they are rendered from — so a page invalidated is rendered again from the space as it is now, never from the copy it was cached with.
614
614
 
615
615
  ```ts
616
616
  const server = createServer({ cacheTtlMs: 300_000, adapters });
@@ -631,7 +631,7 @@ server.cache?.clear();
631
631
  console.log(server.cache?.size);
632
632
  ```
633
633
 
634
- `server.cache` is `null` when caching is disabled (`cacheTtlMs: 0`).
634
+ `server.cache` is `null` when nothing is cached (`cacheTtlMs: 0` and `rsc.cacheTtlMs: 0`). `size` counts the entries of all three caches.
635
635
 
636
636
  #### `CacheFilter`
637
637
 
@@ -641,7 +641,7 @@ console.log(server.cache?.size);
641
641
  | `environment` | `string` | Match entries for this environment. |
642
642
  | `hostname` | `string` | Match entries for this hostname. |
643
643
 
644
- All fields are optional and combined with AND logic. Calling `invalidate()` with no filter (or an empty object) clears the entire cache.
644
+ All fields are optional and combined with AND logic. Calling `invalidate()` with no filter (or an empty object) clears the entire cache. The schema data belongs to no one host, so a filter on `hostname` drops it too — the host's pages are rendered from it — and it is read again on the next miss.
645
645
 
646
646
  ## Plugins
647
647
 
package/dist/auth.js CHANGED
@@ -1,8 +1,8 @@
1
+ import { createCarriers, presentedOrigin } from "./core/auth/credentials.js";
1
2
  import { appendCookies, clearFlowCookie, clearSessionCookies, createSessionCookies, isLocalHost, parseSessionHint, readFlowCookie, readRefreshToken, readSessionHint, readSessionToken, sessionCookieParams, sessionHintValue, sessionReturnTarget, writeFlowCookie, writeSessionCookies } from "./core/auth/session.js";
2
3
  import { createCsrf, csrfFailureMessage } from "./core/auth/csrf.js";
3
4
  import { applySessionOutcome, authPolicyRules, authRoutes } from "./core/auth/routes.js";
4
5
  import { isDocumentNavigation, renewForNavigation } from "./core/auth/renewal.js";
5
- import { createCarriers, presentedOrigin } from "./core/auth/credentials.js";
6
6
  import { ANY_DOMAIN, corsOrigins, domainAllowed, frameAncestors, hostnameOf, normalizeDomain } from "./core/auth/domains.js";
7
7
  import { checkPermission, checkSpaceAccess, createAuthorizer, requirementFor } from "./core/auth/authorize.js";
8
8
  import { generateToken, hashPassword, verifyPassword } from "./core/auth/passwords.js";
@@ -1,7 +1,7 @@
1
+ import { createCarriers, presentedOrigin } from "./credentials.js";
1
2
  import { createSessionCookies, sessionCookieParams } from "./session.js";
2
3
  import { createCsrf } from "./csrf.js";
3
4
  import { applySessionOutcome, authPolicyRules, authRoutes } from "./routes.js";
4
- import { createCarriers, presentedOrigin } from "./credentials.js";
5
5
  import { createAuthorizer } from "./authorize.js";
6
6
  import { createTokens } from "./tokens.js";
7
7
  import { createAuthApi } from "./api.js";
@@ -1,7 +1,7 @@
1
+ import { readCookies } from "../../auth/credentials.js";
1
2
  import { requestOrigin } from "../../requestParser.js";
2
3
  import { safeRedirectTarget } from "../navigation.js";
3
4
  import { appendCookies, writeSessionCookies } from "../../auth/session.js";
4
- import { readCookies } from "../../auth/credentials.js";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  //#region src/core/http/stages/signInRoute.ts
7
7
  /**
@@ -40,18 +40,19 @@ var createHttpServer = (config, makeHandlerForPort, parts) => {
40
40
  }) },
41
41
  listen(port, host = "0.0.0.0") {
42
42
  if (plan.requested !== void 0 && role !== "worker") serverLog.warn(label, `${plan.requested} workers asked for, ${plan.count} cores to run them on: starting ${plan.count}`);
43
- if (role === "primary") {
43
+ if (role === "primary") return new Promise((resolve) => {
44
44
  (parts.beforeFork?.() ?? Promise.resolve()).catch((error) => serverLog.error(label, "preparing for the workers failed", error)).then(async () => {
45
45
  const { startFleet } = await import("./fleet/supervisor.js");
46
46
  if (closed) return;
47
47
  fleet = startFleet(plan.count, label);
48
48
  serverLog.info(label, `${plan.count} workers on ${host}:${port}`);
49
+ await fleet.listening;
50
+ resolve();
49
51
  }).catch((error) => {
50
52
  serverLog.error(label, "the workers could not start", error);
51
53
  process.exitCode = 1;
52
54
  });
53
- return;
54
- }
55
+ });
55
56
  const handler = makeHandlerForPort(port);
56
57
  ({primary, h3} = buildTransport(config, handler, port, label));
57
58
  connections = watchConnections(primary);
@@ -68,9 +69,13 @@ var createHttpServer = (config, makeHandlerForPort, parts) => {
68
69
  process.exitCode = 1;
69
70
  process.exit(1);
70
71
  });
71
- primary.listen(port, host, () => {
72
- serverLog.info(label, `${protoLabel(version, !!config.tls)} - listening on ${host}:${port}`);
73
- parts.onListen?.();
72
+ const bound = primary;
73
+ return new Promise((resolve) => {
74
+ bound.listen(port, host, () => {
75
+ serverLog.info(label, `${protoLabel(version, !!config.tls)} - listening on ${host}:${port}`);
76
+ parts.onListen?.();
77
+ resolve();
78
+ });
74
79
  });
75
80
  },
76
81
  async close() {
@@ -1,5 +1,12 @@
1
1
  import { HostedStore } from './channel';
2
2
  import { FleetStoreName } from './link';
3
+ import { ActionKvAdapter } from '../../../modules/actions/types';
4
+ /**
5
+ * A `kv` the deployment makes — a file — opened by ONE process of a fleet and reached by the rest: made in the primary
6
+ * (or the single process there is), and in a worker the primary's, over the channel. Made in every worker instead,
7
+ * each would hold its own copy of the file and write it over the others'.
8
+ */
9
+ export declare const fleetKv: (make: () => ActionKvAdapter) => ActionKvAdapter;
3
10
  /**
4
11
  * What the primary keeps for its workers: the same in-memory defaults a single server makes for itself, made here
5
12
  * once — when a worker first asks — so the fleet has one of each rather than one per worker.
@@ -1,19 +1,33 @@
1
1
  import { JOB_QUEUE_METHODS, createMemoryJobQueue } from "../../../modules/actions/jobs/memoryQueue.js";
2
2
  import { hostable } from "./channel.js";
3
+ import { fleetStore } from "./link.js";
3
4
  import { KV_METHODS, createMemoryKv } from "../../../modules/actions/runtime/memoryKv.js";
4
5
  import { DRAFT_STORE_METHODS, createMemoryDraftStore } from "../../../modules/ssr/preview.js";
5
6
  import { RATE_LIMIT_METHODS, createMemoryRateLimit } from "../../auth/throttle.js";
6
7
  //#region src/core/server/fleet/stores.ts
8
+ /** The `kv` a deployment keeps somewhere of its own — a file, a database — when the primary is the one that opened it. */
9
+ var deploymentKv;
10
+ /**
11
+ * A `kv` the deployment makes — a file — opened by ONE process of a fleet and reached by the rest: made in the primary
12
+ * (or the single process there is), and in a worker the primary's, over the channel. Made in every worker instead,
13
+ * each would hold its own copy of the file and write it over the others'.
14
+ */
15
+ var fleetKv = (make) => {
16
+ const remote = fleetStore("actions.kv", KV_METHODS);
17
+ if (remote) return remote;
18
+ deploymentKv = make();
19
+ return deploymentKv;
20
+ };
7
21
  /**
8
22
  * What the primary keeps for its workers: the same in-memory defaults a single server makes for itself, made here
9
23
  * once — when a worker first asks — so the fleet has one of each rather than one per worker.
10
24
  */
11
25
  var FLEET_STORES = {
12
- "actions.kv": hostable(createMemoryKv, KV_METHODS),
26
+ "actions.kv": hostable(() => deploymentKv ?? createMemoryKv(), KV_METHODS),
13
27
  "realtime.grants": hostable(createMemoryKv, KV_METHODS),
14
28
  "actions.jobs": hostable(() => createMemoryJobQueue(), JOB_QUEUE_METHODS),
15
29
  "ssr.drafts": hostable(createMemoryDraftStore, DRAFT_STORE_METHODS),
16
30
  "auth.rateLimit": hostable(() => ({ check: createMemoryRateLimit() }), RATE_LIMIT_METHODS)
17
31
  };
18
32
  //#endregion
19
- export { FLEET_STORES };
33
+ export { FLEET_STORES, fleetKv };
@@ -1,4 +1,6 @@
1
1
  export type Fleet = {
2
+ /** Resolved once a worker takes connections: the port answers from then on. */
3
+ listening: Promise<void>;
2
4
  stop: () => Promise<void>;
3
5
  };
4
6
  /**
@@ -37,7 +37,10 @@ var startFleet = (count, label, exit = (code) => process.exit(code)) => {
37
37
  if (shared.count !== count) throw new Error(`[${label}] this process already runs ${shared.count} workers; every server in it must ask for the same number`);
38
38
  shared.servers += 1;
39
39
  const state = shared;
40
- return { stop: () => leave(state) };
40
+ return {
41
+ listening: state.listening,
42
+ stop: () => leave(state)
43
+ };
41
44
  }
42
45
  const workers = /* @__PURE__ */ new Set();
43
46
  const restarts = /* @__PURE__ */ new Set();
@@ -45,6 +48,10 @@ var startFleet = (count, label, exit = (code) => process.exit(code)) => {
45
48
  let deaths = [];
46
49
  let served = false;
47
50
  let stopping = false;
51
+ let listened = () => void 0;
52
+ const listening = new Promise((resolve) => {
53
+ listened = resolve;
54
+ });
48
55
  const stopAll = () => {
49
56
  restarts.forEach((timer) => clearTimeout(timer));
50
57
  restarts.clear();
@@ -62,6 +69,7 @@ var startFleet = (count, label, exit = (code) => process.exit(code)) => {
62
69
  workers.add(worker);
63
70
  worker.once("listening", () => {
64
71
  served = true;
72
+ listened();
65
73
  });
66
74
  worker.once("exit", (code, signal) => {
67
75
  workers.delete(worker);
@@ -89,13 +97,17 @@ var startFleet = (count, label, exit = (code) => process.exit(code)) => {
89
97
  const state = {
90
98
  count,
91
99
  servers: 1,
100
+ listening,
92
101
  stop: async () => {
93
102
  stopping = true;
94
103
  await stopAll();
95
104
  }
96
105
  };
97
106
  shared = state;
98
- return { stop: () => leave(state) };
107
+ return {
108
+ listening,
109
+ stop: () => leave(state)
110
+ };
99
111
  };
100
112
  /** The workers stop when the last server of this process that started them closes. */
101
113
  var leave = async (state) => {
@@ -37,7 +37,7 @@ var createPageServer = (config, services, extensions) => {
37
37
  reportReactBuild(config.devMode);
38
38
  if (config.preview?.enabled && !config.draftStore) config.draftStore = fleetStore("ssr.drafts", DRAFT_STORE_METHODS) ?? createMemoryDraftStore();
39
39
  const caches = createServerCaches(htmlTtlMs, config.rsc?.cacheTtlMs ?? DEFAULT_TTL_MS.rsc);
40
- const cache = caches.html ? buildCacheManager(caches.html) : null;
40
+ const cache = caches.html || caches.rsc ? buildCacheManager(caches) : null;
41
41
  const renderFn = config.templateFn ?? compileTemplate();
42
42
  const pluginManager = new PluginManager(normalizePlugins(config.plugins ?? {}), config.pluginsCacheDir, config.pluginsTtlMs, config.devMode);
43
43
  const plugins = {
@@ -94,6 +94,15 @@ export declare type FunctionContext = {
94
94
  grant: (topic: string, ttlSeconds?: number) => Promise<string>;
95
95
  /** Takes a grant back — or every grant for the topic, naming none. */
96
96
  revoke: (topic: string, grant?: string) => Promise<void>;
97
+ /**
98
+ * Starts one of the space's actions — one with a `later` trigger — in so many seconds, on the server, whether or not a
99
+ * page is still open: a turn that runs out, a bot's move, a hold that lapses. A `key` names it: set again under the
100
+ * same key, the one still waiting is replaced. Answers the job and when it is due (epoch ms, by the queue's clock).
101
+ * `ctx.later({ action: 'game.timeout', in: 45, input: { room }, key: 'turn:' + room })`.
102
+ */
103
+ later: (request: LaterRequest) => Promise<LaterAnswer>;
104
+ /** Drops what waits under `key` — the timer of a game that ended — and answers how many. */
105
+ cancelLater: (key: string) => Promise<number>;
97
106
  /**
98
107
  * One file of the space's data — a project's `src/data/<file>` — parsed, as of the version the run belongs to: what
99
108
  * a page must not carry, read here and answered only as far as it is shown. Read-only. `ctx.data('products.json')`.
@@ -293,6 +302,26 @@ declare type KvListRange = {
293
302
  order?: 'asc' | 'desc';
294
303
  };
295
304
 
305
+ /** The job it became, and when it is due — epoch ms, by the queue's clock. */
306
+ declare type LaterAnswer = {
307
+ id: string;
308
+ at: number;
309
+ };
310
+
311
+ /** What a run asks to start later: one of the space's actions, in so many seconds, with its input — and a name. */
312
+ declare type LaterRequest = {
313
+ /** The action, by its identifier. It says it may be started this way with a `later` trigger. */
314
+ action: string;
315
+ /** Seconds from now, by the queue's clock — whole or not, from 0 to a month. */
316
+ in: number;
317
+ input?: Record<string, unknown>;
318
+ /**
319
+ * What it is known by in this space: setting another under the same key replaces the one still waiting — a turn's
320
+ * timer moved on by a move — and `cancelLater(key)` drops it. The one already running is left be.
321
+ */
322
+ key?: string;
323
+ };
324
+
296
325
  /** Where a limit stands after counting one more: whether that one was within it, and how many are left. */
297
326
  declare type RateCount = {
298
327
  allowed: boolean;
@@ -1,3 +1,10 @@
1
- import { TtlCache } from './TtlCache';
1
+ import { ServerCaches } from './serverCaches';
2
2
  import { CacheManager } from '@plitzi/sdk-shared';
3
- export declare const buildCacheManager: <T>(store: TtlCache<T>) => CacheManager;
3
+ /**
4
+ * `server.cache` over every cache a render is kept in: the pages, the RSC answers, and the space as read for both.
5
+ *
6
+ * All three, because they hold one change between them. Dropping the pages alone left the space they were rendered
7
+ * from, and the next request rendered the page that had just been dropped from it — an invalidation that cleared
8
+ * every page and changed none of them.
9
+ */
10
+ export declare const buildCacheManager: ({ html, rsc, offlineData }: ServerCaches) => CacheManager;
@@ -1,26 +1,50 @@
1
- import { readHtmlCacheKey } from "./keys.js";
1
+ import { readHtmlCacheKey, readOfflineDataCacheKey, readRscCacheKey } from "./keys.js";
2
2
  //#region src/helpers/cache/cacheManager.ts
3
- var buildCacheManager = (store) => ({
4
- invalidate(filter) {
5
- if (!filter || Object.keys(filter).length === 0) {
6
- const count = store.size;
7
- store.clear();
8
- return count;
3
+ /**
4
+ * Whether an entry is one the filter names. An entry that is not any one host's (the space as read for a render) is
5
+ * named by a filter on a host too: the page that host serves is rendered from it, and dropping the page while keeping
6
+ * it would render the same page again.
7
+ */
8
+ var matches = (facts, filter) => (filter.spaceId === void 0 || facts.spaceId === String(filter.spaceId)) && (filter.environment === void 0 || facts.environment === filter.environment) && (filter.hostname === void 0 || facts.hostname === void 0 || facts.hostname === filter.hostname);
9
+ var clearCounted = (store) => {
10
+ const count = store.size;
11
+ store.clear();
12
+ return count;
13
+ };
14
+ /**
15
+ * `server.cache` over every cache a render is kept in: the pages, the RSC answers, and the space as read for both.
16
+ *
17
+ * All three, because they hold one change between them. Dropping the pages alone left the space they were rendered
18
+ * from, and the next request rendered the page that had just been dropped from it — an invalidation that cleared
19
+ * every page and changed none of them.
20
+ */
21
+ var buildCacheManager = ({ html, rsc, offlineData }) => {
22
+ const caches = [
23
+ ...html ? [{
24
+ store: html,
25
+ read: readHtmlCacheKey
26
+ }] : [],
27
+ ...rsc ? [{
28
+ store: rsc,
29
+ read: readRscCacheKey
30
+ }] : [],
31
+ ...offlineData ? [{
32
+ store: offlineData,
33
+ read: readOfflineDataCacheKey
34
+ }] : []
35
+ ];
36
+ return {
37
+ invalidate(filter) {
38
+ if (!filter || Object.keys(filter).length === 0) return caches.reduce((count, { store }) => count + clearCounted(store), 0);
39
+ return caches.reduce((count, { store, read }) => count + store.invalidateWhere((key) => matches(read(key), filter)), 0);
40
+ },
41
+ clear() {
42
+ caches.forEach(({ store }) => store.clear());
43
+ },
44
+ get size() {
45
+ return caches.reduce((count, { store }) => count + store.size, 0);
9
46
  }
10
- return store.invalidateWhere((key) => {
11
- const { spaceId: keySpaceId, environment: keyEnvironment, hostname: keyHostname } = readHtmlCacheKey(key);
12
- if (filter.spaceId !== void 0 && keySpaceId !== String(filter.spaceId)) return false;
13
- if (filter.environment !== void 0 && keyEnvironment !== filter.environment) return false;
14
- if (filter.hostname !== void 0 && keyHostname !== filter.hostname) return false;
15
- return true;
16
- });
17
- },
18
- clear() {
19
- store.clear();
20
- },
21
- get size() {
22
- return store.size;
23
- }
24
- });
47
+ };
48
+ };
25
49
  //#endregion
26
50
  export { buildCacheManager };