@plitzi/sdk-server 0.37.0 → 0.37.2
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 +155 -0
- package/README.md +26 -0
- package/dist/actions.d.ts +14 -6
- package/dist/actions.js +7 -3
- package/dist/core/server/baseServer.d.ts +4 -1
- package/dist/core/server/baseServer.js +2 -1
- package/dist/core/server/closeOnSignals.d.ts +28 -0
- package/dist/core/server/closeOnSignals.js +46 -0
- package/dist/core/server/pageServer.js +3 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/index.d.ts +12 -3
- package/dist/modules/actions/index.js +9 -3
- package/dist/modules/actions/jobs/index.d.ts +32 -0
- package/dist/modules/actions/jobs/index.js +57 -0
- package/dist/modules/actions/jobs/memoryQueue.d.ts +30 -0
- package/dist/modules/actions/jobs/memoryQueue.js +178 -0
- package/dist/modules/actions/jobs/memoryStores.test.d.ts +1 -0
- package/dist/modules/actions/jobs/scheduler.d.ts +62 -0
- package/dist/modules/actions/jobs/scheduler.js +125 -0
- package/dist/modules/actions/jobs/scheduler.test.d.ts +1 -0
- package/dist/modules/actions/jobs/schedules.d.ts +23 -0
- package/dist/modules/actions/jobs/schedules.js +47 -0
- package/dist/modules/actions/jobs/storeClock.d.ts +16 -0
- package/dist/modules/actions/jobs/storeClock.js +32 -0
- package/dist/modules/actions/jobs/testing/jobQueueContract.d.ts +25 -0
- package/dist/modules/actions/jobs/worker.d.ts +62 -0
- package/dist/modules/actions/jobs/worker.js +262 -0
- package/dist/modules/actions/jobs/worker.test.d.ts +1 -0
- package/dist/modules/actions/moduleFor.js +1 -0
- package/dist/modules/actions/runtime/precheck.js +1 -1
- package/dist/modules/actions/runtime/renderResolver.js +2 -2
- package/dist/modules/actions/transport/callHandler.js +1 -1
- package/dist/modules/actions/transport/webhookHandler.js +1 -1
- package/dist/modules/actions/types.d.ts +76 -1
- package/dist/modules/mongo/index.d.ts +4 -0
- package/dist/modules/mongo/jobQueue.d.ts +38 -0
- package/dist/modules/mongo/jobQueue.js +335 -0
- package/dist/modules/mongo/kv.d.ts +15 -0
- package/dist/modules/mongo/kv.js +98 -0
- package/dist/modules/mongo/stores.integration.test.d.ts +1 -0
- package/dist/modules/mysql/jobs/index.d.ts +5 -0
- package/dist/modules/mysql/jobs/jobQueue.d.ts +40 -0
- package/dist/modules/mysql/jobs/jobQueue.js +375 -0
- package/dist/modules/mysql/jobs/kv.d.ts +18 -0
- package/dist/modules/mysql/jobs/kv.js +78 -0
- package/dist/modules/mysql/jobs/schema.d.ts +24 -0
- package/dist/modules/mysql/jobs/schema.js +96 -0
- package/dist/modules/mysql/jobs/stores.integration.test.d.ts +1 -0
- package/dist/modules/ssr/Component.d.ts +2 -5
- package/dist/modules/ssr/Component.js +1 -2
- package/dist/modules/ssr/prepareRender.js +1 -4
- package/dist/mongo.d.ts +25 -0
- package/dist/mongo.js +3 -0
- package/dist/mysql.d.ts +11 -0
- package/dist/mysql.js +4 -1
- package/package.json +16 -6
- package/skills/plitzi-authoring/SKILL.md +73 -267
- package/skills/plitzi-authoring/reference/data-and-visibility.md +114 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +107 -0
- package/skills/plitzi-authoring/reference/flows.md +80 -0
- package/skills/plitzi-authoring/reference/layouts.md +140 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +50 -0
- package/skills/plitzi-authoring/reference/structure.md +83 -0
- package/skills/plitzi-authoring/reference/templates-and-export.md +54 -0
- package/skills/plitzi-authoring/reference/templates.md +69 -0
- package/skills/plitzi-authoring/reference/testing.md +52 -0
- package/dist/modules/actions/runtime/schedule.d.ts +0 -35
- package/dist/modules/actions/runtime/schedule.js +0 -76
- /package/dist/{modules/actions/runtime/schedule.test.d.ts → core/server/closeOnSignals.test.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,160 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.37.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ## Authoring and agents
|
|
8
|
+
- **A step's params are templates in full.** Only a bare name (`{{ post.slug }}`) used to be recognised, so a
|
|
9
|
+
condition or a loop in a `setState` or a webhook body was handed on as its own text — a flag set to
|
|
10
|
+
`{% if … %}1{% endif %}` stored the template, a non-empty string every later check read as true. Any `{{ }}` or
|
|
11
|
+
`{% %}` in a param is now evaluated; what it resolves to is data and is not evaluated again.
|
|
12
|
+
- **Twig tests work.** `x is defined`, `is empty`, `is null`, `is iterable`, `is even`, `is odd` (and `is not …`) used
|
|
13
|
+
to be read as comparisons with a variable of that name — `x is defined` answered true exactly when `x` was not
|
|
14
|
+
defined. Anything else on the right of `is` still compares.
|
|
15
|
+
- **A group takes an access chain.** `(rows|find('id', 3)).title` read the whole row; the key after the parenthesis
|
|
16
|
+
was never parsed.
|
|
17
|
+
- **An expression that chose to be empty renders empty** under `keepEmptyTokens`. A kept token is for a name still
|
|
18
|
+
waiting for a value; `{{ on ? 'active' : '' }}` said "nothing" on purpose, and handing back its text made the
|
|
19
|
+
empty branch a non-empty string.
|
|
20
|
+
- **Checkbox params written as text are booleans.** `dateConverter({ isUnix: 'false' })` read `'false'` as yes and
|
|
21
|
+
gave an ISO date back raw. Every utility's checkbox params are normalised before its callback runs.
|
|
22
|
+
- **`condition-starts-visible`** warns about a visibility computed from data on an element that starts on screen: it
|
|
23
|
+
is drawn until its provider answers and then hidden — an empty state or a "get started" card flashing past on
|
|
24
|
+
every load. `visible: false` makes it wait hidden. The export reads a visibility binding back in its place, so a
|
|
25
|
+
space whose condition is not its last binding round-trips unchanged.
|
|
26
|
+
- **A source named by its short id inside a binding's template is refused** with the full name, as it already was in
|
|
27
|
+
a flow: `{{ stats.total }}` resolved to nothing and the element showed its empty branch.
|
|
28
|
+
- **`activeOn(class, pageIds)`** marks a menu's current entry from `navigation.currentPageId` — one binding for a menu
|
|
29
|
+
kept in a layout. `variantFrom` now says `append: 'true'`, which is what it did.
|
|
30
|
+
- **Two elements with one id** name where the first one was written. **`template-never-resolved`** warns about a
|
|
31
|
+
condition left in an attribute, which only resolves `{{ name|filter }}` tokens.
|
|
32
|
+
- **The authoring skill is a folder**: `SKILL.md` plus references for layouts, data and visibility, templates,
|
|
33
|
+
flows, structure, testing and a review checklist. `plitzi create` copies all of it, and writes an `AGENTS.md`
|
|
34
|
+
(imported by `CLAUDE.md`) pointing any agent at it.
|
|
35
|
+
|
|
36
|
+
- **A new space starts on a page worth keeping.** The space `POST /spaces` and `plitzi create` both begin from is
|
|
37
|
+
redesigned: a top bar with the theme switch, a hero with two calls to action, and six guides — each a card linking to
|
|
38
|
+
its page of the docs (`https://plitzi.com/docs/…`). Every link used to be `#`, and one promised a course that does
|
|
39
|
+
not exist. The palette is a set of light/dark tokens in Geist, the guides are one list mapped to one card, and the
|
|
40
|
+
bands of the page share a `shell` class — the page is written the way the authoring skill asks a space to be.
|
|
41
|
+
- **The copy `plitzi create` writes passes its own project's lint.** An import of the package that would not fit
|
|
42
|
+
120 columns is wrapped one name per line, as the project's formatter would wrap it.
|
|
43
|
+
|
|
44
|
+
## Server actions and scheduled jobs
|
|
45
|
+
|
|
46
|
+
**Scheduled jobs across replicas, out of the box, on the database a deployment already runs.** The scheduler and the
|
|
47
|
+
workers were already the package's; where the jobs wait was left to each deployment to write — and a queue is the part
|
|
48
|
+
that is easiest to get subtly wrong. Two helpers now build the adapters over a store the deployment owns, without the
|
|
49
|
+
package opening a connection or keeping data of its own:
|
|
50
|
+
|
|
51
|
+
- `@plitzi/sdk-server/mongo` — `createMongoJobQueue({ db })` and `createMongoKv({ db })`, over a `Db` (or a getter for a
|
|
52
|
+
client that reconnects). Creates the indexes its queries need; `mongodb` is an optional peer, used for its types.
|
|
53
|
+
- `@plitzi/sdk-server/mysql` — `createMysqlJobQueue({ pool })` and `createMysqlKv({ pool })`, over a `mysql2` pool.
|
|
54
|
+
Each job is claimed by a single-row compare-and-set, so replicas claiming at once never deadlock. Creates its three
|
|
55
|
+
tables on first use, or hands them to your own migrations through `mysqlJobSchemaStatements()`.
|
|
56
|
+
|
|
57
|
+
Every instant is the database server's, never a replica's. The in-process queue and both helpers pass one contract test
|
|
58
|
+
suite: exactly-once enqueue, atomic claims, leases that lapse and are reaped, heartbeats, settlements ignored from a
|
|
59
|
+
worker that lost its claim, refunded hand-backs, schedules advanced by compare-and-set, operator retry and cancel.
|
|
60
|
+
|
|
61
|
+
**`closeOnSignals(server, { afterClose })`** closes the server on SIGTERM/SIGINT and exits only once it has — so a
|
|
62
|
+
deploy finishes the jobs a replica is running and leaves the waiting ones for the next. A second signal exits at once.
|
|
63
|
+
Opt-in: a host with its own signal handling calls `server.close()` itself. Projects from `plitzi create` use it.
|
|
64
|
+
|
|
65
|
+
**A replica that is told to stop finishes the jobs it is running — and only those.** `server.close()` (and the job
|
|
66
|
+
worker's `stop()`) now waits for every running job to end however long past its lease that is, and keeps renewing
|
|
67
|
+
their claims while it waits. It used to stop renewing the moment the drain began and give up at the lease: a job longer
|
|
68
|
+
than its lease was taken over by another replica and run a second time while the first was still finishing it, and the
|
|
69
|
+
process exited in the middle of it. What is still waiting is not touched — it stays in the shared queue for the replica
|
|
70
|
+
that is staying or the one the deploy starts — and a job claimed in the instant the stop arrived is handed back with its
|
|
71
|
+
attempt refunded, never started. A run is bounded by its own timeout, which is the number an orchestrator's grace period
|
|
72
|
+
has to cover.
|
|
73
|
+
|
|
74
|
+
- **An every-minute schedule fired every other minute.** After producing a fire, the scheduler asked for the next
|
|
75
|
+
one from `lateness + 1 minute`, and `cronNextFire` rounds up to a whole minute — so a sweep that ran even a second
|
|
76
|
+
late (with the default 15-second sweep, nearly all of them) skipped the minute it should have produced. It now asks
|
|
77
|
+
from the minute after the one the sweep ran in. Only expressions with consecutive-minute fires were affected;
|
|
78
|
+
hourly and daily schedules were not.
|
|
79
|
+
- **A job that gives up says which step failed and why.** Its `error` and the failed attempt in its history used to
|
|
80
|
+
read "the flow ended failed" for every failure there is; they now read `step "<id>" failed: <message>`, taken from
|
|
81
|
+
the run's outline — already redacted of every credential the run resolved, so nothing reaches the job that the
|
|
82
|
+
run history would not show.
|
|
83
|
+
- **`apiContainer` takes `refreshSeconds`**: it asks again on its own every N seconds, for a page showing something
|
|
84
|
+
still moving — a queue, a feed, a status board. It is the same refresh `performQuery` runs, so it works for both
|
|
85
|
+
runtimes (a browser request is sent again; a server provider asks for its own RSC slice again). It pauses while
|
|
86
|
+
the tab is hidden and never starts a refresh while the last one is in flight. `0`, the default, never does. Before
|
|
87
|
+
this a live server provider needed a plugin element of its own to call `useRscRefresh` on a timer: `onApiSuccess`
|
|
88
|
+
never fires for a server provider, so there was no way to author the loop. The builder shows it as "Refresh every
|
|
89
|
+
(s)" for either runtime.
|
|
90
|
+
- **`onApiSuccess` / `onApiError` fire for a server provider.** The declaration offered them for every
|
|
91
|
+
`apiContainer`, but they were decided from the browser request alone — which a `runtime: 'server'` provider never
|
|
92
|
+
makes — so a flow wired to them never ran. They now fire when the provider's slice arrives (or the payload arrives
|
|
93
|
+
without it), and again on each refresh, the same as a browser refetch. A payload resolved for another page fires
|
|
94
|
+
neither. **A space that wired a flow to one of them will now see it run.**
|
|
95
|
+
- **The theme toggle shows one icon by default.** It renders both — which one is right depends on stored state, and
|
|
96
|
+
markup that depended on it would differ between the server and the browser — and until now every space had to copy
|
|
97
|
+
ten lines of `customCss` to hide the other; one that did not showed a sun and a moon side by side. The SDK's base
|
|
98
|
+
layer now shows the icon of the scheme in use, and a space's own rule still wins.
|
|
99
|
+
- **`variantFrom(cls, source)`** in `@plitzi/sdk-authoring` binds which of a CLASS's variants an element wears to a
|
|
100
|
+
value in the data — a status pill that is amber while a job waits and green once it is done. Written by hand the
|
|
101
|
+
variant key is the trap: it names the selector the variants belong to, and the element's type (`text.base`) is a
|
|
102
|
+
different selector from its class (`statusPill.base`), so the element rendered with no variant and nothing
|
|
103
|
+
reported it. The helper takes the key from the class declaration.
|
|
104
|
+
- **`authorSpace` refuses a flow template that reads a source by its short name.** A binding completes the prefix
|
|
105
|
+
(`jobRows.item.id` → `list_jobRows.item.id`), so the short form is the one an author learns first; a step's params
|
|
106
|
+
are read as written, and there it resolved to nothing — a row's button posted an empty id and every layer below
|
|
107
|
+
reported success. The error names the full source. A root that is a step of the same flow is left alone.
|
|
108
|
+
- New example, [`05-with-server-actions/05-schedules`](../examples/05-with-server-actions/05-schedules): scheduled and
|
|
109
|
+
delayed jobs on a self-hosted server, with the `ActionJobQueue` and `kv` seams written out in full over one SQLite
|
|
110
|
+
file, two replicas sharing it, failover when one is killed, and a board that watches it happen.
|
|
111
|
+
|
|
112
|
+
## Rendering, data and styles
|
|
113
|
+
- **A server element inside a layout is resolved.** `collectServerElements` walked the page alone, so a
|
|
114
|
+
`runtime: 'server'` provider in a layout — a dashboard's sidebar, a section's header — was never resolved: the RSC
|
|
115
|
+
payload came back empty and the element rendered with nothing. It now walks the page and every shell in its layout
|
|
116
|
+
chain.
|
|
117
|
+
- **The twig `date` filter reads an epoch.** `new Date("1790143200000")` is Invalid Date, so an epoch in milliseconds
|
|
118
|
+
handed over as text — how an attribute passes one — formatted as nothing. A number, or a string of digits, is now
|
|
119
|
+
read as milliseconds.
|
|
120
|
+
- **An action is called by its name, not by its trigger kind.** `actionName` took the first trigger's title, and a
|
|
121
|
+
trigger nobody named is titled with its kind — so every rendered action in a workspace read "render" and every clock
|
|
122
|
+
"schedule". A title that only repeats the kind is no longer a name, and `defineAction` titles each trigger with the
|
|
123
|
+
action's name.
|
|
124
|
+
- **`variant` on an element that wears a class is that class's variant** when the class declares it and the type does
|
|
125
|
+
not. Keyed by the type, `text({ class: avatar, variant: 'violet' })` named `text--violet`, a selector nothing wears,
|
|
126
|
+
and rendered with no variant at all. Exporting a document back to authoring reads the same key back into `variant`.
|
|
127
|
+
- **`variantFrom` takes `{ slot, template }`**, where `template` turns a value into a variant name for data that does
|
|
128
|
+
not already speak in them — `"{{ source == 'code' ? 'on' : '' }}"`. The third argument was the slot name alone.
|
|
129
|
+
- **Entry declarations no longer alternate with `export {}`.** In `@plitzi/sdk-server` and `@plitzi/sdk-mcp` a repeated
|
|
130
|
+
`build:dev` left some `dist/<entry>.d.ts` as a ten-byte `export {}`, and consumers saw "has no exported member". The
|
|
131
|
+
cause was `insertTypesEntry` writing a types entry over the real declaration of the same path; it is gone.
|
|
132
|
+
- **Signing in keeps what a guest was doing.** `runtimeStatePersist` reset `runtime.state` whenever its owner changed,
|
|
133
|
+
and a guest becoming a user is a change — so a sign-in screen that remembered where to send somebody (`?redirect=`)
|
|
134
|
+
forgot it the moment the session arrived, and the sign-in ended on the fallback page instead. Only a change FROM an
|
|
135
|
+
account resets now: one account's state still never reaches the next, and a guest has no account to protect.
|
|
136
|
+
|
|
137
|
+
**A value with functions inside functions is written as it was given, and fast.** A CSS value was split into layers
|
|
138
|
+
at commas by a pattern that could only see one level of parentheses, so `var(--bg, light-dark(#fff, #111))` was cut at
|
|
139
|
+
its inner comma and half a function went on into the stylesheet. The half was then tokenised by an expression that
|
|
140
|
+
nested one quantifier inside another and backtracked exponentially on it — nearly two seconds for thirty characters,
|
|
141
|
+
on every generation of that selector's cache. Both are now one linear scan that splits only outside parentheses and
|
|
142
|
+
quotes. Generated caches keep the spacing the value was written with inside functions (`repeat(3, minmax(0, 1fr))`,
|
|
143
|
+
`color-mix(in oklab, oklch(…) 50%, transparent)`), where they used to drop it after the first comma.
|
|
144
|
+
|
|
145
|
+
- Updated dependencies
|
|
146
|
+
- @plitzi/plitzi-sdk@0.37.2
|
|
147
|
+
- @plitzi/sdk-shared@0.37.2
|
|
148
|
+
|
|
149
|
+
## 0.37.1
|
|
150
|
+
|
|
151
|
+
### Patch Changes
|
|
152
|
+
|
|
153
|
+
- v0.37.1
|
|
154
|
+
- Updated dependencies
|
|
155
|
+
- @plitzi/plitzi-sdk@0.37.1
|
|
156
|
+
- @plitzi/sdk-shared@0.37.1
|
|
157
|
+
|
|
3
158
|
## 0.37.0
|
|
4
159
|
|
|
5
160
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1134,6 +1134,32 @@ A stage receives the `SSRContext` — the request, the config, and the render si
|
|
|
1134
1134
|
**is** the decision to mount them: there is no config flag mirroring it, and a server that never passes them
|
|
1135
1135
|
never loads them.
|
|
1136
1136
|
|
|
1137
|
+
## Scheduled jobs across replicas
|
|
1138
|
+
|
|
1139
|
+
Server actions with a `schedule` trigger, and jobs queued for later, run with no configuration — over an in-process
|
|
1140
|
+
queue, which is right for one replica and wrong for two: each keeps its own schedules, and the jobs a replica holds
|
|
1141
|
+
leave with it. More than one replica needs a queue and a key/value store they share. They are adapters, so the jobs
|
|
1142
|
+
live in your database; for Mongo and MySQL they are already written, over the connection you already have:
|
|
1143
|
+
|
|
1144
|
+
```ts
|
|
1145
|
+
import { closeOnSignals, createServer } from '@plitzi/sdk-server';
|
|
1146
|
+
import { createMongoJobQueue, createMongoKv } from '@plitzi/sdk-server/mongo';
|
|
1147
|
+
|
|
1148
|
+
const db = mongoClient.db('app');
|
|
1149
|
+
const server = createServer({
|
|
1150
|
+
adapters,
|
|
1151
|
+
action: { lookups, kv: createMongoKv({ db }), jobs: { queue: createMongoJobQueue({ db }) } }
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
server.listen(3000);
|
|
1155
|
+
// On SIGTERM: finish the jobs this replica is running, leave the waiting ones for the next, then exit.
|
|
1156
|
+
closeOnSignals(server, { afterClose: () => mongoClient.close() });
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
`@plitzi/sdk-server/mysql` has the same pair over a `mysql2` pool (`createMysqlJobQueue`, `createMysqlKv`). Anything
|
|
1160
|
+
else is the `ActionJobQueue` contract written against your store — see
|
|
1161
|
+
[server actions § self-hosted](https://github.com/plitzi/plitzi-workspace/blob/main/docs/en/server-actions.md#13-for-a-self-hosted-deployment).
|
|
1162
|
+
|
|
1137
1163
|
## Running it locally
|
|
1138
1164
|
|
|
1139
1165
|
The package ships a small harness in [`dev/`](./dev) — file-backed adapters over a sample space, three demo
|
package/dist/actions.d.ts
CHANGED
|
@@ -12,18 +12,26 @@
|
|
|
12
12
|
* Builds a runner outside a page server.
|
|
13
13
|
*
|
|
14
14
|
* The page server builds its own from `action.lookups`; this is for the deployment that needs to run an action
|
|
15
|
-
* from somewhere else — a
|
|
16
|
-
*
|
|
15
|
+
* from somewhere else — a queue consumer, or the API role answering a builder's test run. Same runner, same
|
|
16
|
+
* checks: a trigger that skipped them would be a weaker path to the same work.
|
|
17
17
|
*/
|
|
18
18
|
export { createActionsModule } from './modules/actions';
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Scheduled runs: a durable queue, a leaderless producer and the workers that drain it.
|
|
21
|
+
*
|
|
22
|
+
* `createActionsModule` already assembles these from `jobs` — a deployment reaches for them directly only to run a
|
|
23
|
+
* sweep from its own cron, to build a worker in a process that serves no pages, or to hold the queue itself for an
|
|
24
|
+
* operator's panel. The in-memory queue is the default and is correct for exactly one replica.
|
|
25
|
+
*/
|
|
26
|
+
export { createActionJobs, createJobWorker, createMemoryJobQueue, createScheduler } from './modules/actions/jobs';
|
|
27
|
+
export { DEFAULT_MAX_ATTEMPTS, scheduleFor, scheduleJobId, schedulesFor } from './modules/actions/jobs';
|
|
20
28
|
/**
|
|
21
29
|
* Cron, straight from `sdk-shared`, where the parser lives because the validator needs it too.
|
|
22
30
|
*
|
|
23
31
|
* Re-exported HERE and nowhere in between: a deployment mounting its own scheduler reaches for one package, and
|
|
24
32
|
* the file that used to sit in `runtime/` existed only so a neighbouring import could look local.
|
|
25
33
|
*/
|
|
26
|
-
export { cronMatches, parseCron } from '@plitzi/sdk-shared/actions';
|
|
34
|
+
export { cronFiresBetween, cronMatches, cronNextFire, parseCron } from '@plitzi/sdk-shared/actions';
|
|
27
35
|
/**
|
|
28
36
|
* What only this deployment can answer about an action, before anybody runs it: a task it does not register, a
|
|
29
37
|
* credential the space has not got, a key missing from the one it has, a connector that was deleted, an engine
|
|
@@ -56,8 +64,8 @@ export { createKvStore } from './modules/actions/runtime/kvStore';
|
|
|
56
64
|
*/
|
|
57
65
|
export { createRunLogger, createRejectLogger } from './modules/actions/runtime/runLogger';
|
|
58
66
|
export type { TaskRegistryOptions } from './modules/actions/tasks/registry';
|
|
59
|
-
export type { ActionCredential, ActionDbDriver, ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionKvAdapter, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './modules/actions/types';
|
|
67
|
+
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';
|
|
60
68
|
export type { ActionsModule } from './modules/actions';
|
|
69
|
+
export type { ActionJobs, JobWorker, JobWorkerOptions, MemoryJobQueueOptions, Scheduler, SchedulerOptions, SweepResult } from './modules/actions/jobs';
|
|
61
70
|
export type { ActiveRun, RunGuards } from './modules/actions/runtime/guards';
|
|
62
|
-
export type { ScheduleResult, ScheduleRunner, ScheduleTick } from './modules/actions/runtime/schedule';
|
|
63
71
|
export type { ActionTaskDescriptor } from './modules/actions/taskCatalog';
|
package/dist/actions.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { createMemoryJobQueue } from "./modules/actions/jobs/memoryQueue.js";
|
|
2
|
+
import { DEFAULT_MAX_ATTEMPTS, scheduleFor, scheduleJobId, schedulesFor } from "./modules/actions/jobs/schedules.js";
|
|
3
|
+
import { createScheduler } from "./modules/actions/jobs/scheduler.js";
|
|
1
4
|
import { ActionRunError } from "./modules/actions/runtime/errors.js";
|
|
5
|
+
import { createJobWorker } from "./modules/actions/jobs/worker.js";
|
|
6
|
+
import { createActionJobs } from "./modules/actions/jobs/index.js";
|
|
2
7
|
import { createKvStore } from "./modules/actions/runtime/kvStore.js";
|
|
3
8
|
import { DEFAULT_LIMITS } from "./modules/actions/runtime/limits.js";
|
|
4
9
|
import { createMemoryKv } from "./modules/actions/runtime/memoryKv.js";
|
|
5
10
|
import { createTaskRegistry, taskName } from "./modules/actions/tasks/registry.js";
|
|
6
11
|
import { checkAction } from "./modules/actions/runtime/check.js";
|
|
7
|
-
import { createScheduleRunner } from "./modules/actions/runtime/schedule.js";
|
|
8
12
|
import { createRejectLogger, createRunLogger } from "./modules/actions/runtime/runLogger.js";
|
|
9
13
|
import { describeCatalog, describeTask } from "./modules/actions/taskCatalog.js";
|
|
10
14
|
import { createActionsModule } from "./modules/actions/index.js";
|
|
11
|
-
import { cronMatches, parseCron } from "@plitzi/sdk-shared/actions";
|
|
12
|
-
export { ActionRunError, DEFAULT_LIMITS, checkAction, createActionsModule, createKvStore, createMemoryKv, createRejectLogger, createRunLogger,
|
|
15
|
+
import { cronFiresBetween, cronMatches, cronNextFire, parseCron } from "@plitzi/sdk-shared/actions";
|
|
16
|
+
export { ActionRunError, DEFAULT_LIMITS, DEFAULT_MAX_ATTEMPTS, checkAction, createActionJobs, createActionsModule, createJobWorker, createKvStore, createMemoryJobQueue, createMemoryKv, createRejectLogger, createRunLogger, createScheduler, createTaskRegistry, cronFiresBetween, cronMatches, cronNextFire, describeCatalog, describeTask, parseCron, scheduleFor, scheduleJobId, schedulesFor, taskName };
|
|
@@ -4,6 +4,9 @@ export interface HttpServerParts {
|
|
|
4
4
|
label: string;
|
|
5
5
|
cache: CacheManager | null;
|
|
6
6
|
plugins: PluginRegistry;
|
|
7
|
-
|
|
7
|
+
/** Started once the transport is bound: background work that should not run in a server nobody listened on. */
|
|
8
|
+
onListen?: () => void;
|
|
9
|
+
/** Awaited, so a server that holds work in flight — jobs, drains — can finish it before the sockets close. */
|
|
10
|
+
onDestroy?: () => void | Promise<void>;
|
|
8
11
|
}
|
|
9
12
|
export declare const createHttpServer: (config: SSRServerConfig, makeHandlerForPort: (port: number) => Handler, parts: HttpServerParts) => SSRServer;
|
|
@@ -41,10 +41,11 @@ var createHttpServer = (config, makeHandlerForPort, parts) => {
|
|
|
41
41
|
});
|
|
42
42
|
primary.listen(port, host, () => {
|
|
43
43
|
console.log(`[${label}] ${protoLabel(version, !!config.tls)} - listening on ${host}:${port}`);
|
|
44
|
+
parts.onListen?.();
|
|
44
45
|
});
|
|
45
46
|
},
|
|
46
47
|
async close() {
|
|
47
|
-
parts.onDestroy?.();
|
|
48
|
+
await parts.onDestroy?.();
|
|
48
49
|
const open = [primary, h3].filter((srv) => srv !== void 0);
|
|
49
50
|
primary = void 0;
|
|
50
51
|
h3 = void 0;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Anything that closes the way a server from `createServer` does. */
|
|
2
|
+
export type Closable = {
|
|
3
|
+
close: () => Promise<void>;
|
|
4
|
+
};
|
|
5
|
+
export type CloseOnSignalsOptions = {
|
|
6
|
+
/** Default `SIGTERM` (what an orchestrator sends before it replaces a replica) and `SIGINT` (^C). */
|
|
7
|
+
signals?: NodeJS.Signals[];
|
|
8
|
+
/** Runs once the server has closed: what the deployment opened itself — a pool, a client, a file — goes here. */
|
|
9
|
+
afterClose?: () => void | Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Closes the server when the process is told to stop, and only then lets the process exit.
|
|
13
|
+
*
|
|
14
|
+
* `close()` already does the waiting — the page server stops claiming jobs, finishes the ones it is running and lets
|
|
15
|
+
* go of its sockets — but nothing calls it when a deploy sends SIGTERM, and a process that simply exits leaves every
|
|
16
|
+
* running job to be retried by somebody else. This is that call, for a deployment that has no signal handling of its
|
|
17
|
+
* own. Opt in: a host that already manages its process's signals keeps doing so, and calls `close()` from there.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const server = createServer(config);
|
|
21
|
+
* server.listen(3000);
|
|
22
|
+
* closeOnSignals(server, { afterClose: () => pool.end() });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* A second signal while it is closing exits at once — the ^C a person presses when waiting is not what they want.
|
|
26
|
+
* Answers a function that removes the handlers again.
|
|
27
|
+
*/
|
|
28
|
+
export declare const closeOnSignals: (server: Closable, { signals, afterClose }?: CloseOnSignalsOptions) => (() => void);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/core/server/closeOnSignals.ts
|
|
2
|
+
/**
|
|
3
|
+
* Closes the server when the process is told to stop, and only then lets the process exit.
|
|
4
|
+
*
|
|
5
|
+
* `close()` already does the waiting — the page server stops claiming jobs, finishes the ones it is running and lets
|
|
6
|
+
* go of its sockets — but nothing calls it when a deploy sends SIGTERM, and a process that simply exits leaves every
|
|
7
|
+
* running job to be retried by somebody else. This is that call, for a deployment that has no signal handling of its
|
|
8
|
+
* own. Opt in: a host that already manages its process's signals keeps doing so, and calls `close()` from there.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const server = createServer(config);
|
|
12
|
+
* server.listen(3000);
|
|
13
|
+
* closeOnSignals(server, { afterClose: () => pool.end() });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* A second signal while it is closing exits at once — the ^C a person presses when waiting is not what they want.
|
|
17
|
+
* Answers a function that removes the handlers again.
|
|
18
|
+
*/
|
|
19
|
+
var closeOnSignals = (server, { signals = ["SIGTERM", "SIGINT"], afterClose } = {}) => {
|
|
20
|
+
let closing = false;
|
|
21
|
+
const onSignal = (signal) => {
|
|
22
|
+
if (closing) {
|
|
23
|
+
console.warn(`[server] ${signal} again: exiting without waiting`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
closing = true;
|
|
28
|
+
console.info(`[server] ${signal}: finishing what is running, then exiting`);
|
|
29
|
+
(async () => {
|
|
30
|
+
try {
|
|
31
|
+
await server.close();
|
|
32
|
+
await afterClose?.();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("[server] could not close cleanly:", error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
};
|
|
40
|
+
for (const signal of signals) process.on(signal, onSignal);
|
|
41
|
+
return () => {
|
|
42
|
+
for (const signal of signals) process.off(signal, onSignal);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { closeOnSignals };
|
|
@@ -52,7 +52,9 @@ var createPageServer = (config, services, extensions) => {
|
|
|
52
52
|
label: "SSR",
|
|
53
53
|
cache,
|
|
54
54
|
plugins,
|
|
55
|
-
|
|
55
|
+
onListen: () => actions?.jobs?.start(),
|
|
56
|
+
onDestroy: async () => {
|
|
57
|
+
await actions?.jobs?.stop();
|
|
56
58
|
destroyServerCaches(caches);
|
|
57
59
|
pluginManager.destroy();
|
|
58
60
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createServer, resolveServices } from './core/createServer';
|
|
2
|
+
export { closeOnSignals } from './core/server/closeOnSignals';
|
|
2
3
|
export { registerHealthCheck, buildHealthPayload } from './core/health';
|
|
3
4
|
export { consoleLogger, renderLogEvent } from './helpers/serverLog';
|
|
4
5
|
export type { HealthCheckApp, HealthIdentity } from './core/health';
|
|
@@ -19,4 +20,5 @@ export type { RscElementResolver, RscResolveContext, ResolveRscDataOptions } fro
|
|
|
19
20
|
export type { ConnectorAuth, ConnectorCredential, ConnectorEndpoints, ConnectorFilter, ConnectorLookups, ConnectorManifest, ConnectorManifestDraft, ConnectorPageInfo, ConnectorPagination, ConnectorQuery, ConnectorReadEndpoint, ConnectorRecord, ConnectorResponseMapping, ConnectorResult, ProviderPagination } from './modules/connectors';
|
|
20
21
|
export type { FontStore, StoredFont } from './core/fontStore';
|
|
21
22
|
export type { ResolvedServices } from './core/createServer';
|
|
23
|
+
export type { Closable, CloseOnSignalsOptions } from './core/server/closeOnSignals';
|
|
22
24
|
export type { BaseContext, PipelineExtensions, SSRContext, Stage } from './core/http/types';
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createMemoryDraftStore, takeDraftOverride } from "./modules/ssr/preview
|
|
|
6
6
|
import { resolveServices } from "./core/services/resolve.js";
|
|
7
7
|
import { buildHealthPayload, registerHealthCheck } from "./core/health.js";
|
|
8
8
|
import { createServer } from "./core/createServer.js";
|
|
9
|
+
import { closeOnSignals } from "./core/server/closeOnSignals.js";
|
|
9
10
|
import { consoleLogger, renderLogEvent } from "./helpers/serverLog.js";
|
|
10
11
|
import { createCloudAdapters } from "./adapters/cloudAdapters.js";
|
|
11
12
|
import { createJsonAdapters } from "./adapters/jsonAdapters.js";
|
|
@@ -14,4 +15,4 @@ import { createMemoryCache, refuse } from "./adapters/space/types.js";
|
|
|
14
15
|
import { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildcardSubdomain } from "./adapters/space/resolvers.js";
|
|
15
16
|
import { createSpaceAdapters } from "./adapters/space/index.js";
|
|
16
17
|
import { createLocalFontStore, hostedPathsOf } from "./core/fontStore.js";
|
|
17
|
-
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, hostedPathsOf, presentsCredential, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
18
|
+
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, closeOnSignals, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, hostedPathsOf, presentsCredential, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ActionJobs } from './jobs';
|
|
1
2
|
import { RunGuards } from './runtime/guards';
|
|
2
3
|
import { ActionRunner } from './runtime/runAction';
|
|
3
4
|
import { ActionKvStore, ActionsConfig, ActionTaskRegistry, ResolvedActionLimits } from './types';
|
|
@@ -10,6 +11,14 @@ export type ActionsModule = ActionRunner & {
|
|
|
10
11
|
kv: (spaceId: number) => ActionKvStore;
|
|
11
12
|
/** What this server will allow one run of that document to spend — the deployment's ceilings, tightened by it. */
|
|
12
13
|
limitsFor: (document: ActionDocument) => ResolvedActionLimits;
|
|
14
|
+
/**
|
|
15
|
+
* Scheduled runs: the queue, the sweep that fills it and the workers that drain it.
|
|
16
|
+
*
|
|
17
|
+
* Built but NOT started — whoever owns the process decides when background work begins, and a server nobody
|
|
18
|
+
* listened on must not be quietly claiming jobs. Absent when the deployment cannot list a space's actions, since
|
|
19
|
+
* there is then no way to know what is scheduled.
|
|
20
|
+
*/
|
|
21
|
+
jobs?: ActionJobs;
|
|
13
22
|
};
|
|
14
23
|
/**
|
|
15
24
|
* The module's whole public surface: build it once from the deployment's config, then run actions through it.
|
|
@@ -24,7 +33,7 @@ export { precheckRun } from './runtime/precheck';
|
|
|
24
33
|
export { checkAction } from './runtime/check';
|
|
25
34
|
export { DEFAULT_LIMITS, resolveLimits } from './runtime/limits';
|
|
26
35
|
export { createRunGuards, deriveRunKey } from './runtime/guards';
|
|
27
|
-
export {
|
|
36
|
+
export { createActionJobs, createJobWorker, createMemoryJobQueue, createScheduler } from './jobs';
|
|
28
37
|
export { createMemoryKv } from './runtime/memoryKv';
|
|
29
38
|
export { createRunLogger, createRejectLogger } from './runtime/runLogger';
|
|
30
39
|
export { createTaskRegistry, taskName } from './tasks/registry';
|
|
@@ -34,8 +43,8 @@ export { handleActionCatalog } from './transport/catalogHandler';
|
|
|
34
43
|
export { handleActionWebhook } from './transport/webhookHandler';
|
|
35
44
|
export { verifySignature } from './transport/verifySignature';
|
|
36
45
|
export { handleActionCancel } from './transport/cancelHandler';
|
|
46
|
+
export type { ActionJobs, JobWorker, JobWorkerOptions, Scheduler, SchedulerOptions, SweepResult } from './jobs';
|
|
37
47
|
export type { ActiveRun, RunGuards } from './runtime/guards';
|
|
38
|
-
export type { ScheduleResult, ScheduleRunner, ScheduleTick } from './runtime/schedule';
|
|
39
48
|
export type { ActionTaskDescriptor } from './taskCatalog';
|
|
40
49
|
export type { TaskRegistryOptions } from './tasks/registry';
|
|
41
|
-
export type { ActionCredential, ActionDbDriver, ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './types';
|
|
50
|
+
export type { ActionCredential, ActionDbDriver, ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionJobsConfig, ActionKvStore, ActionLookups, ActionRejectRecord, ActionRunRecord, ActionRunRequest, ActionRunResult, ActionsConfig, ActionTask, ActionTaskContext, ActionTaskRegistry, RegisteredTask, ResolvedConnector } from './types';
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import "./jobs/memoryQueue.js";
|
|
2
|
+
import "./jobs/scheduler.js";
|
|
1
3
|
import "./runtime/errors.js";
|
|
4
|
+
import "./jobs/worker.js";
|
|
5
|
+
import { createActionJobs } from "./jobs/index.js";
|
|
2
6
|
import { createRunGuards } from "./runtime/guards.js";
|
|
3
7
|
import { createKvStore } from "./runtime/kvStore.js";
|
|
4
8
|
import { resolveLimits } from "./runtime/limits.js";
|
|
@@ -8,7 +12,6 @@ import "./runtime/precheck.js";
|
|
|
8
12
|
import { createActionRunner } from "./runtime/runAction.js";
|
|
9
13
|
import { createTaskRegistry } from "./tasks/registry.js";
|
|
10
14
|
import "./runtime/check.js";
|
|
11
|
-
import "./runtime/schedule.js";
|
|
12
15
|
import "./runtime/runLogger.js";
|
|
13
16
|
import "./taskCatalog.js";
|
|
14
17
|
import { handleActionCall } from "./transport/callHandler.js";
|
|
@@ -34,13 +37,16 @@ var createActionsModule = (config) => {
|
|
|
34
37
|
*/
|
|
35
38
|
const guards = createRunGuards(config.concurrency, config.kv, config.idempotency);
|
|
36
39
|
const kv = createKvStore(config.kv ?? createMemoryKv());
|
|
37
|
-
|
|
40
|
+
const limitsFor = (document) => resolveLimits(config.limits, document.limits);
|
|
41
|
+
const module = {
|
|
38
42
|
runAction,
|
|
39
43
|
registry,
|
|
40
44
|
guards,
|
|
41
45
|
kv: (spaceId) => namespaceKv(kv, spaceId),
|
|
42
|
-
limitsFor
|
|
46
|
+
limitsFor
|
|
43
47
|
};
|
|
48
|
+
if (config.jobs !== false && config.lookups.listActions) module.jobs = createActionJobs(config.jobs ?? {}, module, config.lookups);
|
|
49
|
+
return module;
|
|
44
50
|
};
|
|
45
51
|
//#endregion
|
|
46
52
|
export { createActionsModule, handleActionCall, handleActionCancel, handleActionCatalog, handleActionWebhook };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Scheduler } from './scheduler';
|
|
2
|
+
import { JobWorker } from './worker';
|
|
3
|
+
import { ActionsModule } from '../index';
|
|
4
|
+
import { ActionJobsConfig, ActionLookups } from '../types';
|
|
5
|
+
import { ActionJobQueue } from '@plitzi/sdk-shared';
|
|
6
|
+
export type ActionJobs = {
|
|
7
|
+
queue: ActionJobQueue;
|
|
8
|
+
scheduler: Scheduler;
|
|
9
|
+
worker: JobWorker;
|
|
10
|
+
start: () => void;
|
|
11
|
+
stop: () => Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The scheduling half of actions: a producer, a consumer and the store they meet in.
|
|
15
|
+
*
|
|
16
|
+
* Both halves run in EVERY replica by default, and neither needs to know about the others. The producer is safe to
|
|
17
|
+
* duplicate because a fire's job id is the fire's own instant; the consumer is safe to duplicate because a claim is
|
|
18
|
+
* atomic. What a deployment tunes is how many jobs each replica carries (`workers`) and whether it produces at all
|
|
19
|
+
* — a pod dedicated to running jobs sets `produce: false`, a pod serving pages sets `workers: 0`.
|
|
20
|
+
*
|
|
21
|
+
* With no `queue` supplied this is one process's memory, which is right for a single replica and wrong the moment
|
|
22
|
+
* there are two. That is a sentence rather than a silent behaviour because the failure is invisible: everything
|
|
23
|
+
* works, and the nightly email goes out once per replica.
|
|
24
|
+
*/
|
|
25
|
+
export declare const createActionJobs: (config: ActionJobsConfig, module: ActionsModule, lookups: ActionLookups) => ActionJobs;
|
|
26
|
+
export { createMemoryJobQueue } from './memoryQueue';
|
|
27
|
+
export { createScheduler } from './scheduler';
|
|
28
|
+
export { createJobWorker } from './worker';
|
|
29
|
+
export { DEFAULT_MAX_ATTEMPTS, scheduleFor, scheduleJobId, schedulesFor } from './schedules';
|
|
30
|
+
export type { MemoryJobQueueOptions } from './memoryQueue';
|
|
31
|
+
export type { Scheduler, SchedulerOptions, SweepResult } from './scheduler';
|
|
32
|
+
export type { JobWorker, JobWorkerOptions } from './worker';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createMemoryJobQueue } from "./memoryQueue.js";
|
|
2
|
+
import { DEFAULT_MAX_ATTEMPTS, scheduleFor, scheduleJobId, schedulesFor } from "./schedules.js";
|
|
3
|
+
import { createScheduler } from "./scheduler.js";
|
|
4
|
+
import { createJobWorker } from "./worker.js";
|
|
5
|
+
//#region src/modules/actions/jobs/index.ts
|
|
6
|
+
/**
|
|
7
|
+
* The scheduling half of actions: a producer, a consumer and the store they meet in.
|
|
8
|
+
*
|
|
9
|
+
* Both halves run in EVERY replica by default, and neither needs to know about the others. The producer is safe to
|
|
10
|
+
* duplicate because a fire's job id is the fire's own instant; the consumer is safe to duplicate because a claim is
|
|
11
|
+
* atomic. What a deployment tunes is how many jobs each replica carries (`workers`) and whether it produces at all
|
|
12
|
+
* — a pod dedicated to running jobs sets `produce: false`, a pod serving pages sets `workers: 0`.
|
|
13
|
+
*
|
|
14
|
+
* With no `queue` supplied this is one process's memory, which is right for a single replica and wrong the moment
|
|
15
|
+
* there are two. That is a sentence rather than a silent behaviour because the failure is invisible: everything
|
|
16
|
+
* works, and the nightly email goes out once per replica.
|
|
17
|
+
*/
|
|
18
|
+
var createActionJobs = (config, module, lookups) => {
|
|
19
|
+
const queue = config.queue ?? createMemoryJobQueue();
|
|
20
|
+
const { produce = true, workers = 4 } = config;
|
|
21
|
+
const scheduler = createScheduler({
|
|
22
|
+
queue,
|
|
23
|
+
lookups,
|
|
24
|
+
...config.environment ? { environment: config.environment } : {},
|
|
25
|
+
...config.schedulePollMs ? { pollMs: config.schedulePollMs } : {},
|
|
26
|
+
...config.maxAttempts ? { maxAttempts: config.maxAttempts } : {},
|
|
27
|
+
...config.reconcileMs ? { reconcileMs: config.reconcileMs } : {},
|
|
28
|
+
...config.spaces ? { spaces: config.spaces } : {},
|
|
29
|
+
...config.onError ? { onError: config.onError } : {}
|
|
30
|
+
});
|
|
31
|
+
const worker = createJobWorker({
|
|
32
|
+
queue,
|
|
33
|
+
lookups,
|
|
34
|
+
module,
|
|
35
|
+
workers,
|
|
36
|
+
...config.pollMs ? { pollMs: config.pollMs } : {},
|
|
37
|
+
...config.leaseMs ? { leaseMs: config.leaseMs } : {},
|
|
38
|
+
...config.backoff ? { backoff: config.backoff } : {},
|
|
39
|
+
...config.workerId ? { workerId: config.workerId } : {},
|
|
40
|
+
...config.onError ? { onError: config.onError } : {}
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
queue,
|
|
44
|
+
scheduler,
|
|
45
|
+
worker,
|
|
46
|
+
start: () => {
|
|
47
|
+
if (produce) scheduler.start();
|
|
48
|
+
worker.start();
|
|
49
|
+
},
|
|
50
|
+
stop: async () => {
|
|
51
|
+
scheduler.stop();
|
|
52
|
+
await worker.stop();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { DEFAULT_MAX_ATTEMPTS, createActionJobs, createJobWorker, createMemoryJobQueue, createScheduler, scheduleFor, scheduleJobId, schedulesFor };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ActionJobQueue } from '@plitzi/sdk-shared';
|
|
2
|
+
export type MemoryJobQueueOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* How many finished jobs to keep before the oldest are dropped.
|
|
5
|
+
*
|
|
6
|
+
* A bound rather than a retention period, because this store has no eviction of its own and an unbounded history
|
|
7
|
+
* in a long-lived process is a leak with a nice name. A deployment that wants jobs it can still read next week
|
|
8
|
+
* supplies a queue backed by something that survives a restart.
|
|
9
|
+
*/
|
|
10
|
+
retainFinished?: number;
|
|
11
|
+
/**
|
|
12
|
+
* What this queue calls now, in epoch ms.
|
|
13
|
+
*
|
|
14
|
+
* The seam that makes a lease, a backoff and an overdue schedule testable without waiting for one — and the
|
|
15
|
+
* shape of the rule a shared store keeps too: everything here reads ONE clock, and it is the store's.
|
|
16
|
+
*/
|
|
17
|
+
clock?: () => number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The default queue: one process's Maps.
|
|
21
|
+
*
|
|
22
|
+
* Correct for a deployment that runs ONE replica, which is most self-hosted ones, and honest about the rest — a
|
|
23
|
+
* second replica running on this gets its own schedules and its own jobs, so a nightly digest goes out twice. The
|
|
24
|
+
* cure is any store the replicas share, behind the same {@link ActionJobQueue} seam.
|
|
25
|
+
*
|
|
26
|
+
* It keeps the contract exactly, rather than approximately: enqueue is idempotent by id, a claim reaps lapsed
|
|
27
|
+
* leases, and every instant is read here rather than accepted from a caller. A test that passes against this one
|
|
28
|
+
* is testing the rules a shared store has to keep too.
|
|
29
|
+
*/
|
|
30
|
+
export declare const createMemoryJobQueue: ({ retainFinished, clock }?: MemoryJobQueueOptions) => ActionJobQueue;
|