fyflow-plugin-sdk 0.3.0 → 0.5.0

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/README.md CHANGED
@@ -18,13 +18,17 @@ export default definePlugin({
18
18
  id: "acme.signer", // stable and unique across Plugins
19
19
  version: "1.0.0",
20
20
  base: import.meta.url, // what Worker `url`s resolve against
21
+ // Set on the Host's Profiles, once for every Node Type that takes them (see Profiles and secrets).
22
+ profile: {
23
+ schema: {
24
+ type: "object",
25
+ properties: { key: { type: "string", writeOnly: true } }, // a secret
26
+ },
27
+ startup: ["key"], // read once, when a Worker starts
28
+ },
21
29
  workers: {
22
30
  sign: {
23
31
  url: "./sign.worker.js",
24
- configSchema: {
25
- type: "object",
26
- properties: { key: { type: "string", writeOnly: true } }, // a Profile secret
27
- },
28
32
  settingsSchema: { type: "object", properties: { field: { type: "string" } } },
29
33
  },
30
34
  },
@@ -32,6 +36,7 @@ export default definePlugin({
32
36
  Sign: {
33
37
  worker: "sign",
34
38
  inline: true,
39
+ profile: ["key"], // the fields of `profile` its Workers get
35
40
  groups: ["acme.signing"],
36
41
  icon: "signature", // from the app's icon set, below
37
42
  description: "Signs a field with HMAC.",
@@ -50,11 +55,12 @@ export default definePlugin({
50
55
 
51
56
  | Field | What it is |
52
57
  | --- | --- |
53
- | `workers` | Worker modules by key. A Worker module's default export is a class: its constructor gets the Profile's config, and `run({ settings, input }, context)` does one Task (see [Workers](#workers)). Schemas are JSON Schema in fyflow's subset. A Worker's `outputSchema`, what `run` returns (accepted by every version of the SDK, as fyflow's own Worker entry), is the form a person fills in to mark a failed Task done, and the paths the editor suggests for conditions on its Node's Edges and gates before any Run. |
54
- | `nodeTypes` | Node Types by name, each naming one of this Plugin's `workers`, with fyflow's declaration fields (`inline`, `pool`, `config`, `settings` defaults, `retryPolicy`, `groups`), and, since 0.2.0, how the app shows it: an optional `icon` and a one-line `description`. |
58
+ | `profile` | The Plugin's Profile fields, since 0.5.0: one object `schema`, `defaults` for fields no Profile sets, the `startup` fields its Workers read when they start, and the per-Task fields only a Profile may set (`profileOnly`). See [Profiles and secrets](#profiles-and-secrets). |
59
+ | `workers` | Worker modules by key. A Worker module's default export is a class: its constructor gets the start-up fields its Node Type takes, and `run({ settings, input }, context)` does one Task (see [Workers](#workers)). A Worker declares no `configSchema` since 0.5.0: its config is the Plugin's `profile`. Schemas are JSON Schema in fyflow's subset. A Worker's `outputSchema`, what `run` returns (accepted by every version of the SDK, as fyflow's own Worker entry), is the form a person fills in to mark a failed Task done, and the paths the editor suggests for conditions on its Node's Edges and gates before any Run. |
60
+ | `nodeTypes` | Node Types by name, each naming one of this Plugin's `workers`, with fyflow's declaration fields (`inline`, `pool`, `settings` defaults, `retryPolicy`, `groups`; no `config` since 0.5.0), since 0.2.0 how the app shows it (an optional `icon` and a one-line `description`), and since 0.5.0 the `profile` fields it takes. |
55
61
  | `gateTypes` | Gate types by name: their routes, default route, settings schema, and the decision `fn`. |
56
62
  | `groupTypes` | Resource Group types by name: a `create(config, key)` factory for the scheduler's limit, and a `configSchema`. |
57
- | `groups` | Resource Groups by key, of fyflow's built-in types (`ConcurrentLimitGroup`, `RateLimitGroup`, `KeyedRateLimitGroup`) or this Plugin's own. A Node Type may use its own Plugin's groups, and the built-in Plugin's `disk` group (below). |
63
+ | `groups` | Resource Groups by key, of fyflow's built-in types (`ConcurrentLimitGroup`, `RateLimitGroup`, `KeyedRateLimitGroup`) or this Plugin's own. A Node Type may use its own Plugin's groups, and the built-in Plugin's `disk` and `cpu` groups (below). |
58
64
 
59
65
  Do not name a setting like a member of every JavaScript object (`constructor`, `toString`, `valueOf`, `hasOwnProperty`, `__proto__`). The Host hands Agents a Flow Definition's shape as a JSON Schema that includes your `settingsSchema`, and some JSON Schema validators misread such a property: they refuse, or throw, on a Flow that is valid.
60
66
 
@@ -74,10 +80,10 @@ A Plugin's own imports resolve from the Plugin directory's `node_modules`, so `n
74
80
  Declare the SDK versions your Plugin works with as a peer dependency:
75
81
 
76
82
  ```json
77
- { "peerDependencies": { "fyflow-plugin-sdk": "^0.2.0" } }
83
+ { "peerDependencies": { "fyflow-plugin-sdk": "^0.5.0" } }
78
84
  ```
79
85
 
80
- A Host implements its SDK version (`SDK_VERSION`, exported here) and the earlier ones it is compatible with (`SDK_COMPATIBLE`): a 0.3 Host still loads a Plugin declaring `^0.2.0` or `^0.1.0`. It skips, with the reason, a Plugin whose range excludes them all. Raise the range only for what a Host must do at run time: declare `^0.2.0` when you use `icon` or `description`. The Worker types of 0.3.0 vanish at run time (`import type`), so take 0.3 as a devDependency for them and leave the peer range as it was.
86
+ A Host implements its SDK version (`SDK_VERSION`, exported here) and the earlier ones it is compatible with (`SDK_COMPATIBLE`). It skips, with the reason, a Plugin whose range excludes them all. 0.5.0 changed what Profiles are, so it is compatible with no earlier version: a Plugin written for 0.4 or before is skipped until it is upgraded, as [UPGRADING.md](./UPGRADING.md) describes. From 0.5 on, raise the range only for what a Host must do at run time.
81
87
 
82
88
  ## Icons
83
89
 
@@ -85,7 +91,7 @@ A Node Type's `icon` names one of the app's icons, from [Lucide](https://lucide.
85
91
 
86
92
  ## Workers
87
93
 
88
- A Worker module's default export is a class. The Host builds each instance (one per worker of the pool: a thread, or on an inline pool an object on the Host's own thread) with the config of the Profile the pool is for, and reuses it across Tasks, so keep per-Task state out of fields. `run` gets the Task's envelope (the Node's `settings` and the Task input) and a context; what it returns is the Task's result, and throwing fails the Task with the error's message. Since 0.3.0 the SDK types all of it:
94
+ A Worker module's default export is a class. The Host builds each instance (one per worker of the pool: a thread, or on an inline pool an object on the Host's own thread) with the start-up fields its Node Type takes, from the Profile the pool is for, and reuses it across Tasks, so keep per-Task state out of fields. `run` gets the Task's envelope (the Node's `settings` and the Task input) and a context; what it returns is the Task's result, and throwing fails the Task with the error's message. Since 0.3.0 the SDK types all of it:
89
95
 
90
96
  ```ts
91
97
  import type { TaskEnvelope, Worker, WorkerContext } from "fyflow-plugin-sdk";
@@ -112,26 +118,77 @@ export default class Sign implements Worker<Settings> {
112
118
 
113
119
  `WorkerClass<Config, Settings, Input, Output>` types the class itself, constructor included. Check `context.signal` between steps, or pass it to anything that takes one (`fetch`, streams): a Worker that stops promptly frees its slot sooner, and what it returns after a Cancel is discarded. Use `import type`, so a Worker module needs nothing from the SDK at run time. The context offers what the Host shows (progress) and Cancel; fyflow's scheduler passes more, but the SDK promises only these.
114
120
 
115
- ## The `disk` group
121
+ ### A Worker's lifecycle
122
+
123
+ Since 0.5.0 the SDK also promises what happens to an instance between Tasks, for a Worker that owns something long-lived (a child process, a connection, a loaded model):
124
+
125
+ 1. **Built**: `new Worker(config, pool)`, where `config` is its start-up fields and `pool` a `PoolContext`.
126
+ 2. **`setup()`**, if it has one: once, awaited before its first Task. Start what it owns here, not in the constructor.
127
+ 3. **`run`**, for any number of Tasks.
128
+ 4. **`teardown()`**, if it has one: once, when the pool retires the instance. That is when it has been idle past its pool's `idleTimeout` (the pool checks every 5 seconds, so up to 5 seconds later), when it asked to be replaced, and when its Runtime shuts down: the Host stopping, or after a Reload once the last Run on the replaced Runtime ends.
129
+
130
+ ```ts
131
+ import type { PoolContext, Worker } from "fyflow-plugin-sdk";
132
+
133
+ export default class Thumbnails implements Worker {
134
+ readonly #pool: PoolContext | undefined;
135
+ #service: Service | undefined; // Service, start: your own
136
+
137
+ constructor(_config: object, pool?: PoolContext) {
138
+ this.#pool = pool;
139
+ }
140
+
141
+ async setup() {
142
+ // Port 0: another copy of this Worker may be running (see below).
143
+ this.#service = await start({ port: 0, onExit: (error) => this.#pool?.terminateWithError(error, { canRestart: true }) });
144
+ }
145
+
146
+ async teardown() {
147
+ await this.#service?.stop(); // undefined when setup() did not get that far
148
+ }
149
+
150
+ async run() {
151
+ return this.#service?.thumbnail();
152
+ }
153
+ }
154
+ ```
155
+
156
+ What to rely on, and what not to:
116
157
 
117
- The Host's built-in Plugin declares one Resource Group any Plugin's Node Types may name: `disk` (exported as `DISK_GROUP` since 0.3.0), a concurrency limit on Tasks that read or write the Host's disks. It is 4 unless the Host overrides it on its Resource Groups page. Name it when your Tasks do heavy disk work (copying, transcoding, indexing), so they share one budget with the Host's own file system Node Types instead of thrashing the disk beside them:
158
+ - **A `setup()` that throws** fails every Task the instance is given, with its message, and its `teardown()` runs at once. It is not tried again on that instance: the pool builds a new one only once it retires this one, at a Reload, or once no Task has reached it for its `idleTimeout`, which a steady stream of Tasks never allows. So a `setup()` that may fail for a passing reason (a port taken, a service still starting) should retry within itself before it throws, and `teardown()` must cope with a `setup()` that never finished.
159
+ - **`pool.terminateWithError(error, { canRestart: true })`** asks the pool to replace the instance, say after the process it owns dies. It returns at once and never throws, so call it from a callback or a timer. The instance's `teardown()` runs, its Tasks still running go back in the queue to run again on a new instance, and that one is built and set up. With `canRestart: false` those Tasks fail instead. The pool replaces instances only so many times (3, fyflow-scheduler's `maxWorkerRestarts`); after that it builds no more, and its Tasks wait until a Reload. So ask when the instance is broken, not because a Task failed: throw from `run` for that.
160
+ - **Never assume one copy.** Each Profile has its own pool, a pool may hold several instances (up to its `maxThreads`), and after a Reload the replaced Runtime's instances keep running its Runs while the new Runtime sets up its own. Bind port 0, not a fixed port, and keep anything shared outside the Worker.
161
+ - **`idleTimeout`** defaults to 5 seconds. For a Worker costly to set up, give its Node Type's `pool` a longer one (`pool: { idleTimeout: 60_000 }`); a Profile may override it for that Node Type. `0` keeps an instance until its Runtime shuts down.
162
+
163
+ ## The `disk` and `cpu` groups
164
+
165
+ The Host's built-in Plugin declares two Resource Groups any Plugin's Node Types may name, each a concurrency limit a Host can override on its Resource Groups page:
166
+
167
+ - `disk` (exported as `DISK_GROUP` since 0.3.0), on Tasks that read or write the Host's disks. It is 4. Name it when your Tasks do heavy disk work (copying, transcoding, indexing), so they share one budget with the Host's own file system Node Types instead of thrashing the disk beside them.
168
+ - `cpu` (exported as `CPU_GROUP` since 0.4.0), on Tasks that keep a processor busy (hashing, encoding, image work). It is the Host's logical processor count, so such Tasks from every Plugin together take no more processors than there are.
169
+
170
+ A Task that does both names both:
118
171
 
119
172
  ```ts
120
- import { DISK_GROUP, definePlugin } from "fyflow-plugin-sdk";
173
+ import { CPU_GROUP, DISK_GROUP, definePlugin } from "fyflow-plugin-sdk";
121
174
 
122
175
  export default definePlugin({
123
176
  // …
124
- nodeTypes: { Transcode: { worker: "transcode", groups: [DISK_GROUP] } },
177
+ nodeTypes: { Transcode: { worker: "transcode", groups: [DISK_GROUP, CPU_GROUP] } },
125
178
  });
126
179
  ```
127
180
 
128
- Other Plugins' groups stay out of reach, so Disabling one Plugin never breaks another. `DISK_GROUP` is a value, so a Plugin that imports it needs the SDK at run time; writing `"disk"` instead works on any Host that has the group.
181
+ Other Plugins' groups stay out of reach, so Disabling one Plugin never breaks another. The keys are values, so a Plugin that imports them needs the SDK at run time; writing `"disk"` or `"cpu"` instead works on any Host that has the group. Hosts before SDK 0.4.0 have no `cpu` and skip a Plugin naming it, so a Plugin that names it says `^0.4.0` in its peer range.
129
182
 
130
183
  ## Profiles and secrets
131
184
 
132
- A Node Type's Worker config lives on the Host, not in Flows: a **Profile** is a named configuration of a Node Type, edited in the Host's UI with a form built from the Worker's `configSchema`, and a task Node chooses one. Every Node Type has a default Profile seeded from its declaration's `config`. A Profile's config is merged over the default's, which is merged over yours.
185
+ A Plugin's configuration lives on the Host, not in Flows: a **Profile** is a named configuration with a section for each Plugin, edited in the Host's UI with a form built from the Plugin's `profile.schema`. Every Host has a Default Profile. Declare your fields once, in `profile`, and list on each Node Type the ones it takes: a field several Node Types take is set once, for all of them. A field no Profile sets is its `defaults` value.
186
+
187
+ A **start-up field** (listed in `profile.startup`) is read when a Worker starts: the start-up fields a Node Type takes are its Workers' config, the constructor's argument, and a change takes effect at the Host's next Reload. Use one for what a Worker sets up once: a connection, a loaded model.
188
+
189
+ Every other field is a **per-Task field**: the Host reads it as each Task is dispatched and hands the ones the Node Type takes to `run` as `settings.profile` (`{}` when it takes none), so a change reaches the next Task with no Reload. Type them with `TaskEnvelope<Settings, Input, ProfileFields>`. Your `settingsSchema` may not name `profile`. List in `profile.profileOnly` the per-Task fields only a Profile may set: a grant, such as the folders Tasks may reach, beside a field a Flow may set to narrow it, which your Worker checks against the grant. Per-Task fields never reach a Run's records, but what `run` returns is recorded, so do not return them.
133
190
 
134
- Mark a config property `writeOnly: true` to make it a **secret**: the Host never shows it again, only whether it is set, and redacts its value from errors and logs. Only properties of the config object can be secrets.
191
+ Mark a property of `profile.schema` `writeOnly: true` to make it a **secret**: the Host never shows it again, only whether it is set, and redacts its value from errors and logs. Only properties of the schema can be secrets.
135
192
 
136
193
  ## Versioning
137
194
 
package/UPGRADING.md ADDED
@@ -0,0 +1,160 @@
1
+ # Upgrading a Plugin
2
+
3
+ How to move a Plugin written for an earlier `fyflow-plugin-sdk` to this one, one version step at a time, newest first. A Host skips a Plugin whose peer range it does not implement, and says so on its Plugins page; when the Plugin was written for an earlier SDK, the reason points here.
4
+
5
+ To upgrade across several steps, apply them oldest first: a Plugin on 0.3 takes [0.3 to 0.4](#03-to-04), then [0.4 to 0.5](#04-to-05).
6
+
7
+ ## 0.4 to 0.5
8
+
9
+ 0.5.0 changed what a Profile is. A Profile used to be a configuration of one Node Type, validated by its Worker's `configSchema` and seeded from the Node Type's `config`. Now a Profile is a named, Host-wide configuration with one **section per Plugin**: the Plugin declares its fields once, in `profile`, and each Node Type lists the fields it takes. A Host on 0.5 implements no earlier SDK version, so a Plugin is skipped until it is upgraded.
10
+
11
+ Each field is one of these:
12
+
13
+ - a **start-up field** (listed in `profile.startup`): read once, when a Worker starts, as its config (the constructor's argument). Only a Profile sets it, and a change takes effect at the Host's next Reload. This is what every Profile field was before 0.5.
14
+ - a **per-Task field** (every other field): handed to each Task as `settings.profile`, read when the Task is dispatched, so a change needs no Reload. A Flow or one of its Nodes may set its own value, unless the field is listed in `profile.profileOnly`.
15
+
16
+ A property marked `writeOnly: true` is still a **secret**. Moving it into `profile.schema` keeps it one. Resource Groups, `DISK_GROUP` and `CPU_GROUP`, icons and gate types are as in 0.4. The Worker types gained what per-Task fields need: `TaskEnvelope` and `Worker` take a third type parameter, `ProfileFields`, typing `settings.profile`; with it left out, a Worker typed for 0.4 still type-checks. And the SDK now promises a Worker's lifecycle, which fyflow already gave it: optional `setup()` and `teardown()`, and the constructor's second argument, a `PoolContext` with `terminateWithError` (README, "A Worker's lifecycle"). A Worker that already uses them can drop its own types for them and import `PoolContext`; one that does not changes nothing.
17
+
18
+ ### Checklist
19
+
20
+ Do each step, in order. Each one is mechanical: the Plugin behaves as before once all are done.
21
+
22
+ 1. **The peer range.** In `package.json`, set `"peerDependencies": { "fyflow-plugin-sdk": "^0.5.0" }`.
23
+ 2. **One schema of the Plugin's fields.** Add `profile: { schema }` to the Plugin. Its `schema` is an object schema (`type: "object"`, `properties`, and `additionalProperties: false` if your `configSchema`s had it) holding every property of every Worker's `configSchema`, each once.
24
+ - Two Workers' `configSchema`s naming the same property with different schemas: keep one property per meaning, and rename the other (say `timeoutMs` and `uploadTimeoutMs`). Rename it in that Worker's code too (step 6).
25
+ - Keep `writeOnly: true` on secrets. Only a property may be `writeOnly` (a nested one, such as `auth.password`, too), never the schema itself.
26
+ 3. **Delete `configSchema` from every Worker.** A Host refuses a Worker that still declares one.
27
+ 4. **Move each Node Type's `config` to `profile.defaults`**, and delete `config` from the Node Type. A Host refuses a Node Type that still declares one.
28
+ - Two Node Types giving the same field different values: a default is one value for the whole Plugin. To keep both, split the field in two, one for each Node Type (say `greeting` and `shoutGreeting`), each with its old value as its default, and have each Node Type take its own in step 5. A Worker class serving both reads whichever it was given (`config.shoutGreeting ?? config.greeting`). This keeps the Plugin working as before with nothing set on the Host.
29
+ - A default must pass `profile.schema` (a `minLength`, an `enum`). A field with no default, such as a secret, is absent from a Worker's config until a Profile sets it, as it was before.
30
+ 5. **List what each Node Type takes.** Give each Node Type `profile: [...]`, naming the fields of its Worker's old `configSchema`. A field left out never reaches its Workers, and a name `profile.schema` lacks is refused.
31
+ 6. **Make them start-up fields.** Set `profile.startup` to every field you listed (each must be a property of `profile.schema`). Workers still get the start-up fields their Node Type takes as their constructor's `config`, exactly as before, so no Worker code changes, except for a field you renamed or split in step 2 or 4.
32
+ 7. **Optional: per-Task fields.** A field a Worker could read per Task instead (a timeout, a target folder) can leave `startup`. Then:
33
+ - read it in `run` from `settings.profile.<field>`, not from the constructor's config;
34
+ - type it with `TaskEnvelope<Settings, Input, ProfileFields>` (or `Worker<Settings, Input, Output, ProfileFields>`);
35
+ - list a per-Task field a Flow must never set for itself (a grant, such as the folders Tasks may reach) in `profile.profileOnly`. Check any field a Flow narrows it with against the grant in the Worker.
36
+ 8. **Rename `profile` in a `settingsSchema`.** A Worker's `settingsSchema` may not have a property called `profile`: the Host puts the per-Task fields there. Flows that set it need their Nodes' settings renamed to match.
37
+ 9. **Error messages.** Where a Worker tells a person to set a value "on this Node's Profile of <Node Type>", say "in the <Plugin id> section of this Node's Profile".
38
+
39
+ A Host's stored Profiles from before 0.5 are not carried over: each Host sets its values again, on its Profiles page, once the upgraded Plugin loads.
40
+
41
+ ### Before and after
42
+
43
+ The example `signer` Plugin, as it moved from 0.2 to 0.5. Before:
44
+
45
+ ```ts
46
+ export default definePlugin({
47
+ id: "example.signer",
48
+ version: "1.0.0",
49
+ base: import.meta.url,
50
+ workers: {
51
+ sign: {
52
+ url: "./sign.worker.ts",
53
+ configSchema: {
54
+ type: "object",
55
+ properties: {
56
+ key: { type: "string", writeOnly: true, minLength: 8 },
57
+ algorithm: { enum: ["SHA-256", "SHA-512"] },
58
+ },
59
+ additionalProperties: false,
60
+ },
61
+ settingsSchema: {
62
+ type: "object",
63
+ properties: { field: { type: "string", minLength: 1 } },
64
+ additionalProperties: false,
65
+ },
66
+ },
67
+ },
68
+ nodeTypes: {
69
+ Sign: {
70
+ worker: "sign",
71
+ inline: true,
72
+ config: { algorithm: "SHA-256" },
73
+ groups: ["example.signing"],
74
+ icon: "signature",
75
+ description: "Signs a field of what reaches it with HMAC, keyed by its Profile's secret.",
76
+ },
77
+ },
78
+ groups: { "example.signing": { type: "ConcurrentLimitGroup", config: { limit: 2 } } },
79
+ // gateTypes unchanged
80
+ });
81
+ ```
82
+
83
+ After:
84
+
85
+ ```ts
86
+ export default definePlugin({
87
+ id: "example.signer",
88
+ version: "1.0.0",
89
+ base: import.meta.url,
90
+ // Steps 2, 4 and 6: the old configSchema's properties, the old config as defaults, both read at start-up.
91
+ profile: {
92
+ schema: {
93
+ type: "object",
94
+ properties: {
95
+ key: { type: "string", writeOnly: true, minLength: 8 },
96
+ algorithm: { enum: ["SHA-256", "SHA-512"] },
97
+ },
98
+ additionalProperties: false,
99
+ },
100
+ defaults: { algorithm: "SHA-256" },
101
+ startup: ["key", "algorithm"],
102
+ },
103
+ workers: {
104
+ sign: {
105
+ url: "./sign.worker.ts",
106
+ // Step 3: no configSchema.
107
+ settingsSchema: {
108
+ type: "object",
109
+ properties: { field: { type: "string", minLength: 1 } },
110
+ additionalProperties: false,
111
+ },
112
+ },
113
+ },
114
+ nodeTypes: {
115
+ Sign: {
116
+ worker: "sign",
117
+ inline: true,
118
+ profile: ["key", "algorithm"], // step 5, in place of `config` (step 4)
119
+ groups: ["example.signing"],
120
+ icon: "signature",
121
+ description: "Signs a field of what reaches it with HMAC, keyed by its Profile's secret.",
122
+ },
123
+ },
124
+ groups: { "example.signing": { type: "ConcurrentLimitGroup", config: { limit: 2 } } },
125
+ // gateTypes unchanged
126
+ });
127
+ ```
128
+
129
+ Its Worker kept its constructor and `run`; only its error message changed (step 9):
130
+
131
+ ```ts
132
+ if (key === undefined)
133
+ throw new Error("No signing key: set `key` in the example.signer section of this Node's Profile");
134
+ ```
135
+
136
+ and its `package.json` says `"fyflow-plugin-sdk": "^0.5.0"` (step 1).
137
+
138
+ ### Check the result
139
+
140
+ - **Types.** With `definePlugin` from `fyflow-plugin-sdk` 0.5 given the Plugin as one object literal, `tsc` refuses a leftover `configSchema` or Node Type `config`. A definition built from variables or spreads escapes that check, so rely on the Host's.
141
+ - **It loads.** Put the Plugin in a Host's Plugin directory and Reload on the Plugins page. A Plugin the Host refuses is listed as skipped, with the reason: each reason names what to fix, for instance ``its Node Type "Sign" takes the Profile field "keys", which its Plugin's `profile` does not declare``. The same list is `GET /api/plugins` (`skipped`).
142
+ - **It runs.** On the Profiles page, open the Default Profile: your Plugin's section shows a form from `profile.schema`. Set what has no default (a secret, typically), Reload when you set a start-up field, then run a Flow with one of its Node Types.
143
+
144
+ ## 0.3 to 0.4
145
+
146
+ 0.4.0 added `CPU_GROUP`, the key of the built-in `cpu` Resource Group: a concurrency limit, the Host's logical processor count unless it overrides it, for Tasks that keep a processor busy. Nothing else changed, and a Plugin for 0.3 loads as it is on a 0.4 Host.
147
+
148
+ - To name the group, add `CPU_GROUP` (or `"cpu"`) to a Node Type's `groups`, beside `DISK_GROUP` when its Tasks do both.
149
+ - A Host before 0.4 has no `cpu` group and skips a Plugin naming it, so such a Plugin's peer range says `^0.4.0`. One that does not name it may keep its range.
150
+
151
+ ## 0.2 to 0.3
152
+
153
+ 0.3.0 added types and `DISK_GROUP`; nothing a Plugin says changed meaning, and a Plugin for 0.2 loads as it is on a 0.3 Host.
154
+
155
+ - `Worker`, `WorkerClass`, `TaskEnvelope` and `WorkerContext` type a Worker class, its `run(envelope, context)` and its constructor's config. Import them with `import type`.
156
+ - `DISK_GROUP` is the key of the built-in `disk` Resource Group, for Tasks that do heavy disk work. A Plugin naming it says `^0.3.0`.
157
+
158
+ ## 0.1 to 0.2
159
+
160
+ 0.2.0 added a Node Type's optional `icon` (a name from the app's icon set, listed in the README) and one-line `description`, which the palette shows. Nothing changed meaning, and a Plugin for 0.1 loads as it is on a 0.2 Host.
package/dist/index.d.ts CHANGED
@@ -1,14 +1,15 @@
1
- import type { GateType, GroupInstance, GroupType, NodeTypeDeclaration, WorkerEntry } from "fyflow-orchestration";
1
+ import type { GateType, GroupInstance, GroupType, JsonSchema, NodeTypeDeclaration, WorkerEntry } from "fyflow-orchestration";
2
2
  export type { GateContext, GateType, GroupInstance, GroupType, JsonSchema, NodeTypeDeclaration, RetryPolicy, WorkerEntry, } from "fyflow-orchestration";
3
3
  /**
4
4
  * The SDK version this package is, and so the one a Host built with it implements. A Plugin whose
5
5
  * `fyflow-plugin-sdk` peer range excludes it, and every one in `SDK_COMPATIBLE`, is not loaded. Kept equal to
6
6
  * package.json's `version` (tested).
7
7
  */
8
- export declare const SDK_VERSION = "0.3.0";
8
+ export declare const SDK_VERSION = "0.5.0";
9
9
  /**
10
10
  * The earlier SDK versions this one is a superset of: nothing a Plugin written for them says has changed meaning,
11
- * so a Host implements them too (ADR-0021). 0.2.0 only added optional fields, and 0.3.0 only types.
11
+ * so a Host implements them too (ADR-0021). None: 0.5.0 replaced per-Node-Type Profiles with the Plugin's `profile`
12
+ * (ADR-0024), so a Plugin written for an earlier one is upgraded first (UPGRADING.md).
12
13
  */
13
14
  export declare const SDK_COMPATIBLE: readonly string[];
14
15
  /**
@@ -18,12 +19,44 @@ export declare const SDK_COMPATIBLE: readonly string[];
18
19
  * are out of reach.
19
20
  */
20
21
  export declare const DISK_GROUP = "disk";
21
- /** A Node Type as a Plugin declares it: fyflow's declaration, and how the app shows it (since 0.2.0). */
22
- export interface PluginNodeType extends NodeTypeDeclaration {
22
+ /**
23
+ * The key of the `cpu` Resource Group the Host's built-in Plugin declares (exported since 0.4.0): a concurrency
24
+ * limit (the Host's logical processors unless it overrides it) on Tasks that keep a processor busy. Any Plugin's
25
+ * Node Type may name it in `groups`, beside `disk` when its Tasks do both.
26
+ */
27
+ export declare const CPU_GROUP = "cpu";
28
+ /**
29
+ * A Node Type as a Plugin declares it: fyflow's declaration, how the app shows it (since 0.2.0), and the Profile
30
+ * fields it takes (since 0.5.0). Its config comes from Profiles: it declares none of its own.
31
+ */
32
+ export interface PluginNodeType extends Omit<NodeTypeDeclaration, "config"> {
23
33
  /** An icon from the app's icon set (Lucide names, listed in the README); an unknown one shows its Kind's icon. */
24
34
  icon?: string;
25
35
  /** One line on what it does, shown in the palette. */
26
36
  description?: string;
37
+ /** The fields of its Plugin's `profile` its Workers get; none when left out (since 0.5.0). */
38
+ profile?: readonly string[];
39
+ }
40
+ /**
41
+ * The fields a Plugin's Profile section holds (since 0.5.0, ADR-0024): set once per Profile, for every Node Type of
42
+ * the Plugin that takes them. A property of `schema` marked `writeOnly: true` is a secret: a Host never shows it.
43
+ */
44
+ export interface PluginProfile {
45
+ /** An object schema (fyflow's subset) of the fields, by name. */
46
+ schema: JsonSchema;
47
+ /** What a field is where no Profile sets it; checked against `schema`. */
48
+ defaults?: Readonly<Record<string, unknown>>;
49
+ /**
50
+ * The start-up fields: read once, when a Worker starts, as its config (its constructor's argument). Only a
51
+ * Profile sets them, and a change takes effect at the Host's next Reload. Every other field is a per-Task field:
52
+ * it reaches each Task as `settings.profile`, read when the Task is dispatched, so a change needs no Reload.
53
+ */
54
+ startup?: readonly string[];
55
+ /**
56
+ * The per-Task fields only a Profile may set, never a Flow or a Node: a grant, such as the folders Tasks may
57
+ * reach, beside a field a Flow may set to narrow it, which the Worker checks against the grant.
58
+ */
59
+ profileOnly?: readonly string[];
27
60
  }
28
61
  export interface Plugin {
29
62
  /** Stable, unique id, e.g. `acme.images`. */
@@ -32,10 +65,12 @@ export interface Plugin {
32
65
  /** What worker `url`s resolve against: the plugin module's own `import.meta.url`. */
33
66
  base: string;
34
67
  /**
35
- * Worker modules by key; `url` is relative to `base`. A property of a `configSchema` marked `writeOnly: true` is
36
- * a Profile secret: a Host never shows it (ADR-0016).
68
+ * Worker modules by key; `url` is relative to `base`. Their config is the Profile's start-up fields (see
69
+ * `profile`), so a Worker declares no `configSchema` of its own (since 0.5.0).
37
70
  */
38
- workers?: Readonly<Record<string, WorkerEntry>>;
71
+ workers?: Readonly<Record<string, Omit<WorkerEntry, "configSchema">>>;
72
+ /** This Plugin's Profile fields, which its Node Types take by name (since 0.5.0). */
73
+ profile?: PluginProfile;
39
74
  /**
40
75
  * Node Types by name, each naming one of this plugin's `workers`; `groups` names Resource Groups (from this
41
76
  * plugin's `groups`) every Task of the type acquires.
@@ -50,10 +85,14 @@ export interface Plugin {
50
85
  }
51
86
  /**
52
87
  * What a Worker's `run` is handed for one Task: the Node's `settings` (checked by the Worker's `settingsSchema`)
53
- * and the Task input, after the Node's transform if it has one (since 0.3.0).
88
+ * and the Task input, after the Node's transform if it has one (since 0.3.0). The Host adds the per-Task fields the
89
+ * Node Type takes as `settings.profile` (since 0.5.0; `{}` when it takes none), so a Worker's own `settingsSchema`
90
+ * may not name `profile`.
54
91
  */
55
- export interface TaskEnvelope<Settings = Record<string, unknown>, Input = unknown> {
56
- settings: Settings;
92
+ export interface TaskEnvelope<Settings = Record<string, unknown>, Input = unknown, ProfileFields = Readonly<Record<string, unknown>>> {
93
+ settings: Settings & {
94
+ readonly profile: ProfileFields;
95
+ };
57
96
  input: Input;
58
97
  }
59
98
  /** The second argument to a Worker's `run`: what it may do while its Task runs (since 0.3.0). */
@@ -66,15 +105,46 @@ export interface WorkerContext {
66
105
  */
67
106
  readonly signal: AbortSignal;
68
107
  }
69
- /** One instance of a Worker, reused across the Tasks of its pool (since 0.3.0). */
70
- export interface Worker<Settings = Record<string, unknown>, Input = unknown, Output = unknown> {
108
+ /**
109
+ * The second argument to a Worker's constructor: what it may ask of its pool while no Task is involved, such as after
110
+ * the process it owns dies (since 0.5.0).
111
+ */
112
+ export interface PoolContext {
113
+ /**
114
+ * Asks the pool to retire this instance: its `teardown()` runs, and a new instance is built and set up for what
115
+ * comes next. Its Tasks still running go back in the queue, unless `canRestart` is false, which fails them. It
116
+ * returns at once and never throws, so it is safe from a timer or a callback.
117
+ */
118
+ terminateWithError(error: Error, options?: {
119
+ canRestart?: boolean;
120
+ }): void;
121
+ }
122
+ /**
123
+ * One instance of a Worker, reused across the Tasks of its pool (since 0.3.0); `ProfileFields` its per-Task fields.
124
+ * Since 0.5.0 the SDK promises its lifecycle: built, `setup()`, any number of Tasks, `teardown()`.
125
+ */
126
+ export interface Worker<Settings = Record<string, unknown>, Input = unknown, Output = unknown, ProfileFields = Readonly<Record<string, unknown>>> {
71
127
  /** Does one Task. Throwing fails it; the error's message is what the Host shows. */
72
- run(envelope: TaskEnvelope<Settings, Input>, context: WorkerContext): Output | Promise<Output>;
128
+ run(envelope: TaskEnvelope<Settings, Input, ProfileFields>, context: WorkerContext): Output | Promise<Output>;
129
+ /**
130
+ * Runs once, after the constructor and before the first Task: where to start what the instance owns (a process, a
131
+ * connection, a loaded model). Throwing fails every Task of this instance with the error's message. It is not tried
132
+ * again on this instance: the pool builds a new one only once it retires this one (a Reload, or no Task reaching it for
133
+ * its `idleTimeout`).
134
+ */
135
+ setup?(): Promise<void>;
136
+ /**
137
+ * Runs once, when the pool retires the instance: idle past its `idleTimeout`, replaced, or its Runtime shut down
138
+ * (the Host stopping, or a Reload once the Runtime's last Run ends). Also after a `setup()` that threw, so it must
139
+ * cope with one that never finished.
140
+ */
141
+ teardown?(): Promise<void>;
73
142
  }
74
143
  /**
75
- * A Worker module's default export: a class whose constructor gets the config of the Profile its pool was built
76
- * for (the Worker's `configSchema` checks it), once per instance (since 0.3.0).
144
+ * A Worker module's default export: a class whose constructor gets, once per instance, the start-up fields its
145
+ * Node Type takes from the Profile its pool was built for (since 0.3.0; from the Plugin's `profile` since 0.5.0), and its
146
+ * pool's context (since 0.5.0).
77
147
  */
78
- export type WorkerClass<Config = Record<string, unknown>, Settings = Record<string, unknown>, Input = unknown, Output = unknown> = new (config: Config) => Worker<Settings, Input, Output>;
148
+ export type WorkerClass<Config = Record<string, unknown>, Settings = Record<string, unknown>, Input = unknown, Output = unknown> = new (config: Config, pool?: PoolContext) => Worker<Settings, Input, Output>;
79
149
  /** Identity at runtime; gives plugin authors type checking and a single import. */
80
150
  export declare function definePlugin(plugin: Plugin): Plugin;
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
- // The contract between fyflow-app and the code that extends it (ADR-0007, ADR-0015, ADR-0016). Published as
2
- // `fyflow-plugin-sdk`: everything here is types but `definePlugin`, `SDK_VERSION`, `SDK_COMPATIBLE` and
3
- // `DISK_GROUP`.
1
+ // The contract between fyflow-app and the code that extends it (ADR-0007, ADR-0015, ADR-0024). Published as
2
+ // `fyflow-plugin-sdk`: everything here is types but `definePlugin`, `SDK_VERSION`, `SDK_COMPATIBLE`, `DISK_GROUP`
3
+ // and `CPU_GROUP`.
4
4
 
5
5
 
6
6
 
7
7
 
8
+
8
9
 
9
10
 
10
11
 
@@ -25,13 +26,14 @@
25
26
  * `fyflow-plugin-sdk` peer range excludes it, and every one in `SDK_COMPATIBLE`, is not loaded. Kept equal to
26
27
  * package.json's `version` (tested).
27
28
  */
28
- export const SDK_VERSION = "0.3.0";
29
+ export const SDK_VERSION = "0.5.0";
29
30
 
30
31
  /**
31
32
  * The earlier SDK versions this one is a superset of: nothing a Plugin written for them says has changed meaning,
32
- * so a Host implements them too (ADR-0021). 0.2.0 only added optional fields, and 0.3.0 only types.
33
+ * so a Host implements them too (ADR-0021). None: 0.5.0 replaced per-Node-Type Profiles with the Plugin's `profile`
34
+ * (ADR-0024), so a Plugin written for an earlier one is upgraded first (UPGRADING.md).
33
35
  */
34
- export const SDK_COMPATIBLE = ["0.2.0", "0.1.0"];
36
+ export const SDK_COMPATIBLE = [];
35
37
 
36
38
  /**
37
39
  * The key of the `disk` Resource Group the Host's built-in Plugin declares (exported since 0.3.0): a concurrency
@@ -41,12 +43,46 @@ export const SDK_COMPATIBLE = ["0.2.0", "0.1.0"];
41
43
  */
42
44
  export const DISK_GROUP = "disk";
43
45
 
44
- /** A Node Type as a Plugin declares it: fyflow's declaration, and how the app shows it (since 0.2.0). */
45
-
46
+ /**
47
+ * The key of the `cpu` Resource Group the Host's built-in Plugin declares (exported since 0.4.0): a concurrency
48
+ * limit (the Host's logical processors unless it overrides it) on Tasks that keep a processor busy. Any Plugin's
49
+ * Node Type may name it in `groups`, beside `disk` when its Tasks do both.
50
+ */
51
+ export const CPU_GROUP = "cpu";
52
+
53
+ /**
54
+ * A Node Type as a Plugin declares it: fyflow's declaration, how the app shows it (since 0.2.0), and the Profile
55
+ * fields it takes (since 0.5.0). Its config comes from Profiles: it declares none of its own.
56
+ */
57
+
46
58
 
47
59
 
48
60
 
49
61
 
62
+
63
+
64
+
65
+
66
+ /**
67
+ * The fields a Plugin's Profile section holds (since 0.5.0, ADR-0024): set once per Profile, for every Node Type of
68
+ * the Plugin that takes them. A property of `schema` marked `writeOnly: true` is a secret: a Host never shows it.
69
+ */
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
50
86
 
51
87
 
52
88
 
@@ -56,10 +92,12 @@ export const DISK_GROUP = "disk";
56
92
 
57
93
 
58
94
 
59
-
60
-
95
+
96
+
61
97
 
62
-
98
+
99
+
100
+
63
101
 
64
102
 
65
103
 
@@ -75,10 +113,16 @@ export const DISK_GROUP = "disk";
75
113
 
76
114
  /**
77
115
  * What a Worker's `run` is handed for one Task: the Node's `settings` (checked by the Worker's `settingsSchema`)
78
- * and the Task input, after the Node's transform if it has one (since 0.3.0).
116
+ * and the Task input, after the Node's transform if it has one (since 0.3.0). The Host adds the per-Task fields the
117
+ * Node Type takes as `settings.profile` (since 0.5.0; `{}` when it takes none), so a Worker's own `settingsSchema`
118
+ * may not name `profile`.
79
119
  */
80
-
81
-
120
+
121
+
122
+
123
+
124
+
125
+
82
126
 
83
127
 
84
128
 
@@ -93,22 +137,60 @@ export const DISK_GROUP = "disk";
93
137
 
94
138
 
95
139
 
96
- /** One instance of a Worker, reused across the Tasks of its pool (since 0.3.0). */
97
-
140
+ /**
141
+ * The second argument to a Worker's constructor: what it may ask of its pool while no Task is involved, such as after
142
+ * the process it owns dies (since 0.5.0).
143
+ */
144
+
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+ /**
154
+ * One instance of a Worker, reused across the Tasks of its pool (since 0.3.0); `ProfileFields` its per-Task fields.
155
+ * Since 0.5.0 the SDK promises its lifecycle: built, `setup()`, any number of Tasks, `teardown()`.
156
+ */
157
+
158
+
159
+
160
+
161
+
162
+
98
163
 
99
-
164
+
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+
100
181
 
101
182
 
102
183
  /**
103
- * A Worker module's default export: a class whose constructor gets the config of the Profile its pool was built
104
- * for (the Worker's `configSchema` checks it), once per instance (since 0.3.0).
184
+ * A Worker module's default export: a class whose constructor gets, once per instance, the start-up fields its
185
+ * Node Type takes from the Profile its pool was built for (since 0.3.0; from the Plugin's `profile` since 0.5.0), and its
186
+ * pool's context (since 0.5.0).
105
187
  */
106
188
 
107
189
 
108
190
 
109
191
 
110
192
 
111
-
193
+
112
194
 
113
195
  /** Identity at runtime; gives plugin authors type checking and a single import. */
114
196
  export function definePlugin(plugin ) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fyflow-plugin-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Types and definePlugin for fyflow-app Plugins: Node Types, Workers, gate types and Resource Groups",
5
5
  "keywords": [
6
6
  "fyflow",
@@ -20,7 +20,7 @@
20
20
  "access": "public"
21
21
  },
22
22
  "peerDependencies": {
23
- "fyflow-orchestration": "^0.12.0"
23
+ "fyflow-orchestration": "^0.13.0"
24
24
  },
25
25
  "peerDependenciesMeta": {
26
26
  "fyflow-orchestration": {
@@ -28,12 +28,13 @@
28
28
  }
29
29
  },
30
30
  "devDependencies": {
31
- "fyflow-orchestration": "0.12.0"
31
+ "fyflow-orchestration": "0.13.0"
32
32
  },
33
33
  "files": [
34
34
  "dist",
35
35
  "src",
36
- "!src/**/*.test.ts"
36
+ "!src/**/*.test.ts",
37
+ "UPGRADING.md"
37
38
  ],
38
39
  "main": "./dist/index.js",
39
40
  "types": "./dist/index.d.ts"
package/src/index.ts CHANGED
@@ -1,10 +1,11 @@
1
- // The contract between fyflow-app and the code that extends it (ADR-0007, ADR-0015, ADR-0016). Published as
2
- // `fyflow-plugin-sdk`: everything here is types but `definePlugin`, `SDK_VERSION`, `SDK_COMPATIBLE` and
3
- // `DISK_GROUP`.
1
+ // The contract between fyflow-app and the code that extends it (ADR-0007, ADR-0015, ADR-0024). Published as
2
+ // `fyflow-plugin-sdk`: everything here is types but `definePlugin`, `SDK_VERSION`, `SDK_COMPATIBLE`, `DISK_GROUP`
3
+ // and `CPU_GROUP`.
4
4
  import type {
5
5
  GateType,
6
6
  GroupInstance,
7
7
  GroupType,
8
+ JsonSchema,
8
9
  NodeTypeDeclaration,
9
10
  WorkerEntry,
10
11
  } from "fyflow-orchestration";
@@ -25,13 +26,14 @@ export type {
25
26
  * `fyflow-plugin-sdk` peer range excludes it, and every one in `SDK_COMPATIBLE`, is not loaded. Kept equal to
26
27
  * package.json's `version` (tested).
27
28
  */
28
- export const SDK_VERSION = "0.3.0";
29
+ export const SDK_VERSION = "0.5.0";
29
30
 
30
31
  /**
31
32
  * The earlier SDK versions this one is a superset of: nothing a Plugin written for them says has changed meaning,
32
- * so a Host implements them too (ADR-0021). 0.2.0 only added optional fields, and 0.3.0 only types.
33
+ * so a Host implements them too (ADR-0021). None: 0.5.0 replaced per-Node-Type Profiles with the Plugin's `profile`
34
+ * (ADR-0024), so a Plugin written for an earlier one is upgraded first (UPGRADING.md).
33
35
  */
34
- export const SDK_COMPATIBLE: readonly string[] = ["0.2.0", "0.1.0"];
36
+ export const SDK_COMPATIBLE: readonly string[] = [];
35
37
 
36
38
  /**
37
39
  * The key of the `disk` Resource Group the Host's built-in Plugin declares (exported since 0.3.0): a concurrency
@@ -41,12 +43,46 @@ export const SDK_COMPATIBLE: readonly string[] = ["0.2.0", "0.1.0"];
41
43
  */
42
44
  export const DISK_GROUP = "disk";
43
45
 
44
- /** A Node Type as a Plugin declares it: fyflow's declaration, and how the app shows it (since 0.2.0). */
45
- export interface PluginNodeType extends NodeTypeDeclaration {
46
+ /**
47
+ * The key of the `cpu` Resource Group the Host's built-in Plugin declares (exported since 0.4.0): a concurrency
48
+ * limit (the Host's logical processors unless it overrides it) on Tasks that keep a processor busy. Any Plugin's
49
+ * Node Type may name it in `groups`, beside `disk` when its Tasks do both.
50
+ */
51
+ export const CPU_GROUP = "cpu";
52
+
53
+ /**
54
+ * A Node Type as a Plugin declares it: fyflow's declaration, how the app shows it (since 0.2.0), and the Profile
55
+ * fields it takes (since 0.5.0). Its config comes from Profiles: it declares none of its own.
56
+ */
57
+ export interface PluginNodeType extends Omit<NodeTypeDeclaration, "config"> {
46
58
  /** An icon from the app's icon set (Lucide names, listed in the README); an unknown one shows its Kind's icon. */
47
59
  icon?: string;
48
60
  /** One line on what it does, shown in the palette. */
49
61
  description?: string;
62
+ /** The fields of its Plugin's `profile` its Workers get; none when left out (since 0.5.0). */
63
+ profile?: readonly string[];
64
+ }
65
+
66
+ /**
67
+ * The fields a Plugin's Profile section holds (since 0.5.0, ADR-0024): set once per Profile, for every Node Type of
68
+ * the Plugin that takes them. A property of `schema` marked `writeOnly: true` is a secret: a Host never shows it.
69
+ */
70
+ export interface PluginProfile {
71
+ /** An object schema (fyflow's subset) of the fields, by name. */
72
+ schema: JsonSchema;
73
+ /** What a field is where no Profile sets it; checked against `schema`. */
74
+ defaults?: Readonly<Record<string, unknown>>;
75
+ /**
76
+ * The start-up fields: read once, when a Worker starts, as its config (its constructor's argument). Only a
77
+ * Profile sets them, and a change takes effect at the Host's next Reload. Every other field is a per-Task field:
78
+ * it reaches each Task as `settings.profile`, read when the Task is dispatched, so a change needs no Reload.
79
+ */
80
+ startup?: readonly string[];
81
+ /**
82
+ * The per-Task fields only a Profile may set, never a Flow or a Node: a grant, such as the folders Tasks may
83
+ * reach, beside a field a Flow may set to narrow it, which the Worker checks against the grant.
84
+ */
85
+ profileOnly?: readonly string[];
50
86
  }
51
87
 
52
88
  export interface Plugin {
@@ -56,10 +92,12 @@ export interface Plugin {
56
92
  /** What worker `url`s resolve against: the plugin module's own `import.meta.url`. */
57
93
  base: string;
58
94
  /**
59
- * Worker modules by key; `url` is relative to `base`. A property of a `configSchema` marked `writeOnly: true` is
60
- * a Profile secret: a Host never shows it (ADR-0016).
95
+ * Worker modules by key; `url` is relative to `base`. Their config is the Profile's start-up fields (see
96
+ * `profile`), so a Worker declares no `configSchema` of its own (since 0.5.0).
61
97
  */
62
- workers?: Readonly<Record<string, WorkerEntry>>;
98
+ workers?: Readonly<Record<string, Omit<WorkerEntry, "configSchema">>>;
99
+ /** This Plugin's Profile fields, which its Node Types take by name (since 0.5.0). */
100
+ profile?: PluginProfile;
63
101
  /**
64
102
  * Node Types by name, each naming one of this plugin's `workers`; `groups` names Resource Groups (from this
65
103
  * plugin's `groups`) every Task of the type acquires.
@@ -75,10 +113,16 @@ export interface Plugin {
75
113
 
76
114
  /**
77
115
  * What a Worker's `run` is handed for one Task: the Node's `settings` (checked by the Worker's `settingsSchema`)
78
- * and the Task input, after the Node's transform if it has one (since 0.3.0).
116
+ * and the Task input, after the Node's transform if it has one (since 0.3.0). The Host adds the per-Task fields the
117
+ * Node Type takes as `settings.profile` (since 0.5.0; `{}` when it takes none), so a Worker's own `settingsSchema`
118
+ * may not name `profile`.
79
119
  */
80
- export interface TaskEnvelope<Settings = Record<string, unknown>, Input = unknown> {
81
- settings: Settings;
120
+ export interface TaskEnvelope<
121
+ Settings = Record<string, unknown>,
122
+ Input = unknown,
123
+ ProfileFields = Readonly<Record<string, unknown>>,
124
+ > {
125
+ settings: Settings & { readonly profile: ProfileFields };
82
126
  input: Input;
83
127
  }
84
128
 
@@ -93,22 +137,60 @@ export interface WorkerContext {
93
137
  readonly signal: AbortSignal;
94
138
  }
95
139
 
96
- /** One instance of a Worker, reused across the Tasks of its pool (since 0.3.0). */
97
- export interface Worker<Settings = Record<string, unknown>, Input = unknown, Output = unknown> {
140
+ /**
141
+ * The second argument to a Worker's constructor: what it may ask of its pool while no Task is involved, such as after
142
+ * the process it owns dies (since 0.5.0).
143
+ */
144
+ export interface PoolContext {
145
+ /**
146
+ * Asks the pool to retire this instance: its `teardown()` runs, and a new instance is built and set up for what
147
+ * comes next. Its Tasks still running go back in the queue, unless `canRestart` is false, which fails them. It
148
+ * returns at once and never throws, so it is safe from a timer or a callback.
149
+ */
150
+ terminateWithError(error: Error, options?: { canRestart?: boolean }): void;
151
+ }
152
+
153
+ /**
154
+ * One instance of a Worker, reused across the Tasks of its pool (since 0.3.0); `ProfileFields` its per-Task fields.
155
+ * Since 0.5.0 the SDK promises its lifecycle: built, `setup()`, any number of Tasks, `teardown()`.
156
+ */
157
+ export interface Worker<
158
+ Settings = Record<string, unknown>,
159
+ Input = unknown,
160
+ Output = unknown,
161
+ ProfileFields = Readonly<Record<string, unknown>>,
162
+ > {
98
163
  /** Does one Task. Throwing fails it; the error's message is what the Host shows. */
99
- run(envelope: TaskEnvelope<Settings, Input>, context: WorkerContext): Output | Promise<Output>;
164
+ run(
165
+ envelope: TaskEnvelope<Settings, Input, ProfileFields>,
166
+ context: WorkerContext,
167
+ ): Output | Promise<Output>;
168
+ /**
169
+ * Runs once, after the constructor and before the first Task: where to start what the instance owns (a process, a
170
+ * connection, a loaded model). Throwing fails every Task of this instance with the error's message. It is not tried
171
+ * again on this instance: the pool builds a new one only once it retires this one (a Reload, or no Task reaching it for
172
+ * its `idleTimeout`).
173
+ */
174
+ setup?(): Promise<void>;
175
+ /**
176
+ * Runs once, when the pool retires the instance: idle past its `idleTimeout`, replaced, or its Runtime shut down
177
+ * (the Host stopping, or a Reload once the Runtime's last Run ends). Also after a `setup()` that threw, so it must
178
+ * cope with one that never finished.
179
+ */
180
+ teardown?(): Promise<void>;
100
181
  }
101
182
 
102
183
  /**
103
- * A Worker module's default export: a class whose constructor gets the config of the Profile its pool was built
104
- * for (the Worker's `configSchema` checks it), once per instance (since 0.3.0).
184
+ * A Worker module's default export: a class whose constructor gets, once per instance, the start-up fields its
185
+ * Node Type takes from the Profile its pool was built for (since 0.3.0; from the Plugin's `profile` since 0.5.0), and its
186
+ * pool's context (since 0.5.0).
105
187
  */
106
188
  export type WorkerClass<
107
189
  Config = Record<string, unknown>,
108
190
  Settings = Record<string, unknown>,
109
191
  Input = unknown,
110
192
  Output = unknown,
111
- > = new (config: Config) => Worker<Settings, Input, Output>;
193
+ > = new (config: Config, pool?: PoolContext) => Worker<Settings, Input, Output>;
112
194
 
113
195
  /** Identity at runtime; gives plugin authors type checking and a single import. */
114
196
  export function definePlugin(plugin: Plugin): Plugin {