@pikku/skills 0.12.2 → 0.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/skills.gen.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/pikku-addon/SKILL.md +56 -29
  4. package/skills/pikku-ai-agent/SKILL.md +197 -105
  5. package/skills/pikku-ai-vercel/SKILL.md +57 -18
  6. package/skills/pikku-ai-voice/SKILL.md +126 -52
  7. package/skills/pikku-audit/SKILL.md +35 -13
  8. package/skills/pikku-aws/SKILL.md +66 -16
  9. package/skills/pikku-backblaze/SKILL.md +44 -11
  10. package/skills/pikku-better-auth/SKILL.md +80 -34
  11. package/skills/pikku-cli/SKILL.md +67 -18
  12. package/skills/pikku-cli/references/complete-example.md +2 -0
  13. package/skills/pikku-concepts/SKILL.md +82 -10
  14. package/skills/pikku-concepts/references/concept-mapping.md +2 -2
  15. package/skills/pikku-config/SKILL.md +134 -52
  16. package/skills/pikku-cron/SKILL.md +13 -6
  17. package/skills/pikku-deploy-azure/SKILL.md +83 -28
  18. package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
  19. package/skills/pikku-deploy-express/SKILL.md +40 -4
  20. package/skills/pikku-deploy-fastify/SKILL.md +22 -1
  21. package/skills/pikku-deploy-lambda/SKILL.md +99 -19
  22. package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
  23. package/skills/pikku-deploy-uws/SKILL.md +54 -1
  24. package/skills/pikku-deps/SKILL.md +29 -8
  25. package/skills/pikku-emails/SKILL.md +36 -5
  26. package/skills/pikku-fabric/SKILL.md +30 -5
  27. package/skills/pikku-fabric-debug/SKILL.md +5 -1
  28. package/skills/pikku-feature/SKILL.md +12 -7
  29. package/skills/pikku-gateway-slack/SKILL.md +72 -11
  30. package/skills/pikku-http/SKILL.md +18 -5
  31. package/skills/pikku-http/references/http-options.md +10 -5
  32. package/skills/pikku-i18n/SKILL.md +18 -7
  33. package/skills/pikku-info/SKILL.md +18 -8
  34. package/skills/pikku-jose/SKILL.md +35 -6
  35. package/skills/pikku-knowledge/SKILL.md +3 -3
  36. package/skills/pikku-kysely/SKILL.md +78 -15
  37. package/skills/pikku-machine-auth/SKILL.md +36 -1
  38. package/skills/pikku-mcp/SKILL.md +159 -149
  39. package/skills/pikku-middleware/SKILL.md +17 -5
  40. package/skills/pikku-mongodb/SKILL.md +10 -2
  41. package/skills/pikku-n8n-import/SKILL.md +14 -6
  42. package/skills/pikku-permissions/SKILL.md +102 -22
  43. package/skills/pikku-pino/SKILL.md +12 -4
  44. package/skills/pikku-product-second-opinion/SKILL.md +3 -3
  45. package/skills/pikku-queue/SKILL.md +45 -16
  46. package/skills/pikku-react/SKILL.md +41 -14
  47. package/skills/pikku-react-query/SKILL.md +14 -10
  48. package/skills/pikku-realtime/SKILL.md +44 -22
  49. package/skills/pikku-redis/SKILL.md +12 -3
  50. package/skills/pikku-rpc/SKILL.md +23 -12
  51. package/skills/pikku-rtl/SKILL.md +21 -17
  52. package/skills/pikku-scenario/SKILL.md +285 -50
  53. package/skills/pikku-schedule/SKILL.md +39 -6
  54. package/skills/pikku-schema-ajv/SKILL.md +24 -2
  55. package/skills/pikku-schema-cfworker/SKILL.md +22 -2
  56. package/skills/pikku-security/SKILL.md +54 -9
  57. package/skills/pikku-services/SKILL.md +49 -9
  58. package/skills/pikku-services/references/audit-wire-service.md +2 -1
  59. package/skills/pikku-software-archaeology/README.md +16 -6
  60. package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
  61. package/skills/pikku-template-clone/SKILL.md +10 -5
  62. package/skills/pikku-trigger/SKILL.md +50 -6
  63. package/skills/pikku-versioning/SKILL.md +46 -17
  64. package/skills/pikku-websocket/SKILL.md +72 -44
  65. package/skills/pikku-workflow/SKILL.md +35 -1
  66. package/skills/pikku-workflow/references/workflow-reference.md +13 -8
  67. package/skills/pikku-workflows-client/SKILL.md +13 -6
  68. package/skills/pikku-ws/SKILL.md +44 -8
@@ -34,20 +34,22 @@ See `pikku-concepts` for the core mental model.
34
34
 
35
35
  ## Function Versioning
36
36
 
37
- When you need to introduce a breaking change, keep the old function as a pinned version and let the new one become the latest.
37
+ A function with `version: N` is registered under the id `name@vN`. The bare
38
+ name still resolves to it, so callers that don't care about versions keep
39
+ working while a pinned `getBook@v1` stays addressable for the ones that do.
38
40
 
39
- **The pattern:**
41
+ **The pattern:** when you need to introduce a breaking change, copy the current
42
+ function into a pinned `v1` and bump the live one to `version: 2`.
40
43
 
41
- 1. Create a new file `my-function-v1.function.ts` — export a variable with the `V1` suffix
42
- 2. Set `override: 'myFunction'` — this is the contract key the manifest groups under
43
- 3. Set `version: 1` — pins this as version 1 of the contract
44
- 4. The existing `my-function.function.ts` (no `version:` field) automatically becomes the latest version
44
+ 1. Create `my-function-v1.function.ts` exporting `getBookV1` with `version: 1` —
45
+ the trailing `V1` matching the version is stripped automatically, so the id
46
+ becomes `getBook@v1`
47
+ 2. Add `version: 2` to the existing `getBook`
45
48
 
46
49
  ```typescript
47
50
  // my-function-v1.function.ts — old contract, kept for running workflows/agents
48
51
  export const getBookV1 = pikkuFunc({
49
- override: 'getBook', // REQUIRED — links this to the 'getBook' contract family
50
- version: 1,
52
+ version: 1, // id becomes getBook@v1 — the V1 suffix is stripped
51
53
  input: z.object({ bookId: z.string() }),
52
54
  output: z.object({ title: z.string() }),
53
55
  func: async ({ db }, { bookId }) => {
@@ -55,8 +57,9 @@ export const getBookV1 = pikkuFunc({
55
57
  },
56
58
  })
57
59
 
58
- // my-function.function.ts — latest contract, no version: field
60
+ // my-function.function.ts — latest contract, id becomes getBook@v2
59
61
  export const getBook = pikkuFunc({
62
+ version: 2,
60
63
  input: z.object({
61
64
  bookId: z.string(),
62
65
  format: z.enum(['full', 'summary']),
@@ -72,7 +75,17 @@ export const getBook = pikkuFunc({
72
75
  })
73
76
  ```
74
77
 
75
- **Why `override` is required:** The manifest groups functions by a shared contract key. Without `override: 'getBook'`, `getBookV1` is stored internally as `getBookV1@v1` (key: `getBookV1`), which is a different contract family from `getBook`. With `override: 'getBook'`, it becomes `getBook@v1` (key: `getBook`), which groups with the unversioned `getBook` — and the unversioned one is automatically promoted to `getBook@v2`.
78
+ **Bump the live function explicitly.** Nothing promotes an unversioned function
79
+ to the next version for you — without `version: 2` it is treated as version 1 of
80
+ the `getBook` contract, colliding with the pinned `getBook@v1` and making
81
+ `versions check` report the published contract as modified.
82
+
83
+ **`override` is the escape hatch, not the requirement.** The contract key comes
84
+ from the exported name with a matching `V<n>` suffix removed, so
85
+ `getBookV1` + `version: 1` already lands on `getBook`. Use
86
+ `override: 'getBook'` only when the export can't follow that convention — for
87
+ instance `legacyGetBook` with `version: 1`, which would otherwise key under
88
+ `legacyGetBook`.
76
89
 
77
90
  ## Version Manifest (`versions.pikku.json`)
78
91
 
@@ -99,22 +112,38 @@ Pikku tracks contract hashes to detect breaking changes:
99
112
  }
100
113
  ```
101
114
 
102
- Each hash is derived from the function's input and output schemas plus the contract key. If a schema changes without a version bump, `pikku versions check` will fail.
115
+ Each hash is derived from the function's input and output schemas plus the
116
+ contract key. If a schema changes without a version bump, `pikku versions check`
117
+ will fail.
118
+
119
+ The manifest lives at `versions.pikku.json` in the project's `rootDir`, and its
120
+ presence is what switches versioning on — with no manifest, nothing is checked.
103
121
 
104
122
  ## CLI Commands
105
123
 
106
124
  ```bash
107
- npx pikku versions init # Initialize versioning manifest (run once)
125
+ npx pikku versions init # Create an empty versioning manifest (run once)
108
126
  npx pikku versions check # Detect contract changes (use in CI)
109
- npx pikku versions update # Update contract hashes after version bump
127
+ npx pikku versions update # Record current contract hashes
110
128
  ```
111
129
 
130
+ `init` writes `{ "manifestVersion": 1, "contracts": {} }` and nothing more — it
131
+ does **not** capture the hashes of the functions you already have. Run
132
+ `versions update` straight after it to record the current state, otherwise
133
+ `check` has nothing to compare against and silently passes.
134
+
135
+ `update` refuses to save when a published version's hash changed, so it can
136
+ never overwrite an immutable record; it reports that as a diagnostic and leaves
137
+ the manifest alone. Fix the contract or bump the version, then run it again.
138
+
112
139
  **Workflow:**
113
140
 
114
- 1. `pikku versions init` — run once to create `versions.pikku.json`
141
+ 1. `pikku versions init` then `pikku versions update` — once, to create and
142
+ populate `versions.pikku.json`
115
143
  2. Develop normally — add/modify functions
116
144
  3. `pikku versions check` — CI catches unversioned breaking changes
117
- 4. If intentional: create `my-function-v1.function.ts` with `override` + `version: 1`, then `pikku versions update`
145
+ 4. If intentional: pin the old contract as `…V1` with `version: 1`, bump the
146
+ live function to `version: 2`, then `pikku versions update`
118
147
 
119
148
  ## CI Integration
120
149
 
@@ -135,9 +164,8 @@ jobs:
135
164
  ## Complete Example
136
165
 
137
166
  ```typescript
138
- // create-todo-v1.function.ts — v1 locked contract
167
+ // create-todo-v1.function.ts — v1 locked contract, id: createTodo@v1
139
168
  export const createTodoV1 = pikkuSessionlessFunc({
140
- override: 'createTodo', // groups under 'createTodo' contract family
141
169
  version: 1,
142
170
  input: z.object({ title: z.string() }),
143
171
  output: z.object({ id: z.string(), title: z.string() }),
@@ -146,6 +174,7 @@ export const createTodoV1 = pikkuSessionlessFunc({
146
174
 
147
175
  // create-todo.function.ts — v2 (latest), called by default
148
176
  export const createTodo = pikkuSessionlessFunc({
177
+ version: 2,
149
178
  input: z.object({
150
179
  title: z.string(),
151
180
  priority: z.enum(['low', 'medium', 'high']),
@@ -6,7 +6,8 @@ description: >-
6
6
  generated WebSocket client. TRIGGER when: code uses wireChannel, user asks about WebSocket,
7
7
  real-time, live updates, chat, pub/sub, or the generated WebSocket client. DO NOT TRIGGER when:
8
8
  user asks about HTTP/REST (use pikku-http), SSE (use pikku-http with sse: true), or WebSocket
9
- deployment specifics (use pikku-deploy-uws).
9
+ deployment specifics (use pikku-deploy-uws), or typed pub/sub events (use pikku-realtime).
10
+ installGroups: [core]
10
11
  ---
11
12
 
12
13
  # Pikku WebSocket Wiring
@@ -41,19 +42,32 @@ import { wireChannel } from '@pikku/core/channel'
41
42
 
42
43
  wireChannel({
43
44
  name: string, // Channel name (e.g. 'todos')
44
- onConnect: async () => {}, // Called when client connects
45
- onDisconnect: async () => {}, // Called when client disconnects
46
- onMessageWiring: { // Action → function mapping
47
- [actionName: string]: {
48
- func: PikkuFunc,
49
- auth?: boolean, // Override channel-level auth
50
- permissions?: Record<string, PikkuPermission | PikkuPermission[]>,
45
+ route: string, // REQUIRED — the URL path (e.g. '/todos')
46
+ auth?: boolean, // Channel-level auth default
47
+ onConnect?: PikkuFunc, // Called when client connects
48
+ onDisconnect?: PikkuFunc, // Called when client disconnects
49
+ onMessage?: PikkuFunc, // Catch-all for unrouted messages
50
+ onMessageWiring?: { // TWO levels — see below
51
+ [messageField: string]: {
52
+ [fieldValue: string]: {
53
+ func: PikkuFunc,
54
+ auth?: boolean, // Override channel-level auth
55
+ middleware?: PikkuMiddleware[],
56
+ }
51
57
  }
52
58
  },
59
+ middleware?: PikkuMiddleware[],
53
60
  channelMiddleware?: PikkuChannelMiddleware[],
61
+ binary?: boolean | null,
62
+ onBinaryMessage?: (services, data, channel) => ...,
63
+ tags?: string[], // Targets tag middleware
54
64
  })
55
65
  ```
56
66
 
67
+ Note there is **no `permissions` key on a message wiring** — wire-level
68
+ permissions were removed in #972. Authorization lives on the function's own
69
+ `permissions` field (see `pikku-permissions`).
70
+
57
71
  ### `pikkuChannelMiddleware(fn)`
58
72
 
59
73
  ```typescript
@@ -78,37 +92,46 @@ addChannelMiddleware('todos', [addTimestamp, filterSensitive])
78
92
  ```typescript
79
93
  wireChannel({
80
94
  name: 'todos',
81
- onConnect: async () => {},
82
- onDisconnect: async () => {},
95
+ route: '/todos',
83
96
  onMessageWiring: {
84
- create: { func: createTodo },
85
- list: { func: listTodos, auth: false },
97
+ action: { // ← the field to route on
98
+ create: { func: createTodo }, // ← its possible values
99
+ list: { func: listTodos, auth: false },
100
+ },
86
101
  },
87
102
  })
88
103
  ```
89
104
 
90
105
  ### Action Routing with Auth
91
106
 
92
- Clients send `{ action: 'create', data: {...} }`. Pikku routes to the matching function.
107
+ `onMessageWiring` nests two levels because the routing key is configurable. The
108
+ **outer** key names the field in the incoming message to dispatch on; the
109
+ **inner** keys are the values that field can take. With the conventional outer
110
+ key `action`, a client sending `{ action: 'create', data: {...} }` reaches
111
+ `createTodo` — but a CLI channel might route on `command` instead, which is why
112
+ the field is not hardcoded.
93
113
 
94
114
  ```typescript
95
- const authenticate = pikkuFunc({
115
+ const authenticate = pikkuSessionlessFunc({
96
116
  title: 'Authenticate',
97
- func: async ({ setSession }, { token }) => {
117
+ // setSession lives on the WIRE (third param), not on services
118
+ func: async (services, { token }, { setSession }) => {
98
119
  const session = await verifyJWT(token)
99
- setSession(session)
120
+ await setSession(session)
100
121
  return { success: true }
101
122
  },
102
123
  })
103
124
 
104
125
  wireChannel({
105
126
  name: 'todos',
106
- onConnect: async () => {},
107
- onDisconnect: async () => {},
127
+ route: '/todos',
128
+ auth: true,
108
129
  onMessageWiring: {
109
- auth: { func: authenticate, auth: false }, // No session required
110
- subscribe: { func: subscribeTodos }, // Session required
111
- create: { func: createTodo },
130
+ action: {
131
+ authenticate: { func: authenticate, auth: false }, // No session required
132
+ subscribe: { func: subscribeTodos }, // Session required
133
+ create: { func: createTodo },
134
+ },
112
135
  },
113
136
  })
114
137
  ```
@@ -120,25 +143,28 @@ Use EventHub for real-time broadcasting across connections:
120
143
  ```typescript
121
144
  wireChannel({
122
145
  name: 'todos',
123
- onConnect: async ({ eventHub, channel }) => {
146
+ route: '/todos',
147
+ // eventHub is a service (1st param); channel lives on the wire (3rd)
148
+ onConnect: async ({ eventHub }, _data, { channel }) => {
124
149
  eventHub.subscribe('todos:updated', (data) => {
125
150
  channel.send(data)
126
151
  })
127
152
  },
128
- onDisconnect: async () => {},
129
153
  onMessageWiring: {
130
- create: {
131
- func: pikkuFunc({
132
- title: 'Create Todo',
133
- func: async ({ db, eventHub }, { text }) => {
134
- const todo = await db.createTodo({ text })
135
- eventHub.publish('todos:updated', {
136
- event: 'created',
137
- todo,
138
- })
139
- return { todo }
140
- },
141
- }),
154
+ action: {
155
+ create: {
156
+ func: pikkuFunc({
157
+ title: 'Create Todo',
158
+ func: async ({ db, eventHub }, { text }) => {
159
+ const todo = await db.createTodo({ text })
160
+ eventHub.publish('todos:updated', {
161
+ event: 'created',
162
+ todo,
163
+ })
164
+ return { todo }
165
+ },
166
+ }),
167
+ },
142
168
  },
143
169
  },
144
170
  })
@@ -167,9 +193,8 @@ addChannelMiddleware('todos', [addTimestamp, filterSensitive])
167
193
  // Or inline on wiring
168
194
  wireChannel({
169
195
  name: 'todos',
196
+ route: '/todos',
170
197
  channelMiddleware: [addTimestamp],
171
- onConnect: async () => {},
172
- onDisconnect: async () => {},
173
198
  onMessageWiring: { ... },
174
199
  })
175
200
  ```
@@ -179,7 +204,7 @@ wireChannel({
179
204
  After `npx pikku all`:
180
205
 
181
206
  ```typescript
182
- import { PikkuWebSocket } from '.pikku/pikku-websocket.gen.js'
207
+ import { PikkuWebSocket } from '#pikku/pikku-websocket.gen.js'
183
208
 
184
209
  const pikku = new PikkuWebSocket(ws)
185
210
  const todosRoute = pikku.getRoute('todos')
@@ -197,7 +222,7 @@ todosRoute.subscribe('todos:updated', (data) => {
197
222
 
198
223
  ```typescript
199
224
  // functions/chat.functions.ts
200
- export const authenticate = pikkuFunc({
225
+ export const authenticate = pikkuSessionlessFunc({
201
226
  title: 'Authenticate',
202
227
  func: async ({ jwt }, { token }, { setSession }) => {
203
228
  const payload = await jwt.verify(token)
@@ -228,16 +253,19 @@ export const listMessages = pikkuSessionlessFunc({
228
253
  // wirings/chat.channel.ts
229
254
  wireChannel({
230
255
  name: 'chat',
231
- onConnect: async ({ eventHub, channel }) => {
256
+ route: '/chat',
257
+ auth: true,
258
+ onConnect: async ({ eventHub }, _data, { channel }) => {
232
259
  eventHub.subscribe('chat:message', (data) => {
233
260
  channel.send(data)
234
261
  })
235
262
  },
236
- onDisconnect: async () => {},
237
263
  onMessageWiring: {
238
- auth: { func: authenticate, auth: false },
239
- send: { func: sendMessage },
240
- history: { func: listMessages, auth: false },
264
+ action: {
265
+ authenticate: { func: authenticate, auth: false },
266
+ send: { func: sendMessage },
267
+ history: { func: listMessages, auth: false },
268
+ },
241
269
  },
242
270
  })
243
271
  ```
@@ -121,8 +121,42 @@ await workflow.sleep('Wait 5 minutes', '5min')
121
121
 
122
122
  // Suspend — pause until externally resumed (e.g. awaiting approval), then continue
123
123
  await workflow.suspend('Awaiting approval')
124
+
125
+ // Approval — suspend for a human decision and resume with the answer
126
+ await workflow.approval('Manager sign-off', { ... })
124
127
  ```
125
128
 
129
+ `workflow.name`, `workflow.runId` and `await workflow.getRun()` identify the
130
+ current run if a step needs to reference it.
131
+
132
+ ### Error handling: `onError`, never try/catch
133
+
134
+ **Do not wrap steps in try/catch.** The DSL extractor serialises the body into a
135
+ step graph, and a `catch` block is control flow it cannot represent — so the
136
+ graph would no longer describe what actually runs, which is the whole point of
137
+ the DSL mode. This is a settled design decision, not a temporary limitation.
138
+
139
+ Use the `onError` step option instead: it names an RPC to invoke when the step
140
+ has failed *after* exhausting its retries.
141
+
142
+ ```typescript
143
+ await workflow.do('Charge', 'chargePayment', { orderId }, {
144
+ retries: 3,
145
+ retryDelay: '1s',
146
+ onError: 'refundReservation', // compensation RPC
147
+ })
148
+ ```
149
+
150
+ The handler receives `{ error: { message } }`, and the original error is still
151
+ thrown afterwards — so the workflow still fails. `onError` is **compensation, not
152
+ recovery**: it exists to undo work, not to swallow the failure and carry on. If
153
+ you genuinely need to branch on a failure, have the step return a result object
154
+ (`{ success: false, reason }`) and branch on that, the way the `processOrder`
155
+ example branches on `payment.success`.
156
+
157
+ Full step options: `description`, `retries`, `retryDelay`, `onError` (plus
158
+ `actor`, which is scenario-only — see `pikku-scenario`).
159
+
126
160
  ### Parallel fan-out
127
161
 
128
162
  ```typescript
@@ -161,7 +195,7 @@ export const userOnboarding = pikkuWorkflowGraph({
161
195
 
162
196
  ## Step dispatch & HTTP wiring
163
197
 
164
- For per-step inline-vs-queue dispatch (`inline: false` and the `dispatchStep` rules), the manual `workflowStart`/`workflow`/`workflowStatus` HTTP wirings, and a suspend/resume example, read `references/workflow-reference.md`.
198
+ For per-step inline-vs-queue dispatch (`workflowQueued: true` and the `dispatchStep` rules), the manual `workflowStart`/`workflow`/`workflowStatus` HTTP wirings, and a suspend/resume example, read `references/workflow-reference.md`.
165
199
 
166
200
  ## After writing
167
201
 
@@ -2,31 +2,36 @@
2
2
 
3
3
  ## Step execution: inline vs queue dispatch
4
4
 
5
- Whether a step runs **inline** (same process/session, no queue round-trip) or is **dispatched to the queue** is decided **purely by the step's function** — there is no workflow-level or per-call `inline` flag. `workflow.do(...)` options are only `retries`/`retryDelay`/`description`.
5
+ Whether a step runs **inline** (same process/session, no queue round-trip) or is **dispatched to the queue** is decided **purely by the step's function** — there is no workflow-level or per-call dispatch flag. `workflow.do(...)` options are only `description`/`retries`/`retryDelay`/`onError`.
6
6
 
7
7
  - **Steps default to inline.** Most steps don't need their own worker; running them inline avoids a queue round-trip per step, so a normally-started workflow executes its whole chain in one orchestrator pass.
8
- - **`inline: false` opts a function out.** Set `inline: false` on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits.
8
+ - **`workflowQueued: true` opts a function out.** Set it on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits. `workflowRetries` and `workflowTimeout` sit alongside it.
9
9
  - **Run-level `inline` is separate** and only controls whether the *whole run* executes in-process without queue infrastructure (set automatically when there is no `queueService`, or via `startWorkflow(..., { inline: true })`). It governs sleep handling, not per-step dispatch.
10
10
 
11
11
  The rule (`dispatchStep`):
12
12
 
13
- | Function `inline` | `queueService` present? | Result |
13
+ | Function `workflowQueued` | `queueService` present? | Result |
14
14
  |---|---|---|
15
- | default / `true` | any | **inline** |
16
- | `false` | yes | **queued** (own worker) |
17
- | `false` | no | **inline + a `logger.warn`** (misconfiguration: can't dispatch) |
15
+ | default / `false` | any | **inline** |
16
+ | `true` | yes | **queued** (own worker) |
17
+ | `true` | no | **throws** |
18
18
 
19
19
  ```typescript
20
20
  // Push this one expensive step onto the queue; every other step stays inline:
21
21
  export const renderLargeReport = pikkuSessionlessFunc({
22
- inline: false, // dispatch via queue instead of running inline
22
+ workflowQueued: true, // dispatch via queue instead of running inline
23
+ workflowRetries: 3,
24
+ workflowTimeout: '5m',
23
25
  input: ReportInput,
24
26
  output: ReportOutput,
25
27
  func: async (services, data) => { /* ... */ },
26
28
  })
27
29
  ```
28
30
 
29
- `inline: false` requires a `queueService`; without one the step still runs (so the workflow progresses) but emits a `logger.warn` so the misconfiguration is visible.
31
+ `workflowQueued: true` **requires** a `queueService`. Without one the step throws
32
+ rather than quietly running inline — a step marked for its own worker usually
33
+ carries timeout and concurrency expectations that inline execution would silently
34
+ violate, so failing loudly is safer than proceeding.
30
35
 
31
36
  ## HTTP workflow wiring (manual)
32
37
 
@@ -16,8 +16,8 @@ Use this skill as an execution checklist, not reference material.
16
16
  4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
17
17
  5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
18
18
 
19
- When a project has `pikkuWorkflowGraph` workflows, three React Query
20
- hooks are auto-generated alongside the standard RPC hooks. They handle
19
+ When a project defines any workflow — DSL or `pikkuWorkflowGraph` — three
20
+ React Query hooks are auto-generated alongside the standard RPC hooks. They handle
21
21
  the two common shapes: **run-and-wait** (short workflows where the
22
22
  client waits for the result) and **fire-and-poll** (long workflows where
23
23
  the client gets a `runId` and polls status).
@@ -103,8 +103,14 @@ function VideoStatus({ runId }: { runId: string }) {
103
103
 
104
104
  Status values: `'running' | 'suspended' | 'completed' | 'failed' | 'cancelled'`.
105
105
 
106
- The hook stops auto-polling when the run reaches a terminal state (set
107
- `refetchInterval` to false in those cases — pattern shown above).
106
+ **Stopping the poll is your job.** The hook adds no terminal-state logic of its
107
+ own — the `refetchInterval` callback above is what ends it, by returning `false`
108
+ once `status` is no longer `running`. Leave that out and a finished run keeps
109
+ being polled forever.
110
+
111
+ The hook is disabled until `runId` is set, so passing `undefined` while the run
112
+ has not started yet is the intended shape rather than something to guard around.
113
+ That `enabled` is owned by the hook and cannot be overridden through `options`.
108
114
 
109
115
  ## Putting it together — start + observe
110
116
 
@@ -140,8 +146,9 @@ docs).
140
146
 
141
147
  ## What NOT to do
142
148
 
143
- - Don't poll status manually — use `useWorkflowStatus` with
144
- `refetchInterval`. It dedupes and stops on terminal states.
149
+ - Don't poll status manually with `setInterval` — use `useWorkflowStatus`
150
+ with a `refetchInterval` callback, which dedupes across components and
151
+ lets you stop on a terminal state in one place.
145
152
  - Don't call `useRunWorkflow` for workflows that take more than a few
146
153
  seconds. The user-facing component will hold a long-running pending
147
154
  state with no progress indication; use start + status instead.
@@ -31,17 +31,53 @@ yarn add @pikku/ws ws
31
31
 
32
32
  ### Basic Setup
33
33
 
34
+ The package exports one function, `pikkuWebsocketHandler` — there is no server
35
+ class. You own the `http.Server` and the `WebSocketServer`; the handler attaches
36
+ the upgrade and message plumbing to them.
37
+
34
38
  ```typescript
35
- import { PikkuWSServer } from '@pikku/ws'
39
+ import { pikkuWebsocketHandler } from '@pikku/ws'
40
+ import { stopSingletonServices } from '@pikku/core'
41
+ import { Server } from 'http'
42
+ import { WebSocketServer } from 'ws'
43
+
44
+ import '../.pikku/pikku-bootstrap.gen.js'
45
+ import { createConfig, createSingletonServices } from './services.js'
46
+
47
+ const config = await createConfig()
48
+ const singletonServices = await createSingletonServices(config)
49
+
50
+ const server = new Server()
51
+ const wss = new WebSocketServer({ noServer: true })
36
52
 
37
- const wsServer = new PikkuWSServer({
38
- server: httpServer, // Node.js HTTP server
39
- singletonServices,
40
- createWireServices,
41
- channelStore,
53
+ pikkuWebsocketHandler({
54
+ server,
55
+ wss,
56
+ logger: singletonServices.logger,
57
+ logRoutes: true, // print the wired channels at startup
58
+ loadSchemas: true, // compile input schemas up front
42
59
  })
43
60
 
44
- await wsServer.init()
61
+ server.listen(4002, 'localhost')
45
62
  ```
46
63
 
47
- This runtime bridges the `ws` WebSocket library with Pikku's channel wiring. See `pikku-websocket` for channel wiring details and `pikku-deploy-fastify`/`pikku-deploy-express` for integrating with HTTP servers.
64
+ `noServer: true` is not optional decoration — the handler performs the upgrade
65
+ itself so it can run pikku's HTTP middleware chain (auth, cors) against the
66
+ upgrade request before a channel exists. Letting `ws` bind the server directly
67
+ would skip that.
68
+
69
+ Services come from the bootstrap import and the global singleton registry, which
70
+ is why nothing is passed in. The event hub is taken from
71
+ `singletonServices.eventHub` when it is a `LocalEventHubService`, and a local one
72
+ is created otherwise — so a single-process app gets pub/sub for free, while a
73
+ multi-instance deployment must register a distributed hub (see `pikku-realtime`).
74
+
75
+ The options type also extends `RunHTTPWiringOptions`, so per-request settings
76
+ such as `respondWith404`, `coerceDataFromSchema` and `bubbleErrors` are accepted
77
+ here too.
78
+
79
+ On shutdown, call `stopSingletonServices()` then close `wss` and `server`.
80
+
81
+ See `pikku-websocket` for channel wiring details, and
82
+ `pikku-deploy-fastify`/`pikku-deploy-express` when the WebSocket server shares a
83
+ port with an HTTP app.