@pikku/skills 0.12.4 → 0.12.8
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/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +74 -33
- package/skills/pikku-ai-agent/SKILL.md +197 -105
- package/skills/pikku-ai-vercel/SKILL.md +57 -18
- package/skills/pikku-ai-voice/SKILL.md +126 -52
- package/skills/pikku-audit/SKILL.md +35 -13
- package/skills/pikku-aws/SKILL.md +66 -16
- package/skills/pikku-backblaze/SKILL.md +44 -11
- package/skills/pikku-better-auth/SKILL.md +45 -10
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +75 -10
- package/skills/pikku-config/SKILL.md +56 -14
- package/skills/pikku-cron/SKILL.md +13 -6
- package/skills/pikku-deploy-azure/SKILL.md +83 -28
- package/skills/pikku-deploy-cloudflare/SKILL.md +79 -37
- package/skills/pikku-deploy-express/SKILL.md +40 -4
- package/skills/pikku-deploy-fastify/SKILL.md +22 -1
- package/skills/pikku-deploy-lambda/SKILL.md +99 -19
- package/skills/pikku-deploy-nextjs/SKILL.md +49 -5
- package/skills/pikku-deploy-uws/SKILL.md +54 -1
- package/skills/pikku-deps/SKILL.md +29 -8
- package/skills/pikku-emails/SKILL.md +36 -5
- package/skills/pikku-fabric/SKILL.md +27 -2
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +6 -1
- package/skills/pikku-gateway-slack/SKILL.md +72 -11
- package/skills/pikku-http/SKILL.md +18 -5
- package/skills/pikku-http/references/http-options.md +10 -5
- package/skills/pikku-i18n/SKILL.md +18 -7
- package/skills/pikku-info/SKILL.md +18 -8
- package/skills/pikku-jose/SKILL.md +35 -6
- package/skills/pikku-knowledge/SKILL.md +50 -7
- package/skills/pikku-kysely/SKILL.md +78 -15
- package/skills/pikku-machine-auth/SKILL.md +36 -1
- package/skills/pikku-mcp/SKILL.md +159 -149
- package/skills/pikku-middleware/SKILL.md +17 -5
- package/skills/pikku-mongodb/SKILL.md +10 -2
- package/skills/pikku-n8n-import/SKILL.md +14 -6
- package/skills/pikku-permissions/SKILL.md +102 -22
- package/skills/pikku-pino/SKILL.md +12 -4
- package/skills/pikku-product-second-opinion/SKILL.md +3 -3
- package/skills/pikku-queue/SKILL.md +45 -16
- package/skills/pikku-react/SKILL.md +41 -14
- package/skills/pikku-react-query/SKILL.md +14 -10
- package/skills/pikku-realtime/SKILL.md +44 -22
- package/skills/pikku-redis/SKILL.md +12 -3
- package/skills/pikku-rpc/SKILL.md +23 -12
- package/skills/pikku-rtl/SKILL.md +21 -17
- package/skills/pikku-scenario/SKILL.md +141 -76
- package/skills/pikku-schedule/SKILL.md +39 -6
- package/skills/pikku-schema-ajv/SKILL.md +24 -2
- package/skills/pikku-schema-cfworker/SKILL.md +22 -2
- package/skills/pikku-security/SKILL.md +54 -9
- package/skills/pikku-services/SKILL.md +49 -9
- package/skills/pikku-services/references/audit-wire-service.md +2 -1
- package/skills/pikku-template-clone/SKILL.md +10 -5
- package/skills/pikku-trigger/SKILL.md +50 -6
- package/skills/pikku-versioning/SKILL.md +46 -17
- package/skills/pikku-websocket/SKILL.md +72 -44
- package/skills/pikku-workflow/SKILL.md +123 -11
- package/skills/pikku-workflow/references/workflow-reference.md +13 -8
- package/skills/pikku-workflows-client/SKILL.md +13 -6
- package/skills/pikku-ws/SKILL.md +44 -8
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
82
|
-
onDisconnect: async () => {},
|
|
95
|
+
route: '/todos',
|
|
83
96
|
onMessageWiring: {
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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 =
|
|
115
|
+
const authenticate = pikkuSessionlessFunc({
|
|
96
116
|
title: 'Authenticate',
|
|
97
|
-
|
|
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
|
-
|
|
107
|
-
|
|
127
|
+
route: '/todos',
|
|
128
|
+
auth: true,
|
|
108
129
|
onMessageWiring: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 '
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
```
|
|
@@ -29,11 +29,11 @@ Build durable, multi-step workflows with automatic retry, sleep, suspend/resume,
|
|
|
29
29
|
|
|
30
30
|
## Choosing the right factory
|
|
31
31
|
|
|
32
|
-
| Factory
|
|
33
|
-
|
|
34
|
-
| `pikkuWorkflowFunc`
|
|
35
|
-
| `pikkuWorkflowGraph`
|
|
36
|
-
| `pikkuWorkflowComplexFunc` | Escape hatch only — arbitrary TypeScript, no top-level restriction (e.g. dynamic inline functions the DSL extractor cannot handle).
|
|
32
|
+
| Factory | When to use | Step-graph view? |
|
|
33
|
+
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
34
|
+
| `pikkuWorkflowFunc` | **Default for all new workflows.** Sequential + conditional logic; DSL mode (serialisable, replay-safe). ALL `const`/`let` declarations must be at the top level of the function body (not inside blocks). | ✅ Yes |
|
|
35
|
+
| `pikkuWorkflowGraph` | DAG / fan-out with nodes and typed refs between them. | ✅ Yes |
|
|
36
|
+
| `pikkuWorkflowComplexFunc` | Escape hatch only — arbitrary TypeScript, no top-level restriction (e.g. dynamic inline functions the DSL extractor cannot handle). | ❌ No (loses step-graph view) |
|
|
37
37
|
|
|
38
38
|
**Default to `pikkuWorkflowFunc`.** Use `pikkuWorkflowGraph` ONLY with explicit user approval AND only for a genuine cyclic dependency or Node.js-only import DSL cannot express. Use `pikkuWorkflowComplexFunc` ONLY with explicit user approval — a last-resort escape hatch. Never switch to either just to dodge a PKU641 error; restructure the code instead.
|
|
39
39
|
|
|
@@ -58,7 +58,11 @@ if (priority === 'high') {
|
|
|
58
58
|
|
|
59
59
|
```typescript
|
|
60
60
|
// CORRECT — workflow factories come from the generated types file
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
pikkuWorkflowFunc,
|
|
63
|
+
pikkuWorkflowGraph,
|
|
64
|
+
pikkuWorkflowComplexFunc,
|
|
65
|
+
} from '#pikku/workflow/pikku-workflow-types.gen.js'
|
|
62
66
|
|
|
63
67
|
// WRONG — '#pikku' does not re-export them (TS2305)
|
|
64
68
|
import { pikkuWorkflowFunc } from '#pikku'
|
|
@@ -73,7 +77,10 @@ import { z } from 'zod'
|
|
|
73
77
|
import { pikkuWorkflowFunc } from '#pikku/workflow/pikku-workflow-types.gen.js'
|
|
74
78
|
|
|
75
79
|
const ProcessOrderInput = z.object({ orderId: z.string(), amount: z.number() })
|
|
76
|
-
const ProcessOrderOutput = z.object({
|
|
80
|
+
const ProcessOrderOutput = z.object({
|
|
81
|
+
status: z.string(),
|
|
82
|
+
discount: z.number().optional(),
|
|
83
|
+
})
|
|
77
84
|
|
|
78
85
|
export const processOrder = pikkuWorkflowFunc({
|
|
79
86
|
description: 'Process an order through payment and fulfillment',
|
|
@@ -86,7 +93,9 @@ export const processOrder = pikkuWorkflowFunc({
|
|
|
86
93
|
let status: string
|
|
87
94
|
|
|
88
95
|
if (data.amount > 1000) {
|
|
89
|
-
const d = await workflow.do('Apply bulk discount', 'calcDiscount', {
|
|
96
|
+
const d = await workflow.do('Apply bulk discount', 'calcDiscount', {
|
|
97
|
+
amount: data.amount,
|
|
98
|
+
})
|
|
90
99
|
discount = d.discountPercent
|
|
91
100
|
}
|
|
92
101
|
|
|
@@ -121,13 +130,113 @@ await workflow.sleep('Wait 5 minutes', '5min')
|
|
|
121
130
|
|
|
122
131
|
// Suspend — pause until externally resumed (e.g. awaiting approval), then continue
|
|
123
132
|
await workflow.suspend('Awaiting approval')
|
|
133
|
+
|
|
134
|
+
// Approval — suspend for a human decision and resume with the answer
|
|
135
|
+
await workflow.approval('Manager sign-off', { ... })
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`workflow.name`, `workflow.runId` and `await workflow.getRun()` identify the
|
|
139
|
+
current run if a step needs to reference it.
|
|
140
|
+
|
|
141
|
+
### Approval gates: who may answer
|
|
142
|
+
|
|
143
|
+
`workflow.approval(reason, options)` takes a `schema` (a runtime value — the
|
|
144
|
+
payload arrives from an untrusted caller, so a type generic would validate
|
|
145
|
+
nothing), an optional `expiry`, and an optional policy for **who** may answer:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const signOff = await workflow.approval('Manager sign-off', {
|
|
149
|
+
schema: SignOffSchema,
|
|
150
|
+
expiry: '3d',
|
|
151
|
+
approvers: 'not-initiator', // four-eyes: anyone but whoever started the run
|
|
152
|
+
approverScope: 'payments:approve', // and they must hold this scope
|
|
153
|
+
})
|
|
154
|
+
if (signOff.status === 'expired') { ... }
|
|
124
155
|
```
|
|
125
156
|
|
|
157
|
+
`approvers` is one of:
|
|
158
|
+
|
|
159
|
+
| value | who may answer |
|
|
160
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
161
|
+
| `any` _(default)_ | anyone the approve entrypoint admits — the gate is a pause for a decision, not an authorization boundary |
|
|
162
|
+
| `owner` | only the user who started the run |
|
|
163
|
+
| `not-initiator` | anyone **except** the user who started the run |
|
|
164
|
+
|
|
165
|
+
Both options are enforced in two phases, because a decision can legitimately
|
|
166
|
+
arrive before the run has reached the gate:
|
|
167
|
+
|
|
168
|
+
- **At submission**, if the run has already reached the gate. Reaching it
|
|
169
|
+
publishes the policy into the run state, so the approve entrypoint can judge
|
|
170
|
+
the caller against it and refuse with a **403**.
|
|
171
|
+
- **On replay**, for a decision that arrived before the gate — there was no
|
|
172
|
+
policy to judge it against yet, so it is accepted and judged when the workflow
|
|
173
|
+
reaches the gate. Failing there discards the decision and leaves the gate
|
|
174
|
+
closed, exactly as a decision that fails the schema does.
|
|
175
|
+
|
|
176
|
+
So the same rejected decision surfaces as an HTTP error or as a silently
|
|
177
|
+
re-closed gate depending on timing. Both are audited.
|
|
178
|
+
|
|
179
|
+
A gate declaring neither option accepts a decision from anyone the approve
|
|
180
|
+
route lets through; gate the route with `auth`/`permissions` to narrow that.
|
|
181
|
+
|
|
182
|
+
#### What survives the run
|
|
183
|
+
|
|
184
|
+
A settled decision carries `decidedBy` and `decidedAt`, so the answer keeps its
|
|
185
|
+
provenance in the step result:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
if (signOff.status === 'decided') {
|
|
189
|
+
logger.info(`signed by ${signOff.decidedBy?.userId} at ${signOff.decidedAt}`)
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
That record is deleted with the run, though — `deleteRun` cascades to steps and
|
|
194
|
+
history — and an attempt that was _refused_ never reaches a step at all. So
|
|
195
|
+
every answer is also written to the audit sink as `workflow.approval.decided`,
|
|
196
|
+
with `outcome: 'success' | 'denied'`, the decider under `userIdentity`, and the
|
|
197
|
+
run, reason and refusal in `metadata`. Wire an `audit` service to keep it; a
|
|
198
|
+
project without one records nothing and is otherwise unaffected.
|
|
199
|
+
|
|
200
|
+
### Error handling: `onError`, never try/catch
|
|
201
|
+
|
|
202
|
+
**Do not wrap steps in try/catch.** The DSL extractor serialises the body into a
|
|
203
|
+
step graph, and a `catch` block is control flow it cannot represent — so the
|
|
204
|
+
graph would no longer describe what actually runs, which is the whole point of
|
|
205
|
+
the DSL mode. This is a settled design decision, not a temporary limitation.
|
|
206
|
+
|
|
207
|
+
Use the `onError` step option instead: it names an RPC to invoke when the step
|
|
208
|
+
has failed _after_ exhausting its retries.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
await workflow.do(
|
|
212
|
+
'Charge',
|
|
213
|
+
'chargePayment',
|
|
214
|
+
{ orderId },
|
|
215
|
+
{
|
|
216
|
+
retries: 3,
|
|
217
|
+
retryDelay: '1s',
|
|
218
|
+
onError: 'refundReservation', // compensation RPC
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The handler receives `{ error: { message } }`, and the original error is still
|
|
224
|
+
thrown afterwards — so the workflow still fails. `onError` is **compensation, not
|
|
225
|
+
recovery**: it exists to undo work, not to swallow the failure and carry on. If
|
|
226
|
+
you genuinely need to branch on a failure, have the step return a result object
|
|
227
|
+
(`{ success: false, reason }`) and branch on that, the way the `processOrder`
|
|
228
|
+
example branches on `payment.success`.
|
|
229
|
+
|
|
230
|
+
Full step options: `description`, `retries`, `retryDelay`, `onError` (plus
|
|
231
|
+
`actor`, which is scenario-only — see `pikku-scenario`).
|
|
232
|
+
|
|
126
233
|
### Parallel fan-out
|
|
127
234
|
|
|
128
235
|
```typescript
|
|
129
236
|
const users = await Promise.all(
|
|
130
|
-
data.userIds.map((userId) =>
|
|
237
|
+
data.userIds.map((userId) =>
|
|
238
|
+
workflow.do(`Fetch user ${userId}`, 'getUser', { userId })
|
|
239
|
+
)
|
|
131
240
|
)
|
|
132
241
|
```
|
|
133
242
|
|
|
@@ -148,7 +257,10 @@ export const userOnboarding = pikkuWorkflowGraph({
|
|
|
148
257
|
config: {
|
|
149
258
|
createProfile: { next: ['sendWelcome', 'setupDefaults'] }, // run in parallel
|
|
150
259
|
sendWelcome: {
|
|
151
|
-
input: (ref) => ({
|
|
260
|
+
input: (ref) => ({
|
|
261
|
+
to: ref('createProfile', 'email'),
|
|
262
|
+
subject: 'Welcome!',
|
|
263
|
+
}),
|
|
152
264
|
},
|
|
153
265
|
},
|
|
154
266
|
})
|
|
@@ -161,7 +273,7 @@ export const userOnboarding = pikkuWorkflowGraph({
|
|
|
161
273
|
|
|
162
274
|
## Step dispatch & HTTP wiring
|
|
163
275
|
|
|
164
|
-
For per-step inline-vs-queue dispatch (`
|
|
276
|
+
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
277
|
|
|
166
278
|
## After writing
|
|
167
279
|
|
|
@@ -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
|
|
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
|
-
- **`
|
|
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 `
|
|
13
|
+
| Function `workflowQueued` | `queueService` present? | Result |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| default / `
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
107
|
-
`refetchInterval`
|
|
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`
|
|
144
|
-
`refetchInterval
|
|
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.
|
package/skills/pikku-ws/SKILL.md
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
38
|
-
server
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
61
|
+
server.listen(4002, 'localhost')
|
|
45
62
|
```
|
|
46
63
|
|
|
47
|
-
|
|
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.
|