@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.
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +56 -29
- 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 +80 -34
- package/skills/pikku-cli/SKILL.md +67 -18
- package/skills/pikku-cli/references/complete-example.md +2 -0
- package/skills/pikku-concepts/SKILL.md +82 -10
- package/skills/pikku-concepts/references/concept-mapping.md +2 -2
- package/skills/pikku-config/SKILL.md +134 -52
- 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 +30 -5
- package/skills/pikku-fabric-debug/SKILL.md +5 -1
- package/skills/pikku-feature/SKILL.md +12 -7
- 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 +3 -3
- 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 +285 -50
- 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-software-archaeology/README.md +16 -6
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +30 -30
- 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 +35 -1
- 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
|
@@ -34,20 +34,22 @@ See `pikku-concepts` for the core mental model.
|
|
|
34
34
|
|
|
35
35
|
## Function Versioning
|
|
36
36
|
|
|
37
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
**
|
|
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
|
|
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 #
|
|
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 #
|
|
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` —
|
|
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:
|
|
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
|
-
|
|
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
|
```
|
|
@@ -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 (`
|
|
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
|
|
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.
|