@pikku/skills 0.12.22 → 0.12.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-a11y/SKILL.md +59 -0
- package/skills/pikku-addon/SKILL.md +2 -2
- package/skills/pikku-agent/SKILL.md +67 -316
- package/skills/pikku-agent/references/agents.md +299 -0
- package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
- package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
- package/skills/pikku-architect/SKILL.md +265 -0
- package/skills/pikku-auth/SKILL.md +89 -0
- package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +126 -34
- package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
- package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
- package/skills/pikku-auth/references/permissions.md +261 -0
- package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
- package/skills/pikku-build/SKILL.md +88 -0
- package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +76 -24
- package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
- package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +56 -1
- package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
- package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
- package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
- package/skills/{pikku-build-app → pikku-build}/references/ship.md +9 -3
- package/skills/pikku-concepts/SKILL.md +72 -7
- package/skills/pikku-concepts/references/concept-mapping.md +8 -8
- package/skills/pikku-deploy/SKILL.md +158 -0
- package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
- package/skills/pikku-deploy/references/cloudflare.md +104 -0
- package/skills/pikku-deploy/references/express.md +92 -0
- package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
- package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
- package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
- package/skills/pikku-deploy/references/uws.md +72 -0
- package/skills/pikku-deploy/references/ws.md +75 -0
- package/skills/pikku-emails/SKILL.md +3 -2
- package/skills/pikku-fabric/SKILL.md +47 -20
- package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
- package/skills/pikku-i18n/SKILL.md +62 -207
- package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
- package/skills/pikku-i18n/references/messages.md +218 -0
- package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
- package/skills/pikku-knowledge/SKILL.md +15 -0
- package/skills/pikku-kysely/SKILL.md +13 -13
- package/skills/pikku-list-query/SKILL.md +163 -0
- package/skills/pikku-meta/SKILL.md +58 -130
- package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
- package/skills/pikku-meta/references/meta.md +114 -0
- package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-n8n-import/SKILL.md +0 -1
- package/skills/pikku-permissions/SKILL.md +75 -229
- package/skills/pikku-react/SKILL.md +50 -298
- package/skills/pikku-react/references/client.md +313 -0
- package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
- package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
- package/skills/pikku-realtime/SKILL.md +110 -251
- package/skills/pikku-scenario/SKILL.md +60 -45
- package/skills/pikku-scenario/references/persona-run.md +148 -0
- package/skills/pikku-seo/SKILL.md +133 -0
- package/skills/pikku-service-backends/SKILL.md +154 -0
- package/skills/pikku-service-backends/references/aws.md +106 -0
- package/skills/pikku-service-backends/references/backblaze.md +57 -0
- package/skills/pikku-service-backends/references/mongodb.md +90 -0
- package/skills/pikku-service-backends/references/redis.md +75 -0
- package/skills/pikku-service-backends/references/schema.md +63 -0
- package/skills/pikku-services/SKILL.md +68 -291
- package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
- package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
- package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
- package/skills/pikku-services/references/services.md +272 -0
- package/skills/pikku-software-archaeology/README.md +5 -1
- package/skills/pikku-software-archaeology/SKILL.md +16 -2
- package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
- package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
- package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
- package/skills/pikku-webhook/SKILL.md +224 -0
- package/skills/pikku-wiring/SKILL.md +180 -0
- package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
- package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
- package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
- package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
- package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
- package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
- package/skills/pikku-wiring/references/realtime.md +265 -0
- package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
- package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
- package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
- package/skills/pikku-workflow/SKILL.md +39 -2
- package/skills/pikku-aws/SKILL.md +0 -161
- package/skills/pikku-backblaze/SKILL.md +0 -104
- package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
- package/skills/pikku-deploy-express/SKILL.md +0 -122
- package/skills/pikku-deploy-uws/SKILL.md +0 -144
- package/skills/pikku-mongodb/SKILL.md +0 -113
- package/skills/pikku-product-second-opinion/README.md +0 -43
- package/skills/pikku-redis/SKILL.md +0 -99
- package/skills/pikku-schema-ajv/SKILL.md +0 -83
- package/skills/pikku-schema-cfworker/SKILL.md +0 -82
- package/skills/pikku-ws/SKILL.md +0 -87
- /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
- /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
- /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
- /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
- /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
|
@@ -1,25 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-react-query
|
|
3
|
-
description: 'Use the Pikku auto-generated React Query hooks (`usePikkuQuery`, `usePikkuMutation`, `usePikkuInfiniteQuery`) to call backend RPC functions from a React frontend with full type safety. TRIGGER when: writing React components that need to call a Pikku function, fetch data, mutate data, or paginate; user mentions React Query, useQuery, useMutation, or building a frontend that talks to a Pikku backend. DO NOT TRIGGER when: working on the backend (use pikku-rpc / pikku-feature) or wiring a non-React frontend.'
|
|
4
|
-
installGroups: [client, fabric]
|
|
5
|
-
---
|
|
6
|
-
|
|
7
1
|
# Pikku React Query Hooks
|
|
8
2
|
|
|
9
|
-
## Agent Operating Procedure
|
|
10
|
-
|
|
11
|
-
Use this skill as an execution checklist, not reference material.
|
|
12
|
-
|
|
13
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
14
|
-
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
|
|
15
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
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
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
18
|
-
|
|
19
|
-
Pikku generates a typed React Query layer from your backend `expose: true`
|
|
20
|
-
functions. You don't write `useQuery`/`useMutation` against `fetch`
|
|
21
|
-
yourself — you call hooks named after RPCs and get full type inference for
|
|
22
|
-
input + output.
|
|
23
3
|
|
|
24
4
|
## Discover what's available on the client
|
|
25
5
|
|
|
@@ -77,7 +57,7 @@ The two generated files come from `pikku.config.json`'s
|
|
|
77
57
|
`clientFiles.fetchFile` and `clientFiles.rpcWiringsFile`. Hooks live in
|
|
78
58
|
the file at `clientFiles.reactQueryFile` (typically `api.gen.ts`).
|
|
79
59
|
|
|
80
|
-
`apiUrl()` is the shared server-URL helper — see
|
|
60
|
+
`apiUrl()` is the shared server-URL helper — see `references/client.md`. Never
|
|
81
61
|
inline `?? 'http://localhost:3000'`: a deploy that supplies the URL as a
|
|
82
62
|
runtime binding leaves `import.meta.env.VITE_API_URL` undefined in the
|
|
83
63
|
bundle, so the fallback is the branch that actually runs.
|
|
@@ -206,7 +186,7 @@ instead.
|
|
|
206
186
|
When the project defines any workflow, the same file also gains
|
|
207
187
|
`useStartWorkflow(name)` (mutation → `{ runId }`), `useRunWorkflow(name)`
|
|
208
188
|
(mutation → the workflow's output) and `useWorkflowStatus(name, runId?)` (query,
|
|
209
|
-
disabled until `runId` is set). See
|
|
189
|
+
disabled until `runId` is set). See `references/workflows.md`.
|
|
210
190
|
|
|
211
191
|
## Calling RPCs without React Query
|
|
212
192
|
|
|
@@ -1,26 +1,5 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pikku-workflows-client
|
|
3
|
-
description: 'Run Pikku workflows from a React frontend and track their progress. Covers `useRunWorkflow` (run-and-wait), `useStartWorkflow` (fire-and-poll), and `useWorkflowStatus` (live status). TRIGGER when: a React component needs to invoke or display the status of a Pikku workflow, the user mentions long-running tasks / background jobs / progress UI tied to a workflow, or asks how to start/track a workflow from the client. DO NOT TRIGGER when: the user is wiring the workflow itself (use pikku-workflow) or only making regular RPC calls (use pikku-react-query).'
|
|
4
|
-
installGroups: [client]
|
|
5
|
-
---
|
|
6
|
-
|
|
7
1
|
# Pikku Workflows — Client Hooks
|
|
8
2
|
|
|
9
|
-
## Agent Operating Procedure
|
|
10
|
-
|
|
11
|
-
Use this skill as an execution checklist, not reference material.
|
|
12
|
-
|
|
13
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
14
|
-
2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
|
|
15
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
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
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
18
|
-
|
|
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
|
-
the two common shapes: **run-and-wait** (short workflows where the
|
|
22
|
-
client waits for the result) and **fire-and-poll** (long workflows where
|
|
23
|
-
the client gets a `runId` and polls status).
|
|
24
3
|
|
|
25
4
|
## Discover what workflows exist
|
|
26
5
|
|
|
@@ -36,7 +15,7 @@ below.
|
|
|
36
15
|
|
|
37
16
|
These hooks are generated into the same `api.gen.ts` as `usePikkuQuery` —
|
|
38
17
|
no extra setup beyond `PikkuProvider` + `QueryClientProvider` (see the
|
|
39
|
-
|
|
18
|
+
`references/client.md` and `references/react-query.md`).
|
|
40
19
|
|
|
41
20
|
## `useRunWorkflow(name, options?)` — run and wait
|
|
42
21
|
|
|
@@ -1,288 +1,147 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pikku-realtime
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Use when making ANY view live/realtime in a Pikku app — a board, shared list, dashboard, ticker, bidding room, live count — or when adding two-way chat/presence. Covers the DEFAULT event-hub SSE path and the two-way WebSocket channel.
|
|
5
|
+
TRIGGER when: the user wants live updates, realtime, "update without refresh", a live board/feed/ticker/room, presence, or chat; or when data that MORE THAN ONE signed-in user can change should reflect others
|
|
6
|
+
DO NOT TRIGGER when: a plain one-shot query/refetch is fine (data only one user changes, or a manual refresh is acceptable), or for background jobs (that is pikku-schedule/pikku-workflow).
|
|
7
|
+
installGroups: [core, client]
|
|
4
8
|
---
|
|
5
9
|
|
|
6
|
-
# Pikku Realtime
|
|
10
|
+
# Pikku Realtime (SSE + WebSocket channels)
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
There is NOTHING to hand-roll and NOTHING to "find". The event-hub SSE transport
|
|
13
|
+
is already wired into every app, and the two patterns below ARE the realtime
|
|
14
|
+
templates. Start from them and rename — never grep the project for existing
|
|
15
|
+
`sse`/`eventHub` code to copy, never write a custom `EventSource`, and never
|
|
16
|
+
write a bespoke `sse: true` route for a plain live feed.
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
## Pick the transport (almost always SSE)
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
- **Server → client live updates → SSE via the event-hub.** This is the DEFAULT
|
|
21
|
+
for making any view live: a board, list, dashboard, ticker, feed, or a "room"
|
|
22
|
+
(a bidding room, sale room, live auction). The client only RECEIVES — the
|
|
23
|
+
change itself happens through a NORMAL HTTP RPC (`placeBid`, `updateLot`, …)
|
|
24
|
+
that publishes the new row.
|
|
25
|
+
- **Client → server push mid-session → a WebSocket channel.** ONLY when the
|
|
26
|
+
BROWSER must send up the socket without a page action: live chat messages,
|
|
27
|
+
typing indicators, cursors/presence.
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
and the same publish call, so choose by transport, not by code shape:
|
|
29
|
+
A screen being called a "room", or being multi-user, or being live is NOT a
|
|
30
|
+
reason to use a channel. If the browser isn't pushing frames up, it's SSE.
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
- **SSE** at `GET /events/:topic` — one connection per topic, auto-cleanup on
|
|
24
|
-
disconnect. Good when WebSocket is blocked or for trivially streaming one topic.
|
|
32
|
+
## Level 1 — live updates (event-hub SSE, the default)
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
Two halves; both are required or nothing arrives.
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
**Backend — publish after every write.** In each create/update/status function,
|
|
37
|
+
AFTER the DB write, publish the changed row on a topic:
|
|
29
38
|
|
|
30
39
|
```ts
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Reference it in `application-types.d.ts` and instantiate it in `services.ts`:
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
// application-types.d.ts
|
|
44
|
-
import type { EventHubService } from '@pikku/core/channel'
|
|
45
|
-
import type { EventHubTopics } from './eventhub-topics.js'
|
|
46
|
-
|
|
47
|
-
export interface SingletonServices extends CoreSingletonServices<Config> {
|
|
48
|
-
// `CoreSingletonServices` declares eventHub optional; re-declare it required
|
|
49
|
-
// so functions can use it without a `if (eventHub)` guard on every publish.
|
|
50
|
-
eventHub: EventHubService<EventHubTopics>
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// services.ts
|
|
54
|
-
import { LocalEventHubService } from '@pikku/core/channel'
|
|
55
|
-
const eventHub = new LocalEventHubService<EventHubTopics>()
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
For multi-instance deployments use `CloudflareEventHubService` /
|
|
59
|
-
`LambdaEventHubService` / `UWSEventHubService` instead — same interface.
|
|
60
|
-
|
|
61
|
-
If a deployment genuinely has no eventHub, that belongs in `services.ts` (don't
|
|
62
|
-
create the service there), not as an optional type every function has to guard —
|
|
63
|
-
see `pikku-services`.
|
|
64
|
-
|
|
65
|
-
## 2. Enable the server side
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
yarn pikku enable events
|
|
40
|
+
const lot = await kysely
|
|
41
|
+
.updateTable('lot')
|
|
42
|
+
.set({ status: 'sold' })
|
|
43
|
+
.where('id', '=', input.lotId)
|
|
44
|
+
.returning(['id', 'status', 'currentBid', 'updatedAt'])
|
|
45
|
+
.executeTakeFirstOrThrow()
|
|
46
|
+
await eventHub.publish('lot-updated', null, { topic: 'lot-updated', data: lot })
|
|
47
|
+
return lot
|
|
69
48
|
```
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
50
|
+
**A topic is PUBLIC — publish a projection, never `returningAll()`.** The generated
|
|
51
|
+
`/events/:topic` route is wired `auth: false` with a sessionless handler, so anyone who can
|
|
52
|
+
reach the origin can subscribe to any topic name and read every frame on it. `returningAll()`
|
|
53
|
+
then ships the whole row — `reservePrice`, `sellerId`, internal notes, whatever the table
|
|
54
|
+
grows next — to unauthenticated subscribers, and it does it silently because the RPC's own
|
|
55
|
+
`output` schema never sees the event payload. List the columns the topic is FOR, the way the
|
|
56
|
+
example does. If a change genuinely has per-viewer content, it does not belong on a topic:
|
|
57
|
+
publish an id-only "something changed" frame and let each client refetch through an
|
|
58
|
+
authenticated RPC that applies its own permissions.
|
|
59
|
+
|
|
60
|
+
The **2nd arg is the channel to EXCLUDE** from the broadcast: pass `null` from a
|
|
61
|
+
normal HTTP/RPC write (there is no one to skip); pass `channel.channelId` ONLY
|
|
62
|
+
when you publish from INSIDE a channel handler, or the sender gets an echo of its
|
|
63
|
+
own update. `eventHub` is already injected — do not wire it.
|
|
64
|
+
|
|
65
|
+
**Frontend — subscribe over SSE.** The generated
|
|
66
|
+
`PikkuRealtime.subscribeToTopic(topic, handler)` opens an SSE stream to the
|
|
67
|
+
built-in `/events/:topic` route. Seed state from a normal query, then patch it as
|
|
68
|
+
events arrive; the event is the `{ topic, data }` envelope, so read `.data`.
|
|
79
69
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{
|
|
84
|
-
"clientFiles": {
|
|
85
|
-
"realtimeFile": "packages/sdk/src/pikku/realtime.gen.ts",
|
|
86
|
-
// Optional: full type inference for subscribe/unsubscribe
|
|
87
|
-
"realtimeEventHubTopicsImport": "../../../functions/types/eventhub-topics.js#EventHubTopics",
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Run `pikku all` (or `pikku realtime` to regenerate just this file). Everything is
|
|
93
|
-
on one class — both transports are methods, so switching from WebSocket to SSE is
|
|
94
|
-
a one-word change, not a different import:
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
export class PikkuRealtime {
|
|
98
|
-
constructor(options?: {
|
|
99
|
-
reconnect?: boolean
|
|
100
|
-
reconnectDelayMs?: number
|
|
101
|
-
reconnectMaxDelayMs?: number
|
|
102
|
-
})
|
|
103
|
-
setPikkuFetch(fetch: PikkuFetch): void // server URL + auth come from here, not the constructor
|
|
104
|
-
|
|
105
|
-
// WebSocket at /events — many topics on one connection
|
|
106
|
-
subscribe<K extends keyof EventHubTopics>(
|
|
107
|
-
topic: K,
|
|
108
|
-
handler: (data: EventHubTopics[K]) => void
|
|
109
|
-
): () => void
|
|
110
|
-
unsubscribe<K extends keyof EventHubTopics>(
|
|
111
|
-
topic: K,
|
|
112
|
-
handler?: (data: EventHubTopics[K]) => void
|
|
113
|
-
): void
|
|
114
|
-
|
|
115
|
-
// SSE at GET /events/:topic — one EventSource per topic
|
|
116
|
-
subscribeToTopic<K extends keyof EventHubTopics>(
|
|
117
|
-
topic: K,
|
|
118
|
-
handler: (data: EventHubTopics[K]) => void
|
|
119
|
-
): { close: () => void }
|
|
70
|
+
```tsx
|
|
71
|
+
import { useEffect } from 'react'
|
|
72
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
73
|
+
import { realtime } from '../lib/pikku'
|
|
120
74
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
path: string,
|
|
124
|
-
handler: (data: T) => void
|
|
125
|
-
): { close: () => void }
|
|
126
|
-
connectToChannel(
|
|
127
|
-
channelRoute: string,
|
|
128
|
-
protocols?: string | string[]
|
|
129
|
-
): WebSocket
|
|
75
|
+
export function useLiveLots() {
|
|
76
|
+
const queryClient = useQueryClient()
|
|
130
77
|
|
|
131
|
-
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const subscription = realtime.subscribeToTopic('lot-updated', () => {
|
|
80
|
+
queryClient.invalidateQueries({ queryKey: ['listLots'] })
|
|
81
|
+
})
|
|
82
|
+
return () => subscription.close()
|
|
83
|
+
}, [queryClient])
|
|
132
84
|
}
|
|
133
85
|
```
|
|
134
86
|
|
|
135
|
-
|
|
136
|
-
`
|
|
137
|
-
|
|
87
|
+
**Invalidate; do not hand-patch the cache.** The generated hooks key a query as
|
|
88
|
+
`[name, input]` — `['listLots', { status: 'open', cursor: undefined }]`, one entry per set of
|
|
89
|
+
arguments — so `setQueryData(['listLots'], …)` writes to a key nothing reads and the screen
|
|
90
|
+
never changes. `invalidateQueries({ queryKey: ['listLots'] })` prefix-matches, so it refreshes
|
|
91
|
+
every variant of that list whatever input each one was fetched with.
|
|
138
92
|
|
|
139
|
-
|
|
93
|
+
Patching also has to know the payload's shape, and a list RPC returns
|
|
94
|
+
`ListOutput<Lot>` — `{ rows, nextCursor, totalCount? }`, not `Lot[]` — so a `rows.map(...)`
|
|
95
|
+
updater is reading `.map` off an object. Refetching sidesteps both, and it re-applies the server's own filtering,
|
|
96
|
+
which a locally patched row does not: a lot that just moved to `sold` may no longer belong in
|
|
97
|
+
an "open lots" list at all.
|
|
140
98
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
publish(topic, channelId: string | null, data, isBinary?)
|
|
146
|
-
```
|
|
99
|
+
`subscribeToTopic` returns `{ close }` — ALWAYS close on unmount or you leak the
|
|
100
|
+
stream. Never hand-roll an `EventSource`.
|
|
147
101
|
|
|
148
|
-
|
|
149
|
-
`null` to reach every subscriber, or the current `channel.channelId` when the
|
|
150
|
-
originating connection has already applied the change locally and would otherwise
|
|
151
|
-
render it twice.
|
|
102
|
+
## Level 2 — two-way channel
|
|
152
103
|
|
|
153
|
-
|
|
154
|
-
`
|
|
104
|
+
Only when the client pushes up the socket. The backend channel lives in its own
|
|
105
|
+
`*.channel.ts` with `onConnect`/`onMessage` handlers:
|
|
155
106
|
|
|
156
107
|
```ts
|
|
157
|
-
import {
|
|
108
|
+
import { pikkuChannelFunc, wireChannel } from '#pikku/channel'
|
|
158
109
|
|
|
159
|
-
export const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.insertInto('todos')
|
|
165
|
-
.values(data)
|
|
166
|
-
.returningAll()
|
|
167
|
-
.executeTakeFirstOrThrow()
|
|
168
|
-
|
|
169
|
-
await eventHub.publish('todo-created', null, {
|
|
170
|
-
topic: 'todo-created',
|
|
171
|
-
data: { todo },
|
|
172
|
-
})
|
|
173
|
-
return { id: todo.id }
|
|
110
|
+
export const onMessage = pikkuChannelFunc<{ text: string }>({
|
|
111
|
+
func: async ({ eventHub }, input, { channel, session }) => {
|
|
112
|
+
const message = { id: crypto.randomUUID(), text: input.text, userId: session!.userId }
|
|
113
|
+
await eventHub.publish('room', channel.channelId, { topic: 'room', data: message })
|
|
114
|
+
return message
|
|
174
115
|
},
|
|
175
116
|
})
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
A thin helper removes the duplication:
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
async function publishEvent<K extends keyof EventHubTopics>(
|
|
182
|
-
hub: EventHubService<EventHubTopics>,
|
|
183
|
-
topic: K,
|
|
184
|
-
data: EventHubTopics[K]
|
|
185
|
-
) {
|
|
186
|
-
return hub.publish(topic, null, { topic, data })
|
|
187
|
-
}
|
|
188
|
-
// usage: await publishEvent(eventHub, 'todo-created', { todo })
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## 5. Wire it up — share fetch with PikkuRPC
|
|
192
|
-
|
|
193
|
-
`PikkuRealtime` mirrors `PikkuRPC`: it wraps the same `PikkuFetch`, so server URL +
|
|
194
|
-
auth are configured **once** and shared across HTTP, RPC, and realtime transports.
|
|
195
|
-
|
|
196
|
-
```tsx
|
|
197
|
-
import { createPikku, PikkuProvider } from '@pikku/react'
|
|
198
|
-
import { PikkuFetch } from './pikku/pikku-fetch.gen'
|
|
199
|
-
import { PikkuRPC } from './pikku/pikku-rpc.gen'
|
|
200
|
-
import { PikkuRealtime } from './pikku/realtime.gen'
|
|
201
|
-
|
|
202
|
-
const pikku = createPikku(
|
|
203
|
-
PikkuFetch,
|
|
204
|
-
PikkuRPC,
|
|
205
|
-
PikkuRealtime, // pass the realtime class as the third arg
|
|
206
|
-
{ serverUrl: apiUrl() } // shared env helper — see pikku-react
|
|
207
|
-
)
|
|
208
|
-
// pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch.
|
|
209
|
-
|
|
210
|
-
createRoot(document.getElementById('root')!).render(
|
|
211
|
-
<PikkuProvider pikku={pikku}>
|
|
212
|
-
<App />
|
|
213
|
-
</PikkuProvider>
|
|
214
|
-
)
|
|
215
|
-
```
|
|
216
117
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
const realtime = new PikkuRealtime()
|
|
221
|
-
realtime.setPikkuFetch(pikku.fetch) // inherits serverUrl + auth
|
|
118
|
+
wireChannel({ name: 'room', route: '/room', auth: true, onMessage })
|
|
222
119
|
```
|
|
223
120
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
Subscribe inside `useEffect` (never the render path, or you create a subscription
|
|
227
|
-
per render). `subscribe` returns an unsubscribe function; SSE's `subscribeToTopic`
|
|
228
|
-
returns a handle with `close()`:
|
|
121
|
+
The frontend opens it with `PikkuRealtime.connectToChannel(path)`, which returns
|
|
122
|
+
a socket you both `.send(...)` on and read via `onmessage`:
|
|
229
123
|
|
|
230
124
|
```tsx
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
// WebSocket multi-topic:
|
|
239
|
-
const off = realtime.subscribe('todo-created', ({ todo }) =>
|
|
240
|
-
setTodos((prev) => [...prev, todo])
|
|
241
|
-
)
|
|
242
|
-
return off
|
|
243
|
-
|
|
244
|
-
// Single-topic SSE (auto-cleanup on close) instead:
|
|
245
|
-
// const sub = realtime.subscribeToTopic('todo-created', ({ todo }) =>
|
|
246
|
-
// setTodos((prev) => [...prev, todo]))
|
|
247
|
-
// return () => sub.close()
|
|
248
|
-
}, [realtime])
|
|
249
|
-
|
|
250
|
-
return (
|
|
251
|
-
<ul>
|
|
252
|
-
{todos.map((t) => (
|
|
253
|
-
<li key={t.id}>{t.title}</li>
|
|
254
|
-
))}
|
|
255
|
-
</ul>
|
|
256
|
-
)
|
|
257
|
-
}
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const socket = realtime.connectToChannel('/room')
|
|
127
|
+
socket.onmessage = (event) => appendMessage(JSON.parse(event.data))
|
|
128
|
+
return () => socket.close()
|
|
129
|
+
}, [])
|
|
258
130
|
```
|
|
259
131
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
all topics for the dead channel id). Don't write manual cleanup unless you're
|
|
277
|
-
unsubscribing partway through a session.
|
|
278
|
-
|
|
279
|
-
## What NOT to do
|
|
280
|
-
|
|
281
|
-
- Don't call `eventHub.publish(topic, ..., rawData)` without the `{topic, data}`
|
|
282
|
-
envelope — clients use `topic` to dispatch handlers.
|
|
283
|
-
- Don't create your own `/events` channel by hand — `pikku enable events` already
|
|
284
|
-
does it correctly with disconnect cleanup.
|
|
285
|
-
- Don't subscribe inside the render path — use `useEffect`.
|
|
286
|
-
- Don't subscribe to topics that don't exist in `EventHubTopics`. The generated
|
|
287
|
-
client's types prevent it; if you reach for `as any` to subscribe to a string,
|
|
288
|
-
declare the topic first.
|
|
132
|
+
Publish server→client fan-out from a channel handler with
|
|
133
|
+
`eventHub.publish(topic, channel.channelId, envelope)` — the 2nd arg excludes the
|
|
134
|
+
sender, so the browser that sent the message does not receive its own echo.
|
|
135
|
+
|
|
136
|
+
## Do NOT
|
|
137
|
+
|
|
138
|
+
- Do **not** grep the project for existing SSE/eventHub infra to reverse-engineer
|
|
139
|
+
or copy — the patterns above ARE the template (same rule as never reading
|
|
140
|
+
`.gen.ts` to learn an API).
|
|
141
|
+
- Do **not** write a custom `sse: true` HTTP route or a bespoke `EventSource` for
|
|
142
|
+
an ordinary live feed — the event-hub covers it. (A dedicated `sse: true` route
|
|
143
|
+
is only for a long-job PROGRESS stream, and is not needed for an initial build.)
|
|
144
|
+
- Do **not** use a WebSocket channel for a live board/ticker/room — that is SSE.
|
|
145
|
+
A channel is for client→server push (chat/presence) ONLY.
|
|
146
|
+
- Do **not** forget the backend `eventHub.publish(...)` — a subscribed frontend
|
|
147
|
+
with no publisher is a silent, empty stream.
|