@pikku/skills 0.12.22 → 0.12.25
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 +101 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- 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 +264 -0
- package/skills/pikku-auth/SKILL.md +89 -0
- package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +1 -27
- 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-permissions/SKILL.md → pikku-auth/references/permissions.md} +1 -20
- package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
- package/skills/pikku-build/SKILL.md +87 -0
- package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +75 -23
- package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
- package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +1 -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 +7 -1
- 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 +12 -2
- package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
- package/skills/pikku-i18n/SKILL.md +60 -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 +14 -0
- package/skills/pikku-kysely/SKILL.md +13 -13
- 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-react/SKILL.md +50 -298
- package/skills/pikku-react/references/client.md +293 -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-scenario/SKILL.md +60 -45
- package/skills/pikku-scenario/references/persona-run.md +148 -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 +15 -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 +199 -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-realtime/SKILL.md → pikku-wiring/references/realtime.md} +2 -25
- 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 +2 -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,57 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
name: pikku-deploy-azure
|
|
3
|
-
description: >-
|
|
4
|
-
Use when deploying a Pikku app to Azure Functions. Covers createAzureHandler for HTTP, storage
|
|
5
|
-
queue and timer triggers, plus AzInvocationLogger and PikkuAZTimerRequest. TRIGGER when: user
|
|
6
|
-
asks about Azure Functions, Azure deployment, or @pikku/azure-functions. DO NOT TRIGGER when:
|
|
7
|
-
user asks about AWS Lambda (use pikku-deploy-lambda) or Cloudflare Workers (use
|
|
8
|
-
pikku-deploy-cloudflare).
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Pikku Azure Functions Deployment
|
|
12
|
-
|
|
13
|
-
## Agent Operating Procedure
|
|
14
|
-
|
|
15
|
-
Use this skill as an execution checklist, not reference material.
|
|
16
|
-
|
|
17
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
18
|
-
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.
|
|
19
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
20
|
-
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.
|
|
21
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
22
|
-
|
|
23
|
-
`@pikku/azure-functions` provides Azure Functions runtime adapters for Pikku.
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
1
|
+
# Azure Functions
|
|
26
2
|
|
|
27
3
|
```bash
|
|
28
4
|
yarn add @pikku/azure-functions @azure/functions
|
|
29
5
|
```
|
|
30
6
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- `createAzureWorkerHandler(factories)` — `createAzureHandler(factories, ['fetch'])`.
|
|
38
|
-
- `createAzureWebSocketHandler(factories)` — **a stub**: its `negotiate` always
|
|
39
|
-
answers `501 WebSocket via Azure Web PubSub not yet implemented`. Channels do
|
|
40
|
-
not work on Azure yet; do not plan a deployment around it.
|
|
41
|
-
- `AzInvocationLogger` — the logger. Note the name: there is no
|
|
42
|
-
`PikkuAzFunctionsLogger`.
|
|
43
|
-
- `PikkuAZTimerRequest` — `new PikkuAZTimerRequest(context, data)`, a
|
|
44
|
-
`PikkuRequest` carrying the data. The context argument is accepted and
|
|
45
|
-
ignored.
|
|
46
|
-
- `AzureQueueService`, `AzureDeploymentService`.
|
|
7
|
+
`createAzureHandler(factories, handlerTypes)` is the entry point, returning
|
|
8
|
+
`{ http?, queue?, timer? }` for the handler types you ask for.
|
|
9
|
+
`createAzureWorkerHandler(factories)` is `createAzureHandler(factories,
|
|
10
|
+
['fetch'])`. `factories` is `{ createConfig, createSingletonServices,
|
|
11
|
+
createPlatformServices? }`; services are built from `process.env` and cached in
|
|
12
|
+
module scope across invocations of the same instance.
|
|
47
13
|
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
14
|
+
**Channels do not work on Azure.** `createAzureWebSocketHandler` is a stub whose
|
|
15
|
+
`negotiate` always answers `501 WebSocket via Azure Web PubSub not yet
|
|
16
|
+
implemented` — do not plan a deployment around it.
|
|
51
17
|
|
|
52
|
-
|
|
18
|
+
Two naming traps: the logger is `AzInvocationLogger`, not
|
|
19
|
+
`PikkuAzFunctionsLogger`; and `PikkuAZTimerRequest(context, data)` accepts the
|
|
20
|
+
context argument and ignores it.
|
|
53
21
|
|
|
54
|
-
|
|
22
|
+
## Registering handlers
|
|
55
23
|
|
|
56
24
|
```typescript
|
|
57
25
|
import { app } from '@azure/functions'
|
|
@@ -86,7 +54,7 @@ app.timer('scheduler', {
|
|
|
86
54
|
Note the key names: `handlerTypes` uses **`scheduled`**, but the handler it
|
|
87
55
|
returns is **`timer`**.
|
|
88
56
|
|
|
89
|
-
|
|
57
|
+
## HTTP
|
|
90
58
|
|
|
91
59
|
The handler buffers the whole body, converts to a standard `Request`, and
|
|
92
60
|
returns the response body as **text** — a streaming or binary response is
|
|
@@ -95,7 +63,7 @@ flattened. It takes no `RunHTTPWiringOptions`, so there is no `maxBodySize` or
|
|
|
95
63
|
is logged to `console.error` and whatever the response already holds is
|
|
96
64
|
returned.
|
|
97
65
|
|
|
98
|
-
|
|
66
|
+
## Queue
|
|
99
67
|
|
|
100
68
|
The queue name comes from the message's own `queueName`, falling back to
|
|
101
69
|
`context.triggerMetadata.queueTrigger` and then `'unknown'` — a name that does
|
|
@@ -112,7 +80,7 @@ base64-encoded (Azure requires it), `delay` is milliseconds mapped to
|
|
|
112
80
|
`AZURE_QUEUE_NAME_<SCREAMING_SNAKE>` when that variable exists, otherwise used
|
|
113
81
|
as-is.
|
|
114
82
|
|
|
115
|
-
|
|
83
|
+
## Timer
|
|
116
84
|
|
|
117
85
|
The timer handler runs **every** scheduled task registered in the bundle,
|
|
118
86
|
ignoring both the `Timer` argument and each task's own cron expression. Unlike
|
|
@@ -120,7 +88,7 @@ the Lambda equivalent it does not catch per-task failures, so the first task
|
|
|
120
88
|
that throws aborts the ones after it — keep one schedule per function app, or
|
|
121
89
|
guard the task bodies yourself.
|
|
122
90
|
|
|
123
|
-
|
|
91
|
+
## Logging
|
|
124
92
|
|
|
125
93
|
`new AzInvocationLogger(context)` forwards to the invocation context's
|
|
126
94
|
`info`/`warn`/`error`/`debug`/`trace`. `setLevel()` is a **no-op**: every level
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Cloudflare Workers
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
yarn add @pikku/cloudflare
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Worker entry
|
|
8
|
+
|
|
9
|
+
`@pikku/cloudflare` ships the handler factories the deploy codegen emits — use
|
|
10
|
+
them rather than hand-rolling an `ExportedHandler`. Each returns a
|
|
11
|
+
`WorkerEntrypoint` class that sets services up on every invocation (cached after
|
|
12
|
+
the first) and adds an RPC-callable `runRpc(name, args)`:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { createCloudflareHandler } from '@pikku/cloudflare'
|
|
16
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
17
|
+
import './.pikku/pikku-bootstrap.gen.js'
|
|
18
|
+
|
|
19
|
+
export default createCloudflareHandler(
|
|
20
|
+
{ createConfig, createSingletonServices },
|
|
21
|
+
['fetch', 'scheduled']
|
|
22
|
+
)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Factory | For |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `createCloudflareHandler(factories, handlerTypes)` | combined `fetch`/`queue`/`scheduled` |
|
|
28
|
+
| `createCloudflareWorkerHandler(factories)` | HTTP / agent / RPC / workflow-orchestrator units |
|
|
29
|
+
| `createCloudflareCronHandler(factories)` | cron units |
|
|
30
|
+
| `createCloudflareQueueHandler(factories)` | queue-consumer and workflow-step units |
|
|
31
|
+
| `createCloudflareMCPHandler(factories)` | MCP units (same HTTP transport) |
|
|
32
|
+
| `createCloudflareWebSocketHandler(factories)` | channel units, delegating to the DO |
|
|
33
|
+
|
|
34
|
+
`factories` is `{ createConfig, createSingletonServices, createPlatformServices? }`.
|
|
35
|
+
|
|
36
|
+
## Service setup — do not hand-roll this
|
|
37
|
+
|
|
38
|
+
Cloudflare passes env bindings per-request, so services are built from `env`
|
|
39
|
+
rather than at module load. `setupServices(env, factories)` is exported from
|
|
40
|
+
`@pikku/cloudflare` and is what the factories call:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { setupServices } from '@pikku/cloudflare'
|
|
44
|
+
|
|
45
|
+
const services = await setupServices(env, {
|
|
46
|
+
createConfig,
|
|
47
|
+
createSingletonServices,
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Beyond building `LocalVariablesService` / `LocalSecretService` and caching the
|
|
52
|
+
result, it calls `setSingletonServices()` — and the core runners (`fetchData`,
|
|
53
|
+
`runQueueJob`, `runScheduled`) resolve services through that global slot, _not_
|
|
54
|
+
through the value you were returned. A setup function that only returns the
|
|
55
|
+
services leaves every request throwing "Singleton services not initialized" as a
|
|
56
|
+
CF `1101`. It also stashes the env via `setCloudflareEnv`, which
|
|
57
|
+
`getCloudflareEnv()` reads for bindings.
|
|
58
|
+
|
|
59
|
+
## HTTP
|
|
60
|
+
|
|
61
|
+
`runFetch(request, websocketHibernationServer?, options?)`:
|
|
62
|
+
|
|
63
|
+
- A `GET` with `Upgrade: websocket` is routed to the hibernation server. Without
|
|
64
|
+
one passed in it answers **426**, so a channel worker that forgets the second
|
|
65
|
+
argument fails every upgrade while plain HTTP keeps working.
|
|
66
|
+
- `CF-Ray` becomes the traceId when present, so a Cloudflare trace and a Pikku
|
|
67
|
+
trace line up without extra wiring.
|
|
68
|
+
- `options.exposeErrors` defaults to **`false`** — error detail is withheld from
|
|
69
|
+
responses unless you opt in.
|
|
70
|
+
|
|
71
|
+
## Scheduled tasks
|
|
72
|
+
|
|
73
|
+
`runScheduled(controller)` matches registered tasks against `controller.cron`
|
|
74
|
+
and **returns after the first match**. Two tasks sharing one cron expression
|
|
75
|
+
means only one of them ever runs — give each its own expression, or invoke
|
|
76
|
+
`runScheduledTask({ name })` per task yourself.
|
|
77
|
+
|
|
78
|
+
## WebSocket (Durable Objects)
|
|
79
|
+
|
|
80
|
+
The ready-made DO class is exported; re-export it under the binding name and
|
|
81
|
+
point the worker at it:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
export { PikkuWebSocketHibernationServer as WebSocketHibernationServer } from '@pikku/cloudflare'
|
|
85
|
+
export default createCloudflareWebSocketHandler({
|
|
86
|
+
createConfig,
|
|
87
|
+
createSingletonServices,
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Subclass `CloudflareWebSocketHibernationServer` only when you need something
|
|
92
|
+
`getParams()` cannot express — it is abstract with one method returning
|
|
93
|
+
`{ singletonServices, createWireServices? }`. The channel store
|
|
94
|
+
(`CloudflareWebsocketStore` over the DO's own storage), the event hub and the
|
|
95
|
+
channel handler factory are all built by the base class; do not supply them.
|
|
96
|
+
|
|
97
|
+
The router looks up the DO through the **`WEBSOCKET_HIBERNATION_SERVER`**
|
|
98
|
+
binding and answers `503` naming it if the binding is missing, so declare it in
|
|
99
|
+
`wrangler.toml` under exactly that name.
|
|
100
|
+
|
|
101
|
+
A throw during `onConnect` closes the socket with `1008` and answers `403
|
|
102
|
+
Forbidden` with a deliberately generic body — an auth denial and a genuine fault
|
|
103
|
+
look identical to the client. The real reason is on the logger, so read the
|
|
104
|
+
worker logs rather than the status code.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Express
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
yarn add @pikku/express
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## Standalone server
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { PikkuExpressServer } from '@pikku/express'
|
|
11
|
+
import './.pikku/pikku-bootstrap.gen.js'
|
|
12
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
13
|
+
|
|
14
|
+
const config = await createConfig()
|
|
15
|
+
const singletonServices = await createSingletonServices(config)
|
|
16
|
+
|
|
17
|
+
const appServer = new PikkuExpressServer(
|
|
18
|
+
{ ...config, port: 4002, hostname: 'localhost' },
|
|
19
|
+
singletonServices.logger
|
|
20
|
+
)
|
|
21
|
+
appServer.enableExitOnSigInt()
|
|
22
|
+
await appServer.init()
|
|
23
|
+
await appServer.start()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The config extends `CoreConfig` with `port`, `hostname`, an optional
|
|
27
|
+
`healthCheckPath`, an optional `limits` map and an optional `content`
|
|
28
|
+
(`LocalContentConfig`) for static assets and file uploads. `app: Express` is
|
|
29
|
+
exposed for custom middleware, and `getHttpServer()` returns the underlying
|
|
30
|
+
`http.Server` — to attach a WebSocket server, say — but throws before `start()`.
|
|
31
|
+
|
|
32
|
+
`enableStaticAssets()` serves `content.localFileUploadPath` under
|
|
33
|
+
`content.assetUrlPrefix`, and `enableReaper()` adds a `PUT /reaper/*path` upload
|
|
34
|
+
sink for local development, path-traversal checked and bounded by
|
|
35
|
+
`content.sizeLimit` (default `1mb`). Both throw when `content` is unset.
|
|
36
|
+
|
|
37
|
+
## Ordering, and what `init` installs for you
|
|
38
|
+
|
|
39
|
+
The health check is registered in the **constructor**, so it answers before any
|
|
40
|
+
middleware you add and cannot be wrapped in auth. It defaults to
|
|
41
|
+
`/health-check`; override with `healthCheckPath`.
|
|
42
|
+
|
|
43
|
+
Everything else is installed by `init()`: `express.json`, `express.text` (for
|
|
44
|
+
`text/xml`), `express.urlencoded`, `cookie-parser`, then the Pikku middleware.
|
|
45
|
+
Call `enableCors` **before** `init` if you want CORS applied to Pikku's routes.
|
|
46
|
+
|
|
47
|
+
Express buffers the body before Pikku sees it, so the parser limit is the only
|
|
48
|
+
place an oversized request can actually be stopped. `httpOptions.maxBodySize`
|
|
49
|
+
therefore feeds those parser limits, with an explicit `config.limits` entry
|
|
50
|
+
(`json` / `xml` / `urlencoded`) still winning. Everything defaults to `1mb`.
|
|
51
|
+
|
|
52
|
+
`init` passes `logRoutes: true` and `loadSchemas: true` by default; your
|
|
53
|
+
`httpOptions` spread over them, so you can turn either off.
|
|
54
|
+
|
|
55
|
+
`stop()` throws if the server was never started. `enableExitOnSigInt()`
|
|
56
|
+
installs a SIGINT handler that stops the singleton services, then the server,
|
|
57
|
+
then exits 0.
|
|
58
|
+
|
|
59
|
+
## Middleware (existing Express app)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
yarn add @pikku/express-middleware
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import express from 'express'
|
|
67
|
+
import { pikkuExpressMiddleware } from '@pikku/express-middleware'
|
|
68
|
+
import './.pikku/pikku-bootstrap.gen.js'
|
|
69
|
+
|
|
70
|
+
const app = express()
|
|
71
|
+
app.use(express.json())
|
|
72
|
+
app.use(cookieParser())
|
|
73
|
+
app.use(
|
|
74
|
+
pikkuExpressMiddleware({
|
|
75
|
+
logger: singletonServices.logger,
|
|
76
|
+
logRoutes: true,
|
|
77
|
+
loadSchemas: true,
|
|
78
|
+
// plus any RunHTTPWiringOptions: maxBodySize, respondWith404, coerceDataFromSchema
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Options beyond `logger` are all optional: `logRoutes` logs the wiring table once
|
|
84
|
+
at startup, `loadSchemas` compiles every schema up front, and the rest are
|
|
85
|
+
`RunHTTPWiringOptions` passed through per request.
|
|
86
|
+
|
|
87
|
+
On your own app **you** own the parser stack — the middleware reads `req.body`,
|
|
88
|
+
so a body parser and `cookie-parser` must be registered before it, and
|
|
89
|
+
`maxBodySize` alone will not stop an oversized request that your parser already
|
|
90
|
+
accepted. Unmatched requests fall through to `next()` (unless `respondWith404`
|
|
91
|
+
is set), so Pikku's routes coexist with your existing ones; a streaming response
|
|
92
|
+
is the exception and does not call `next()`.
|
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
name: pikku-deploy-fastify
|
|
3
|
-
description: >-
|
|
4
|
-
Use when deploying a Pikku app with Fastify. Covers PikkuFastifyServer standalone and
|
|
5
|
-
pikkuFastifyPlugin for existing Fastify apps. TRIGGER when: code imports @pikku/fastify or
|
|
6
|
-
@pikku/fastify-plugin, user mentions Fastify deployment, or start.ts creates a
|
|
7
|
-
PikkuFastifyServer. DO NOT TRIGGER when: just defining functions/wirings without
|
|
8
|
-
Fastify-specific code.
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Pikku Fastify Deployment
|
|
12
|
-
|
|
13
|
-
## Agent Operating Procedure
|
|
14
|
-
|
|
15
|
-
Use this skill as an execution checklist, not reference material.
|
|
16
|
-
|
|
17
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
18
|
-
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.
|
|
19
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
20
|
-
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.
|
|
21
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
22
|
-
|
|
23
|
-
## Standalone Server
|
|
1
|
+
# Fastify
|
|
24
2
|
|
|
25
3
|
```bash
|
|
26
4
|
yarn add @pikku/fastify
|
|
27
5
|
```
|
|
28
6
|
|
|
7
|
+
## Standalone server
|
|
8
|
+
|
|
29
9
|
```typescript
|
|
30
10
|
import { PikkuFastifyServer } from '@pikku/fastify'
|
|
31
11
|
import './.pikku/pikku-bootstrap.gen.js'
|
|
@@ -43,16 +23,12 @@ await appServer.init()
|
|
|
43
23
|
await appServer.start()
|
|
44
24
|
```
|
|
45
25
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
**Config extends CoreConfig with:** `port`, `hostname`, `healthCheckPath?`
|
|
49
|
-
|
|
50
|
-
**Methods:** `init(httpOptions?: RunHTTPWiringOptions)`, `start()`, `stop()`, `enableExitOnSigInt()`
|
|
51
|
-
|
|
52
|
-
**Property:** `app: FastifyInstance` — Direct access to Fastify instance.
|
|
26
|
+
The config extends `CoreConfig` with `port`, `hostname` and an optional
|
|
27
|
+
`healthCheckPath`. `app: FastifyInstance` is exposed for direct access.
|
|
53
28
|
|
|
54
|
-
`enableCors` exists on the class but **throws `Method not implemented.`** —
|
|
55
|
-
the Express server. Register `@fastify/cors` on `app` yourself before
|
|
29
|
+
`enableCors` exists on the class but **throws `Method not implemented.`** —
|
|
30
|
+
unlike the Express server. Register `@fastify/cors` on `app` yourself before
|
|
31
|
+
`init()`.
|
|
56
32
|
|
|
57
33
|
Unlike the Express server, the health check is registered by `init()`, not the
|
|
58
34
|
constructor, so nothing answers before `init` runs. `init` also passes
|
|
@@ -1,30 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
name: pikku-deploy-lambda
|
|
3
|
-
description: >-
|
|
4
|
-
Use when deploying a Pikku app to AWS Lambda. Covers HTTP handlers, scheduled tasks, SQS queue
|
|
5
|
-
workers, WebSocket via API Gateway, and cold start caching. TRIGGER when: code imports
|
|
6
|
-
@pikku/lambda, user mentions Lambda/serverless/AWS deployment, or handler files export
|
|
7
|
-
Lambda-typed functions. DO NOT TRIGGER when: just defining functions/wirings without
|
|
8
|
-
Lambda-specific code.
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Pikku AWS Lambda Deployment
|
|
12
|
-
|
|
13
|
-
## Agent Operating Procedure
|
|
14
|
-
|
|
15
|
-
Use this skill as an execution checklist, not reference material.
|
|
16
|
-
|
|
17
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
18
|
-
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.
|
|
19
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
20
|
-
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.
|
|
21
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
1
|
+
# AWS Lambda
|
|
22
2
|
|
|
23
3
|
```bash
|
|
24
4
|
yarn add @pikku/lambda
|
|
25
5
|
```
|
|
26
6
|
|
|
27
|
-
## Cold
|
|
7
|
+
## Cold start pattern
|
|
28
8
|
|
|
29
9
|
Cache singleton services across Lambda invocations:
|
|
30
10
|
|
|
@@ -53,7 +33,7 @@ named exports (`handler`, `queue`, `scheduled`, or `connect`/`disconnect`/
|
|
|
53
33
|
`default`) that `serverless.yml` references. Hand-written handlers are for cases
|
|
54
34
|
the codegen does not cover.
|
|
55
35
|
|
|
56
|
-
## HTTP
|
|
36
|
+
## HTTP handler
|
|
57
37
|
|
|
58
38
|
Pick the entry point that matches the API Gateway payload version — they take
|
|
59
39
|
different event types and are not interchangeable:
|
|
@@ -91,7 +71,7 @@ carried.
|
|
|
91
71
|
Neither takes `RunHTTPWiringOptions` — there is no `maxBodySize` or
|
|
92
72
|
`respondWith404` knob here; API Gateway's own payload limit is the bound.
|
|
93
73
|
|
|
94
|
-
## Scheduled
|
|
74
|
+
## Scheduled tasks
|
|
95
75
|
|
|
96
76
|
```typescript
|
|
97
77
|
import type { ScheduledHandler } from 'aws-lambda'
|
|
@@ -113,7 +93,7 @@ Reach for `runScheduledTask({ name })` from `@pikku/core/scheduler` directly
|
|
|
113
93
|
only when one Lambda genuinely bundles several tasks that must fire on separate
|
|
114
94
|
schedules.
|
|
115
95
|
|
|
116
|
-
## SQS
|
|
96
|
+
## SQS queue worker
|
|
117
97
|
|
|
118
98
|
```typescript
|
|
119
99
|
import type { SQSHandler } from 'aws-lambda'
|
|
@@ -181,8 +161,7 @@ is a separate invocation, so nothing survives in memory between `$connect` and
|
|
|
181
161
|
`LambdaEventHubService` handles cross-connection messaging and takes
|
|
182
162
|
`(logger, event, channelStore, eventHubStore)` — the `event` is needed to derive
|
|
183
163
|
the API Gateway Management endpoint, so it is constructed per invocation, not
|
|
184
|
-
once at cold start. It also needs an `EventHubStore` alongside the channel
|
|
185
|
-
store.
|
|
164
|
+
once at cold start. It also needs an `EventHubStore` alongside the channel store.
|
|
186
165
|
|
|
187
166
|
Two behaviours to design around: **binary payloads throw** (`Binary data is not
|
|
188
167
|
supported on serverless lambdas`), and any `PostToConnection` failure removes
|
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
name: pikku-deploy-nextjs
|
|
3
|
-
description: >-
|
|
4
|
-
Use when deploying a Pikku app with Next.js. Covers API route handlers, server-side data
|
|
5
|
-
fetching, and RPC calls from Server Components. TRIGGER when: code imports @pikku/next, user
|
|
6
|
-
mentions Next.js integration, or app/api route files use pikkuAPIRequest. DO NOT TRIGGER when:
|
|
7
|
-
just defining functions/wirings without Next.js-specific code.
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# Pikku Next.js Deployment
|
|
11
|
-
|
|
12
|
-
## Agent Operating Procedure
|
|
13
|
-
|
|
14
|
-
Use this skill as an execution checklist, not reference material.
|
|
15
|
-
|
|
16
|
-
1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
|
|
17
|
-
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.
|
|
18
|
-
3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
|
|
19
|
-
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.
|
|
20
|
-
5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
|
|
1
|
+
# Next.js
|
|
21
2
|
|
|
22
3
|
```bash
|
|
23
4
|
yarn add @pikku/next
|
|
24
5
|
```
|
|
25
6
|
|
|
26
|
-
## API
|
|
7
|
+
## API route handler
|
|
27
8
|
|
|
28
9
|
The CLI generates a typed wrapper. Use it in a catch-all route:
|
|
29
10
|
|
|
@@ -49,7 +30,7 @@ choose. It also passes no `RunHTTPWiringOptions`: to set `maxBodySize` or
|
|
|
49
30
|
`respondWith404` you need your own handler over `new PikkuNextJS(...)` calling
|
|
50
31
|
`apiRequest(req, options)`.
|
|
51
32
|
|
|
52
|
-
## Server-
|
|
33
|
+
## Server-side data fetching
|
|
53
34
|
|
|
54
35
|
Use the generated `pikku()` helper in Server Components or Server Actions:
|
|
55
36
|
|
|
@@ -69,12 +50,9 @@ const config = await staticGet('/config')
|
|
|
69
50
|
const result = await rpc('calculateTax', { amount: 100, region: 'US' })
|
|
70
51
|
```
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
so they force the component dynamic
|
|
76
|
-
- `staticGet`, `staticPost`, `staticRPC` — no request context, safe for
|
|
77
|
-
precompile/ISR
|
|
53
|
+
`get`, `post`, `patch`, `del` and `rpc` read `next/headers` cookies and headers,
|
|
54
|
+
so they force the component dynamic. `staticGet`, `staticPost` and `staticRPC`
|
|
55
|
+
have no request context and are safe for precompile/ISR.
|
|
78
56
|
|
|
79
57
|
The static variants pass `skipUserSession: true`, so a wiring that expects a
|
|
80
58
|
session sees none. That is the real difference — not just where they can run.
|
|
@@ -85,7 +63,7 @@ Both paths run with `bubbleErrors: true`, so a failing wiring **throws** in your
|
|
|
85
63
|
Server Component rather than resolving to an error status. Wrap the call, or let
|
|
86
64
|
the Next.js error boundary take it.
|
|
87
65
|
|
|
88
|
-
## How
|
|
66
|
+
## How it works
|
|
89
67
|
|
|
90
68
|
`PikkuNextJS` lazy-initializes on first request:
|
|
91
69
|
|
|
@@ -95,8 +73,6 @@ import { PikkuNextJS } from '@pikku/next'
|
|
|
95
73
|
const pikku = new PikkuNextJS(createConfig, createSingletonServices)
|
|
96
74
|
```
|
|
97
75
|
|
|
98
|
-
**Constructor:** `new PikkuNextJS(createConfig | undefined, createSingletonServices)`
|
|
99
|
-
|
|
100
76
|
Both arguments are positional and `createConfig` is only optional in the sense
|
|
101
77
|
that passing `undefined` substitutes an empty config —
|
|
102
78
|
`createSingletonServices` is required.
|
|
@@ -113,8 +89,8 @@ route definitions.
|
|
|
113
89
|
- **`PikkuNextJSWorkerRPC({ fetcher })`** — same surface as `PikkuNextJS`, but
|
|
114
90
|
every call is dispatched through a `Fetcher` (a Cloudflare service binding, a
|
|
115
91
|
local HTTP client, a fabric dispatcher) instead of loading function code
|
|
116
|
-
in-process. Use it to keep functions out of the SSR bundle. A non-2xx
|
|
117
|
-
|
|
92
|
+
in-process. Use it to keep functions out of the SSR bundle. A non-2xx response
|
|
93
|
+
throws with the status and body text.
|
|
118
94
|
- **`toNextJsAuthHandler(auth)`** — wraps a better-auth instance or handler
|
|
119
95
|
function for an auth route. The three-argument form
|
|
120
96
|
`(pikkuAuthFactory, createConfig, createSingletonServices)` resolves a Pikku
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# uWebSockets.js
|
|
2
|
+
|
|
3
|
+
Highest-throughput option among Pikku's runtimes. Handles both HTTP and
|
|
4
|
+
WebSocket automatically.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
yarn add @pikku/uws
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { PikkuUWSServer } from '@pikku/uws'
|
|
12
|
+
import './.pikku/pikku-bootstrap.gen.js'
|
|
13
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
14
|
+
|
|
15
|
+
const config = await createConfig()
|
|
16
|
+
const singletonServices = await createSingletonServices(config)
|
|
17
|
+
|
|
18
|
+
const appServer = new PikkuUWSServer(
|
|
19
|
+
{ ...config, hostname: 'localhost', port: 4002 },
|
|
20
|
+
singletonServices.logger
|
|
21
|
+
)
|
|
22
|
+
appServer.enableExitOnSigInt()
|
|
23
|
+
await appServer.init()
|
|
24
|
+
await appServer.start()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The config extends `CoreConfig` with `port`, `hostname` and an optional
|
|
28
|
+
`healthCheckPath`. `app: uWS.App` is exposed for direct access.
|
|
29
|
+
|
|
30
|
+
## What the server does and does not give you
|
|
31
|
+
|
|
32
|
+
`init()` registers three things in order: the health check (`healthCheckPath`,
|
|
33
|
+
default `/health-check`), a catch-all `app.any('/*')` HTTP handler, and a
|
|
34
|
+
catch-all `app.ws('/*')` websocket handler. Nothing is registered by the
|
|
35
|
+
constructor, so nothing answers before `init` runs.
|
|
36
|
+
|
|
37
|
+
**There is no `enableCors`, no static assets and no `content` support** — unlike
|
|
38
|
+
the Express server. The class is explicitly a prototyping convenience; for
|
|
39
|
+
anything that needs extra handlers, use `@pikku/uws-handler` directly and treat
|
|
40
|
+
`pikku-uws-server.ts` as the template (that is what its own JSDoc says).
|
|
41
|
+
|
|
42
|
+
`httpOptions` reaches the HTTP handler only. The websocket handler is
|
|
43
|
+
constructed with a fixed `{ logger, logRoutes: true }`, so per-request options
|
|
44
|
+
do not apply to the upgrade path. `loadSchemas` is also never passed by the
|
|
45
|
+
server, so schemas compile lazily on first use rather than at startup — pass
|
|
46
|
+
`loadSchemas: true` in `httpOptions` if you want the startup cost paid up front.
|
|
47
|
+
|
|
48
|
+
`stop()` closes the listen socket and then waits a fixed 2 seconds for
|
|
49
|
+
connections to drain. Called before `start()`, it throws a bare **string**, not
|
|
50
|
+
an `Error`, so `catch (e) { e.message }` reads `undefined`.
|
|
51
|
+
|
|
52
|
+
## Body limits
|
|
53
|
+
|
|
54
|
+
uWS hands over raw chunks with no limit of its own, so the handler counts the
|
|
55
|
+
bytes itself. A request over `maxBodySize` (default `DEFAULT_MAX_BODY_SIZE`) is
|
|
56
|
+
answered `413` with a `PayloadTooLargeError` body, and the chunks are dropped
|
|
57
|
+
rather than concatenated — an oversized request never accumulates in memory. A
|
|
58
|
+
`content-length` header that already exceeds the limit short-circuits before any
|
|
59
|
+
data arrives.
|
|
60
|
+
|
|
61
|
+
## Handlers directly (own uWS app)
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { pikkuHTTPHandler, pikkuWebsocketHandler } from '@pikku/uws-handler'
|
|
65
|
+
|
|
66
|
+
app.any('/*', pikkuHTTPHandler({ logger, logRoutes: true, loadSchemas: true }))
|
|
67
|
+
app.ws('/*', pikkuWebsocketHandler({ logger, logRoutes: true }))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Both take `{ logger, logRoutes?, loadSchemas? } & RunHTTPWiringOptions`.
|
|
71
|
+
|
|
72
|
+
For a WebSocket-only server on the `ws` library instead, see `ws.md`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ws (WebSocket only)
|
|
2
|
+
|
|
3
|
+
`@pikku/ws` connects Pikku's channel system to a Node.js WebSocket server built
|
|
4
|
+
on the [ws](https://github.com/websockets/ws) library. Use it for a
|
|
5
|
+
WebSocket-only server; for HTTP and WebSocket on one port see `uws.md`, and when
|
|
6
|
+
the WebSocket server shares a port with an existing HTTP app see `express.md` or
|
|
7
|
+
`fastify.md`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
yarn add @pikku/ws ws
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The package exports one function, `pikkuWebsocketHandler` — there is no server
|
|
14
|
+
class. You own the `http.Server` and the `WebSocketServer`; the handler attaches
|
|
15
|
+
the upgrade and message plumbing to them.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { DEFAULT_WS_MAX_PAYLOAD, pikkuWebsocketHandler } from '@pikku/ws'
|
|
19
|
+
import { stopSingletonServices } from '@pikku/core'
|
|
20
|
+
import { Server } from 'http'
|
|
21
|
+
import { WebSocketServer } from 'ws'
|
|
22
|
+
|
|
23
|
+
import './.pikku/pikku-bootstrap.gen.js'
|
|
24
|
+
import { createConfig, createSingletonServices } from './services.js'
|
|
25
|
+
|
|
26
|
+
const config = await createConfig()
|
|
27
|
+
const singletonServices = await createSingletonServices(config)
|
|
28
|
+
|
|
29
|
+
const server = new Server()
|
|
30
|
+
const wss = new WebSocketServer({
|
|
31
|
+
noServer: true,
|
|
32
|
+
maxPayload: DEFAULT_WS_MAX_PAYLOAD,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
pikkuWebsocketHandler({
|
|
36
|
+
server,
|
|
37
|
+
wss,
|
|
38
|
+
logger: singletonServices.logger,
|
|
39
|
+
logRoutes: true, // print the wired channels at startup
|
|
40
|
+
loadSchemas: true, // compile input schemas up front
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
server.listen(4002, 'localhost')
|
|
44
|
+
|
|
45
|
+
process.on('SIGINT', async () => {
|
|
46
|
+
await stopSingletonServices()
|
|
47
|
+
wss.close()
|
|
48
|
+
server.close()
|
|
49
|
+
process.exit(0)
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## `noServer: true` is required, not stylistic
|
|
54
|
+
|
|
55
|
+
The handler listens for the HTTP server's own `upgrade` event, opens the channel
|
|
56
|
+
— running Pikku's middleware chain, auth and CORS against the upgrade request
|
|
57
|
+
first — and only then calls `wss.handleUpgrade`. A `WebSocketServer` bound to the
|
|
58
|
+
server directly would take the socket before any of that ran.
|
|
59
|
+
|
|
60
|
+
An upgrade the channel rejects gets the socket destroyed, and an auth failure is
|
|
61
|
+
written as a real HTTP response on the raw socket rather than a silent drop.
|
|
62
|
+
|
|
63
|
+
## Services and the event hub
|
|
64
|
+
|
|
65
|
+
Services come from the bootstrap import and the global singleton registry, which
|
|
66
|
+
is why nothing is passed in. The event hub is taken from
|
|
67
|
+
`singletonServices.eventHub` when it is a `LocalEventHubService`, and a local one
|
|
68
|
+
is created otherwise — so a single-process app gets pub/sub for free, while a
|
|
69
|
+
multi-instance deployment must register a distributed hub.
|
|
70
|
+
|
|
71
|
+
The options type also extends `RunHTTPWiringOptions`, so per-request settings
|
|
72
|
+
such as `respondWith404`, `coerceDataFromSchema` and `bubbleErrors` are accepted
|
|
73
|
+
here too.
|
|
74
|
+
|
|
75
|
+
On shutdown, call `stopSingletonServices()` then close `wss` and `server`.
|